@opengeni/api-router 4.1.0 → 4.1.2-canary.36199476632001
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/dist/app.d.ts +7 -1
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
- package/dist/auth/managed-auth.d.ts +37 -1
- package/dist/auth/signup-funnel-metrics.d.ts +44 -0
- package/dist/{chunk-24C5LPNE.js → chunk-QDIBJSM4.js} +3277 -1509
- package/dist/{chunk-24C5LPNE.js.map → chunk-QDIBJSM4.js.map} +1 -1
- package/dist/http/cors.d.ts +12 -0
- package/dist/http/local-browser-boundary.d.ts +72 -0
- package/dist/http/metrics-listener.d.ts +22 -0
- package/dist/http/registered-route-label.d.ts +16 -0
- package/dist/http/request-source.d.ts +41 -3
- package/dist/http/user-content.d.ts +36 -0
- package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
- package/dist/index.js +90 -31
- package/dist/index.js.map +1 -1
- package/dist/integrations/github-lens-connect.d.ts +1 -1
- package/dist/integrations/oauth-client.d.ts +30 -0
- package/dist/integrations/slack-bot.d.ts +48 -2
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/documents.d.ts +2 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +12 -0
- package/dist/mcp/server.d.ts +35 -0
- package/dist/model-catalog.d.ts +9 -3
- package/dist/observability.d.ts +3 -1
- package/dist/routes/billing.d.ts +27 -0
- package/dist/routes/browser-sessions.d.ts +3 -0
- package/dist/routes/client-errors.d.ts +42 -0
- package/dist/routes/computer-sessions.d.ts +2 -0
- package/dist/routes/github-browser-pages.d.ts +12 -0
- package/dist/routes/insights.d.ts +26 -0
- package/dist/sandbox/auth-callout.d.ts +8 -0
- package/dist/sandbox-file-artifacts.d.ts +17 -1
- package/package.json +19 -19
- package/src/app.ts +124 -12
- package/src/auth/managed-auth-rate-limits.ts +252 -0
- package/src/auth/managed-auth.ts +137 -17
- package/src/auth/signup-funnel-metrics.ts +164 -0
- package/src/http/auth.ts +6 -0
- package/src/http/cors.ts +37 -5
- package/src/http/local-browser-boundary.ts +343 -0
- package/src/http/metrics-listener.ts +57 -0
- package/src/http/registered-route-label.ts +34 -0
- package/src/http/request-source.ts +154 -13
- package/src/http/sse.ts +36 -16
- package/src/http/user-content.ts +158 -0
- package/src/http/websocket-upgrade-dispatch.ts +29 -0
- package/src/index.ts +22 -8
- package/src/integrations/atlassian.ts +7 -3
- package/src/integrations/fiken.ts +9 -3
- package/src/integrations/github-app-connect.ts +12 -2
- package/src/integrations/google-drive.ts +7 -3
- package/src/integrations/oauth-client.ts +99 -12
- package/src/integrations/personal-github.ts +4 -2
- package/src/integrations/provider-oauth.ts +10 -3
- package/src/integrations/slack-bot.ts +43 -1
- package/src/integrations/social-oauth.ts +7 -2
- package/src/mcp/documents.ts +9 -2
- package/src/mcp/files.ts +5 -1
- package/src/mcp/first-party-tool-permissions.ts +6 -0
- package/src/mcp/knowledge-entries.ts +15 -2
- package/src/mcp/server.ts +191 -17
- package/src/mcp-oauth.ts +4 -6
- package/src/model-catalog.ts +30 -3
- package/src/observability.ts +15 -1
- package/src/routes/billing.ts +70 -9
- package/src/routes/browser-sessions.ts +169 -0
- package/src/routes/client-errors.ts +182 -0
- package/src/routes/company-brain.ts +4 -0
- package/src/routes/computer-sessions.ts +37 -9
- package/src/routes/connections.ts +7 -1
- package/src/routes/documents.ts +6 -1
- package/src/routes/editable-artifacts.ts +2 -1
- package/src/routes/enrollments.ts +2 -13
- package/src/routes/files.ts +17 -3
- package/src/routes/github-browser-pages.ts +162 -0
- package/src/routes/github.ts +122 -49
- package/src/routes/insights.ts +70 -6
- package/src/routes/knowledge.ts +24 -1
- package/src/routes/managed-auth-session-sets.ts +26 -6
- package/src/routes/managed-onboarding.ts +23 -19
- package/src/routes/organization-memberships.ts +3 -0
- package/src/routes/scheduled-tasks.ts +9 -2
- package/src/routes/sessions.ts +89 -0
- package/src/routes/workspace-artifacts.ts +9 -0
- package/src/routes/workspace-state.ts +4 -0
- package/src/routes/workspaces.ts +38 -15
- package/src/sandbox/auth-callout.ts +62 -11
- package/src/sandbox/channel-a.ts +8 -0
- package/src/sandbox/viewer.ts +16 -3
- package/src/sandbox-file-artifacts.ts +37 -26
- package/src/site-uploads.ts +8 -1
- package/src/workspace-tool-gateway.ts +1 -0
package/dist/http/cors.d.ts
CHANGED
|
@@ -4,3 +4,15 @@ export declare function validateInteractionRequestOrigin(value: string | undefin
|
|
|
4
4
|
publicBaseUrl?: string | undefined;
|
|
5
5
|
webBaseUrl?: string | undefined;
|
|
6
6
|
}): string | null;
|
|
7
|
+
type BrowserOriginSettings = {
|
|
8
|
+
corsAllowOriginRegex: string;
|
|
9
|
+
publicBaseUrl?: string | undefined;
|
|
10
|
+
webBaseUrl?: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Whether a present `Origin` header names a browser origin this deployment
|
|
14
|
+
* serves its web app from: the CORS allowlist, the public base URL, or the
|
|
15
|
+
* separate web base URL. `null` and malformed values are never allowed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isAllowedBrowserOrigin(value: string, input: BrowserOriginSettings): boolean;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Settings } from "@opengeni/config";
|
|
2
|
+
/**
|
|
3
|
+
* Browser boundary for the unauthenticated local product mode.
|
|
4
|
+
*
|
|
5
|
+
* In `local` access mode every request without credentials acts as the local
|
|
6
|
+
* `dev` user, and the default local sandbox runs agent commands on the host.
|
|
7
|
+
* Binding the API to loopback keeps other devices out, but a web page open in
|
|
8
|
+
* the developer's browser can still reach `http://127.0.0.1:<port>`:
|
|
9
|
+
*
|
|
10
|
+
* - any site can send cross-origin requests to it (CORS only hides the
|
|
11
|
+
* response; a simple POST still runs), and
|
|
12
|
+
* - a DNS-rebinding site can make its own hostname resolve to 127.0.0.1 and
|
|
13
|
+
* read responses as a same-origin page.
|
|
14
|
+
*
|
|
15
|
+
* The boundary therefore sorts the `Host` a request is addressed to into two
|
|
16
|
+
* classes:
|
|
17
|
+
*
|
|
18
|
+
* - Browser hosts: loopback, the hosts of the configured web, public, and
|
|
19
|
+
* extra origins, the GitHub App callback tunnel, and a specific API bind
|
|
20
|
+
* address. A present `Origin` must be this stack's web app, an explicitly
|
|
21
|
+
* configured origin, or the API's own address on one of these hosts.
|
|
22
|
+
* - Sandbox hosts: names only sandboxes and the worker use to reach the API
|
|
23
|
+
* (`host.docker.internal` with the Docker sandbox, and the hosts of
|
|
24
|
+
* `OPENGENI_MCP_URL` and `OPENGENI_MCP_INTERNAL_URL`). They serve only the
|
|
25
|
+
* sandbox routes (Codemode, first-party MCP, and the Git broker) and refuse
|
|
26
|
+
* any request that carries browser metadata. A browser cannot tell a
|
|
27
|
+
* sandbox name from an attacker's, so a rebinding page on one of these
|
|
28
|
+
* names can neither send an accepted `Origin` nor reach the rest of the API.
|
|
29
|
+
*
|
|
30
|
+
* Every other `Host` is refused. Non-browser clients (the SDK, curl, sandbox
|
|
31
|
+
* callbacks, host-app servers) send no `Origin` and are unaffected. The `Host`
|
|
32
|
+
* port is not compared: a rebinding page controls only the hostname, while
|
|
33
|
+
* local proxies and forwarders legitimately present their own port.
|
|
34
|
+
*
|
|
35
|
+
* It applies only to `local` access mode in the `local` environment (the
|
|
36
|
+
* `bun run dev` stack and manual local runs). Managed and configured access
|
|
37
|
+
* modes, and deployments that set another `OPENGENI_ENVIRONMENT`, keep their
|
|
38
|
+
* existing CORS policy.
|
|
39
|
+
*/
|
|
40
|
+
export type LocalBrowserBoundarySettings = Pick<Settings, "productAccessMode" | "environment" | "apiHost" | "webBaseUrl" | "publicBaseUrl" | "opengeniMcpUrl" | "opengeniMcpInternalUrl" | "githubAppManifestBaseUrl" | "localAllowedOrigins" | "sandboxBackend">;
|
|
41
|
+
export type LocalBrowserBoundaryRejectionCode = "LOCAL_HOST_NOT_ALLOWED" | "LOCAL_ORIGIN_NOT_ALLOWED" | "LOCAL_SANDBOX_ROUTE_ONLY";
|
|
42
|
+
export type LocalBrowserBoundaryRejection = {
|
|
43
|
+
status: 403;
|
|
44
|
+
code: LocalBrowserBoundaryRejectionCode;
|
|
45
|
+
message: string;
|
|
46
|
+
/** The refused `Host` or `Origin`, for the API log only; never sent back. */
|
|
47
|
+
refused: string;
|
|
48
|
+
};
|
|
49
|
+
/** Called once per distinct refused value, so an operator can see why. */
|
|
50
|
+
export type LocalBrowserBoundaryWarn = (message: string, attributes: Record<string, string>) => void;
|
|
51
|
+
export type LocalBrowserBoundary = {
|
|
52
|
+
readonly allowedOrigins: ReadonlySet<string>;
|
|
53
|
+
/** Hostnames a browser may address. */
|
|
54
|
+
readonly browserHostnames: ReadonlySet<string>;
|
|
55
|
+
/** Hostnames only sandboxes and the worker use; sandbox routes only, no browsers. */
|
|
56
|
+
readonly sandboxHostnames: ReadonlySet<string>;
|
|
57
|
+
/** Whether a present `Origin` may call the API for a request with this `Host`. */
|
|
58
|
+
originAllowed(origin: string, host: string | null): boolean;
|
|
59
|
+
/** The rejection for this request, or null when the boundary admits it. */
|
|
60
|
+
rejection(request: Request): LocalBrowserBoundaryRejection | null;
|
|
61
|
+
};
|
|
62
|
+
/** Mark a request the API dispatches to itself so the boundary admits it. */
|
|
63
|
+
export declare function markLocalInternalDispatch(request: Request): Request;
|
|
64
|
+
export declare function localBrowserBoundaryApplies(settings: Pick<Settings, "productAccessMode" | "environment">): boolean;
|
|
65
|
+
/** Build the boundary, or null when the deployment is not the local dev product. */
|
|
66
|
+
export declare function createLocalBrowserBoundary(settings: LocalBrowserBoundarySettings, options?: {
|
|
67
|
+
warn?: LocalBrowserBoundaryWarn;
|
|
68
|
+
}): LocalBrowserBoundary | null;
|
|
69
|
+
/** The standard API error envelope for a boundary rejection outside Hono. */
|
|
70
|
+
export declare function localBrowserBoundaryResponse(rejection: LocalBrowserBoundaryRejection): Response;
|
|
71
|
+
/** The `Host` the boundary checks: the header, else the request URL's authority. */
|
|
72
|
+
export declare function localBrowserRequestHost(request: Request): string | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
type PrometheusSource = {
|
|
4
|
+
prometheusMetrics(): Promise<string>;
|
|
5
|
+
};
|
|
6
|
+
/** Register the Prometheus exposition route on one Hono app. */
|
|
7
|
+
export declare function registerPrometheusMetricsRoute(app: Hono, observability: PrometheusSource): void;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the public API listener serves `GET /metrics`. A deployment that sets
|
|
10
|
+
* `OPENGENI_API_METRICS_PORT` moves the exposition to a dedicated internal
|
|
11
|
+
* listener, so an ingress that forwards every path to the API cannot publish it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function publicListenerServesMetrics(settings: Pick<Settings, "apiMetricsPort">): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The dedicated metrics listener: only `GET /metrics`, behind the same
|
|
16
|
+
* deployment-key perimeter rules as the public listener. Nothing else is
|
|
17
|
+
* routed here, so exposing this port to a scraper exposes nothing else.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createMetricsListenerApp(settings: Settings, observability: PrometheusSource): Hono;
|
|
20
|
+
/** Start the dedicated listener when configured; returns undefined otherwise. */
|
|
21
|
+
export declare function startApiMetricsListener(settings: Settings, observability: PrometheusSource): ReturnType<typeof Bun.serve> | undefined;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
/**
|
|
3
|
+
* The registered Hono path template of the handler that answers this request.
|
|
4
|
+
*
|
|
5
|
+
* Hono resolves every route matching a request before the first middleware
|
|
6
|
+
* runs, so the metrics middleware can read the exact code-owned template (for
|
|
7
|
+
* example `/v1/workspaces/:workspaceId/rigs/:rigId`) without re-implementing
|
|
8
|
+
* routing. Templates come only from route registrations, never from request
|
|
9
|
+
* data, so they are a closed and bounded metric/log label set.
|
|
10
|
+
*
|
|
11
|
+
* Middleware (arity two: `(context, next)`) and global catch-alls never name a
|
|
12
|
+
* route; the first matched terminal handler is the one Hono dispatches to.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registeredHandlerRoutePath(context: Context): string | null;
|
|
15
|
+
/** Normalize a registered template; drop catch-alls that would name nothing. */
|
|
16
|
+
export declare function boundedRegisteredRouteLabel(path: string | null | undefined): string | null;
|
|
@@ -1,14 +1,52 @@
|
|
|
1
|
+
import { type Settings } from "@opengeni/config";
|
|
1
2
|
import type { Context } from "hono";
|
|
2
3
|
declare const TRANSPORT_PEER_ADDRESS_BINDING = "opengeniTransportPeerAddress";
|
|
4
|
+
/** Returned when the runtime supplied no transport peer (for example an
|
|
5
|
+
* in-process test request). Callers treat it as one shared bucket. */
|
|
6
|
+
export declare const UNKNOWN_REQUEST_SOURCE_ADDRESS = "unknown";
|
|
7
|
+
/**
|
|
8
|
+
* App-owned request header carrying the server-resolved client address into
|
|
9
|
+
* Better Auth, which reads client addresses only from request headers. It is
|
|
10
|
+
* removed from every inbound request and re-stamped on managed-auth routes, so
|
|
11
|
+
* a caller-supplied value never reaches the auth rate limiter or session IP.
|
|
12
|
+
*/
|
|
13
|
+
export declare const TRUSTED_CLIENT_ADDRESS_HEADER = "x-opengeni-trusted-client-address";
|
|
3
14
|
type ApiRequestBindings = {
|
|
4
15
|
[TRANSPORT_PEER_ADDRESS_BINDING]?: string | null;
|
|
5
16
|
};
|
|
17
|
+
/** The deployment's declared proxy chain (`OPENGENI_API_TRUSTED_PROXY_*`). */
|
|
18
|
+
export type RequestSourceTrust = Pick<Settings, "apiTrustedProxyHops" | "apiTrustedProxyCidrs">;
|
|
6
19
|
export declare function apiRequestBindingsForTransportPeer(address: string | null | undefined): ApiRequestBindings;
|
|
7
20
|
/**
|
|
8
21
|
* Resolve a quota/audit source from the server-owned transport peer. Forwarded
|
|
9
22
|
* values are considered only when the operator declares an exact trusted proxy
|
|
10
|
-
* hop count
|
|
11
|
-
*
|
|
23
|
+
* hop count (`OPENGENI_API_TRUSTED_PROXY_HOPS`) and, when
|
|
24
|
+
* `OPENGENI_API_TRUSTED_PROXY_CIDRS` is set, only for a transport peer inside
|
|
25
|
+
* those ranges. Each trusted proxy must append the address of the peer that
|
|
26
|
+
* connected to it, or overwrite the header with the original client address;
|
|
27
|
+
* the chain is then walked from the server side so caller-prepended values
|
|
28
|
+
* cannot replace the address observed by the trusted edge. A missing, short,
|
|
29
|
+
* or malformed chain falls back to the transport peer.
|
|
30
|
+
*/
|
|
31
|
+
export declare function trustedRequestSourceAddress(c: Context, trust: RequestSourceTrust): string;
|
|
32
|
+
/**
|
|
33
|
+
* Rate-limit key for a trusted source address. An IPv6 address keys on its
|
|
34
|
+
* /64, the block one subscriber normally holds and the granularity Better
|
|
35
|
+
* Auth applies to the same stamped address, so rotating addresses inside one
|
|
36
|
+
* /64 cannot mint fresh buckets. An IPv4-mapped IPv6 address (how a
|
|
37
|
+
* dual-stack listener reports an IPv4 peer) keys on the embedded IPv4 address
|
|
38
|
+
* instead of collapsing every IPv4 client into one /64. Anything else,
|
|
39
|
+
* including {@link UNKNOWN_REQUEST_SOURCE_ADDRESS}, is returned unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export declare function requestSourceRateLimitKey(address: string): string;
|
|
42
|
+
/** {@link trustedRequestSourceAddress} as an application rate-limit key. */
|
|
43
|
+
export declare function trustedRequestSourceRateLimitKey(c: Context, trust: RequestSourceTrust): string;
|
|
44
|
+
/**
|
|
45
|
+
* Replace any caller-supplied {@link TRUSTED_CLIENT_ADDRESS_HEADER}. When
|
|
46
|
+
* `stamp` is true the header is set to the trusted source address, so every
|
|
47
|
+
* Better Auth request derived from this request (direct handler calls,
|
|
48
|
+
* isolated provider requests, and `auth.api.*` calls built from its headers)
|
|
49
|
+
* keys rate limits and session records on the real client.
|
|
12
50
|
*/
|
|
13
|
-
export declare function
|
|
51
|
+
export declare function replaceTrustedClientAddressHeader(c: Context, trust: RequestSourceTrust, stamp: boolean): void;
|
|
14
52
|
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const USER_CONTENT_SECURITY_POLICY = "default-src 'none'; img-src 'self' data: blob:; media-src 'self' data: blob:; style-src 'unsafe-inline'; sandbox";
|
|
2
|
+
/** Media players only; scripts stay disabled (see the module comment). */
|
|
3
|
+
export declare const USER_MEDIA_CONTENT_SECURITY_POLICY = "default-src 'none'; img-src 'self' data: blob:; media-src 'self' data: blob:; style-src 'unsafe-inline'; sandbox allow-same-origin";
|
|
4
|
+
export declare const USER_CONTENT_SECURITY_HEADERS: Readonly<{
|
|
5
|
+
readonly "Content-Security-Policy": "default-src 'none'; img-src 'self' data: blob:; media-src 'self' data: blob:; style-src 'unsafe-inline'; sandbox";
|
|
6
|
+
readonly "Cross-Origin-Resource-Policy": "same-origin";
|
|
7
|
+
readonly "X-Content-Type-Options": "nosniff";
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* True when a browser would render this type as an active document (HTML,
|
|
11
|
+
* XHTML, SVG, any XML dialect, or multipart replacement streams). Anything that
|
|
12
|
+
* is not a clean `type/subtype` token is treated as active too.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isActiveUserContentType(contentType: string): boolean;
|
|
15
|
+
/** `Content-Disposition: attachment` for active markup; inline types are unchanged. */
|
|
16
|
+
export declare function userContentDispositionHeaders(contentType: string, filename?: string | null): {
|
|
17
|
+
"Content-Disposition": string;
|
|
18
|
+
} | Record<string, never>;
|
|
19
|
+
/**
|
|
20
|
+
* Signed object-storage GET options for one stored object: active markup gets
|
|
21
|
+
* a signed `attachment` response override, so a browser that opens the URL
|
|
22
|
+
* downloads the bytes instead of rendering them with their stored type. The
|
|
23
|
+
* storage endpoint is not always a separate site (local development serves it
|
|
24
|
+
* from loopback, and a preview may route the bucket path on the app origin),
|
|
25
|
+
* so a signed URL follows the same rule as the API routes. Spread into
|
|
26
|
+
* `createGetUrl`; inline types add nothing.
|
|
27
|
+
*/
|
|
28
|
+
export declare function userContentSignedGetUrlOptions(contentType: string, filename?: string | null): {
|
|
29
|
+
responseContentDisposition: string;
|
|
30
|
+
} | Record<string, never>;
|
|
31
|
+
/** True for audio and video types the browser plays in a synthesized media document. */
|
|
32
|
+
export declare function isPlayableMediaUserContentType(contentType: string): boolean;
|
|
33
|
+
/** Sandbox policy for one response; only playable media keeps its origin. */
|
|
34
|
+
export declare function userContentSecurityPolicy(contentType: string): string;
|
|
35
|
+
/** Full header set for one user-content response with the given content type. */
|
|
36
|
+
export declare function userContentResponseHeaders(contentType: string, filename?: string | null): Record<string, string>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type LocalBrowserBoundary } from "./local-browser-boundary.js";
|
|
2
|
+
export type ApiWebSocketUpgradeRoute<Server> = {
|
|
3
|
+
handles(request: Request): boolean;
|
|
4
|
+
upgrade(request: Request, server: Server): Response | undefined;
|
|
5
|
+
};
|
|
6
|
+
export type ApiWebSocketUpgradeDispatch = {
|
|
7
|
+
handled: false;
|
|
8
|
+
} | {
|
|
9
|
+
handled: true;
|
|
10
|
+
response: Response | undefined;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* WebSocket upgrades bypass the Hono app, so the local-mode browser boundary
|
|
14
|
+
* (./local-browser-boundary.ts) is applied here before a transport upgrades.
|
|
15
|
+
* Returns `{ handled: false }` when no transport claims the request, so the
|
|
16
|
+
* caller dispatches it to the app, where the boundary runs as middleware.
|
|
17
|
+
*/
|
|
18
|
+
export declare function dispatchApiWebSocketUpgrade<Server>(request: Request, server: Server, routes: ReadonlyArray<ApiWebSocketUpgradeRoute<Server>>, localBrowserBoundary: LocalBrowserBoundary | null): ApiWebSocketUpgradeDispatch;
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,13 @@ import {
|
|
|
4
4
|
apiRequestBindingsForTransportPeer,
|
|
5
5
|
cleanupScheduledTaskConnectorAuthorization,
|
|
6
6
|
createAppComposition,
|
|
7
|
+
createLocalBrowserBoundary,
|
|
7
8
|
createOpenGeniSlackBotInteractionClient,
|
|
9
|
+
localBrowserBoundaryResponse,
|
|
10
|
+
startApiMetricsListener,
|
|
8
11
|
startSlackInteractionPump,
|
|
9
12
|
startTemporalScheduleCleanupPump
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-QDIBJSM4.js";
|
|
11
14
|
|
|
12
15
|
// src/index.ts
|
|
13
16
|
import { scheduledTaskKnowledgeSource } from "@opengeni/contracts";
|
|
@@ -48,6 +51,15 @@ import {
|
|
|
48
51
|
} from "@temporalio/client";
|
|
49
52
|
|
|
50
53
|
// src/observability.ts
|
|
54
|
+
import {
|
|
55
|
+
natsSubscriptionTerminationCounter
|
|
56
|
+
} from "@opengeni/events";
|
|
57
|
+
function observabilityEventBusOptions(observability) {
|
|
58
|
+
return {
|
|
59
|
+
logger: observabilityEventLogger(observability),
|
|
60
|
+
onSubscriptionTerminated: natsSubscriptionTerminationCounter(observability)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
51
63
|
function observabilityEventLogger(observability) {
|
|
52
64
|
return {
|
|
53
65
|
debug: (message, attributes) => observability.debug(message, eventAttributes(attributes)),
|
|
@@ -76,6 +88,8 @@ function eventAttributeValue(value) {
|
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
// src/sandbox/auth-callout.ts
|
|
91
|
+
import { createHash } from "crypto";
|
|
92
|
+
import { createLogThrottle } from "@opengeni/observability";
|
|
79
93
|
import {
|
|
80
94
|
resolveEnrollmentSigningSecret
|
|
81
95
|
} from "@opengeni/config";
|
|
@@ -96,6 +110,23 @@ var AGENT_CONNECTION_LEASE_MS = 2e4;
|
|
|
96
110
|
// src/sandbox/auth-callout.ts
|
|
97
111
|
var AUTH_CALLOUT_SUBJECT = "$SYS.REQ.USER.AUTH";
|
|
98
112
|
var NATS_USER_JWT_TTL_SECONDS = 5 * 60;
|
|
113
|
+
var AUTH_CALLOUT_DENIAL_WARNING_INTERVAL_MS = 6e4;
|
|
114
|
+
var processDenialWarningThrottle = createLogThrottle({
|
|
115
|
+
intervalMs: AUTH_CALLOUT_DENIAL_WARNING_INTERVAL_MS,
|
|
116
|
+
maxKeys: 1024
|
|
117
|
+
});
|
|
118
|
+
function warnDenial(deps, key, reason, message, attributes = {}) {
|
|
119
|
+
const admission = (deps.denialWarningThrottle ?? processDenialWarningThrottle).admit(key);
|
|
120
|
+
if (!admission) return;
|
|
121
|
+
deps.observability?.warn?.(message, {
|
|
122
|
+
...attributes,
|
|
123
|
+
reason,
|
|
124
|
+
...admission.suppressedCount > 0 ? { suppressedCount: admission.suppressedCount } : {}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function rejectedBearerKey(bearer) {
|
|
128
|
+
return `invalid-bearer:${createHash("sha256").update(bearer).digest("hex").slice(0, 32)}`;
|
|
129
|
+
}
|
|
99
130
|
async function handleAuthorizationRequest(deps, requestBytes) {
|
|
100
131
|
const requestJwt = Buffer.from(requestBytes).toString("utf8");
|
|
101
132
|
const decoded = decodeAuthRequest(requestJwt);
|
|
@@ -122,15 +153,23 @@ async function handleAuthorizationRequest(deps, requestBytes) {
|
|
|
122
153
|
}
|
|
123
154
|
const claims = await verifyEnrollmentBearer(secret, bearer);
|
|
124
155
|
if (!claims) {
|
|
125
|
-
|
|
156
|
+
warnDenial(
|
|
157
|
+
deps,
|
|
158
|
+
rejectedBearerKey(bearer),
|
|
159
|
+
"invalid_bearer",
|
|
160
|
+
"auth-callout: rejected an invalid enrollment bearer"
|
|
161
|
+
);
|
|
126
162
|
return deny("invalid or expired enrollment bearer");
|
|
127
163
|
}
|
|
128
164
|
const enrollment = await getEnrollment(deps.db, claims.workspaceId, claims.enrollmentId);
|
|
129
165
|
if (!enrollment || enrollment.status !== "active") {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
166
|
+
warnDenial(
|
|
167
|
+
deps,
|
|
168
|
+
`inactive:${claims.workspaceId}:${claims.agentId}`,
|
|
169
|
+
"inactive_enrollment",
|
|
170
|
+
"auth-callout: denied a revoked or unknown enrollment",
|
|
171
|
+
{ workspaceId: claims.workspaceId, agentId: claims.agentId }
|
|
172
|
+
);
|
|
134
173
|
return deny("enrollment is not active");
|
|
135
174
|
}
|
|
136
175
|
if (enrollment.workspaceId !== claims.workspaceId || enrollment.id !== claims.enrollmentId || enrollment.id !== claims.agentId || claims.agentId !== claims.enrollmentId || claims.subjectPrefix !== `agent.${claims.workspaceId}.${claims.agentId}`) {
|
|
@@ -151,10 +190,13 @@ async function handleAuthorizationRequest(deps, requestBytes) {
|
|
|
151
190
|
leaseMs: AGENT_CONNECTION_LEASE_MS
|
|
152
191
|
});
|
|
153
192
|
if (!claim.claimed) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
193
|
+
warnDenial(
|
|
194
|
+
deps,
|
|
195
|
+
`duplicate:${claims.workspaceId}:${claims.agentId}`,
|
|
196
|
+
"duplicate_runner",
|
|
197
|
+
"auth-callout: denied a duplicate live runner",
|
|
198
|
+
{ workspaceId: claims.workspaceId, agentId: claims.agentId }
|
|
199
|
+
);
|
|
158
200
|
return deny("another runner instance currently owns this enrollment");
|
|
159
201
|
}
|
|
160
202
|
const permissions = workspaceAgentPermissions(
|
|
@@ -198,7 +240,7 @@ async function startAuthCalloutResponder(deps, natsUrl) {
|
|
|
198
240
|
(bytes) => handleAuthorizationRequest(deps, bytes),
|
|
199
241
|
{
|
|
200
242
|
name: "opengeni-auth-callout",
|
|
201
|
-
...deps.observability ?
|
|
243
|
+
...deps.observability ? observabilityEventBusOptions(deps.observability) : {}
|
|
202
244
|
}
|
|
203
245
|
);
|
|
204
246
|
deps.observability?.info?.("OpenGeni NATS auth-callout responder started", {
|
|
@@ -1249,8 +1291,17 @@ function closeCode(reason) {
|
|
|
1249
1291
|
return 4400;
|
|
1250
1292
|
}
|
|
1251
1293
|
|
|
1294
|
+
// src/http/websocket-upgrade-dispatch.ts
|
|
1295
|
+
function dispatchApiWebSocketUpgrade(request, server, routes, localBrowserBoundary) {
|
|
1296
|
+
const route = routes.find((candidate) => candidate.handles(request));
|
|
1297
|
+
if (!route) return { handled: false };
|
|
1298
|
+
const rejection = localBrowserBoundary?.rejection(request);
|
|
1299
|
+
if (rejection) return { handled: true, response: localBrowserBoundaryResponse(rejection) };
|
|
1300
|
+
return { handled: true, response: route.upgrade(request, server) };
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1252
1303
|
// src/editable-artifact-production.ts
|
|
1253
|
-
import { createHash as
|
|
1304
|
+
import { createHash as createHash5, randomUUID } from "crypto";
|
|
1254
1305
|
import {
|
|
1255
1306
|
editableArtifactMaterializerCapabilitiesForRuntime
|
|
1256
1307
|
} from "@opengeni/artifact-tool/runtime/materializer";
|
|
@@ -1352,7 +1403,7 @@ async function requireFlushBarrier(connection) {
|
|
|
1352
1403
|
}
|
|
1353
1404
|
|
|
1354
1405
|
// src/editable-artifact-native-kernel.ts
|
|
1355
|
-
import { createHash } from "crypto";
|
|
1406
|
+
import { createHash as createHash2 } from "crypto";
|
|
1356
1407
|
import { pathToFileURL } from "url";
|
|
1357
1408
|
import {
|
|
1358
1409
|
createNativeArtifactSession,
|
|
@@ -1476,7 +1527,7 @@ var NativeEditableArtifactKernelAdapter = class {
|
|
|
1476
1527
|
canonicalChunks: fixedChunks(snapshot, input.signal),
|
|
1477
1528
|
canonicalByteSize: snapshot.byteLength,
|
|
1478
1529
|
canonicalContentHash: editableArtifactContentHash(
|
|
1479
|
-
`sha256:${
|
|
1530
|
+
`sha256:${createHash2("sha256").update(snapshot).digest("hex")}`
|
|
1480
1531
|
),
|
|
1481
1532
|
stateHash: editableArtifactStateHash(session.stateHash()),
|
|
1482
1533
|
coveredHeadSequence: 0,
|
|
@@ -1523,7 +1574,7 @@ var NativeEditableArtifactKernelAdapter = class {
|
|
|
1523
1574
|
canonicalChunks: fixedChunks(canonical, input.signal),
|
|
1524
1575
|
canonicalByteSize: canonical.byteLength,
|
|
1525
1576
|
canonicalContentHash: editableArtifactContentHash(
|
|
1526
|
-
`sha256:${
|
|
1577
|
+
`sha256:${createHash2("sha256").update(canonical).digest("hex")}`
|
|
1527
1578
|
),
|
|
1528
1579
|
stateHash: editableArtifactStateHash(session.stateHash()),
|
|
1529
1580
|
coveredHeadSequence: input.state.artifact.headSequence,
|
|
@@ -1570,7 +1621,7 @@ var NativeEditableArtifactKernelAdapter = class {
|
|
|
1570
1621
|
const stateHash = editableArtifactStateHash(session.stateHash());
|
|
1571
1622
|
if (stateHash !== expected.stateHash) throw new Error("Snapshot state hash mismatch");
|
|
1572
1623
|
const canonicalContentHash = editableArtifactContentHash(
|
|
1573
|
-
`sha256:${
|
|
1624
|
+
`sha256:${createHash2("sha256").update(canonical).digest("hex")}`
|
|
1574
1625
|
);
|
|
1575
1626
|
if (session.modality === "spreadsheet") {
|
|
1576
1627
|
const coveredCausalFrontier = domainFrontier(session.frontier());
|
|
@@ -1786,7 +1837,7 @@ async function readVerifiedSnapshot(objects, snapshot, signal) {
|
|
|
1786
1837
|
snapshot.byteSize,
|
|
1787
1838
|
signal
|
|
1788
1839
|
);
|
|
1789
|
-
const contentHash = `sha256:${
|
|
1840
|
+
const contentHash = `sha256:${createHash2("sha256").update(bytes).digest("hex")}`;
|
|
1790
1841
|
if (contentHash !== snapshot.contentHash) throw new Error("Snapshot content hash mismatch");
|
|
1791
1842
|
await object.assertUnchanged(signal);
|
|
1792
1843
|
return bytes;
|
|
@@ -1822,7 +1873,7 @@ async function* fixedChunks(bytes, signal) {
|
|
|
1822
1873
|
}
|
|
1823
1874
|
}
|
|
1824
1875
|
function namespaceForArtifact(artifactId) {
|
|
1825
|
-
const digest =
|
|
1876
|
+
const digest = createHash2("sha256").update(artifactId).digest();
|
|
1826
1877
|
let value = 0n;
|
|
1827
1878
|
for (let index = 0; index < 8; index += 1) value = value << 8n | BigInt(digest[index]);
|
|
1828
1879
|
return value === 0n ? 1n : value;
|
|
@@ -1846,7 +1897,7 @@ function throwIfAborted(signal) {
|
|
|
1846
1897
|
}
|
|
1847
1898
|
|
|
1848
1899
|
// src/editable-artifact-office-import.ts
|
|
1849
|
-
import { createHash as
|
|
1900
|
+
import { createHash as createHash3 } from "crypto";
|
|
1850
1901
|
import {
|
|
1851
1902
|
ArtifactOfficeSourceUnsupportedError,
|
|
1852
1903
|
prepareArtifactOfficeImport
|
|
@@ -2012,7 +2063,7 @@ async function readVerifiedWorkspaceFile(storage, file, signal) {
|
|
|
2012
2063
|
throw new EditableArtifactOfficeImportError("source_changed");
|
|
2013
2064
|
}
|
|
2014
2065
|
const bytes = new Uint8Array(file.sizeBytes);
|
|
2015
|
-
const digest =
|
|
2066
|
+
const digest = createHash3("sha256");
|
|
2016
2067
|
for (let start = 0; start < file.sizeBytes; start += MAX_BOUNDED_OBJECT_CHUNK_BYTES) {
|
|
2017
2068
|
throwIfAborted2(signal);
|
|
2018
2069
|
const end = Math.min(file.sizeBytes, start + MAX_BOUNDED_OBJECT_CHUNK_BYTES) - 1;
|
|
@@ -2073,7 +2124,7 @@ function throwIfAborted2(signal) {
|
|
|
2073
2124
|
}
|
|
2074
2125
|
|
|
2075
2126
|
// src/editable-artifact-workspace-files.ts
|
|
2076
|
-
import { createHash as
|
|
2127
|
+
import { createHash as createHash4 } from "crypto";
|
|
2077
2128
|
import { completeFileUpload, prepareGeneratedWorkspaceFile } from "@opengeni/db";
|
|
2078
2129
|
import { retryWhileMissing as retryWhileMissing2 } from "@opengeni/storage";
|
|
2079
2130
|
var UPLOAD_INTENT_TTL_MS = 60 * 6e4;
|
|
@@ -2209,7 +2260,7 @@ function plainSha256(value) {
|
|
|
2209
2260
|
return match[1];
|
|
2210
2261
|
}
|
|
2211
2262
|
function deterministicUuid(seed) {
|
|
2212
|
-
const bytes =
|
|
2263
|
+
const bytes = createHash4("sha256").update(seed, "utf8").digest().subarray(0, 16);
|
|
2213
2264
|
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
2214
2265
|
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
2215
2266
|
const hex = bytes.toString("hex");
|
|
@@ -2484,7 +2535,7 @@ function snapshotBytesPort(objects) {
|
|
|
2484
2535
|
throw new Error("Editable artifact snapshot content type mismatch");
|
|
2485
2536
|
}
|
|
2486
2537
|
const bytes = new Uint8Array(snapshot.byteSize);
|
|
2487
|
-
const digest =
|
|
2538
|
+
const digest = createHash5("sha256");
|
|
2488
2539
|
let offset = 0;
|
|
2489
2540
|
for await (const chunk of object.chunks({
|
|
2490
2541
|
chunkBytes: MAX_BOUNDED_OBJECT_CHUNK_BYTES2
|
|
@@ -2947,7 +2998,7 @@ async function startApi(options = {}) {
|
|
|
2947
2998
|
() => createNatsEventBus(
|
|
2948
2999
|
settings.natsUrl,
|
|
2949
3000
|
controlPlaneAuth ? { user: controlPlaneAuth.user, pass: controlPlaneAuth.password } : void 0,
|
|
2950
|
-
|
|
3001
|
+
observabilityEventBusOptions(observability)
|
|
2951
3002
|
),
|
|
2952
3003
|
{
|
|
2953
3004
|
...retryOptions,
|
|
@@ -3012,17 +3063,22 @@ async function startApi(options = {}) {
|
|
|
3012
3063
|
const interactionFrameProxies = new InteractionFrameProxyTransport(
|
|
3013
3064
|
resolveFirstPartyDelegationSecret(settings)
|
|
3014
3065
|
);
|
|
3066
|
+
const localBrowserBoundary = createLocalBrowserBoundary(settings, {
|
|
3067
|
+
warn: (message, attributes) => observability.warn(message, attributes)
|
|
3068
|
+
});
|
|
3069
|
+
const webSocketUpgrades = [interactionFrameProxies, artifactWebSockets];
|
|
3015
3070
|
const server = Bun.serve({
|
|
3016
3071
|
hostname: settings.apiHost,
|
|
3017
3072
|
port: settings.apiPort,
|
|
3018
3073
|
idleTimeout: 255,
|
|
3019
3074
|
fetch: (request, bunServer) => {
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3075
|
+
const upgrade = dispatchApiWebSocketUpgrade(
|
|
3076
|
+
request,
|
|
3077
|
+
bunServer,
|
|
3078
|
+
webSocketUpgrades,
|
|
3079
|
+
localBrowserBoundary
|
|
3080
|
+
);
|
|
3081
|
+
if (upgrade.handled) return upgrade.response;
|
|
3026
3082
|
return app.fetch(
|
|
3027
3083
|
request,
|
|
3028
3084
|
apiRequestBindingsForTransportPeer(bunServer.requestIP(request)?.address)
|
|
@@ -3037,6 +3093,7 @@ async function startApi(options = {}) {
|
|
|
3037
3093
|
close: (socket) => socket.data.transportClosed()
|
|
3038
3094
|
}
|
|
3039
3095
|
});
|
|
3096
|
+
const metricsServer = startApiMetricsListener(settings, observability);
|
|
3040
3097
|
const stopSlackInteractionPump = settings.slackSigningSecret ? startSlackInteractionPump(routeDeps) : void 0;
|
|
3041
3098
|
const stopMemorySlackPublicationPump = startMemorySlackPublicationPump(routeDeps);
|
|
3042
3099
|
const stopTemporalScheduleCleanupPump = startTemporalScheduleCleanupPump({
|
|
@@ -3085,12 +3142,14 @@ async function startApi(options = {}) {
|
|
|
3085
3142
|
}
|
|
3086
3143
|
observability.info("OpenGeni API listening", {
|
|
3087
3144
|
host: settings.apiHost,
|
|
3088
|
-
port: settings.apiPort
|
|
3145
|
+
port: settings.apiPort,
|
|
3146
|
+
...metricsServer ? { metricsPort: settings.apiMetricsPort } : {}
|
|
3089
3147
|
});
|
|
3090
3148
|
return {
|
|
3091
3149
|
server,
|
|
3092
3150
|
close: async () => {
|
|
3093
3151
|
server.stop(true);
|
|
3152
|
+
metricsServer?.stop(true);
|
|
3094
3153
|
stopSlackInteractionPump?.();
|
|
3095
3154
|
await stopMemorySlackPublicationPump();
|
|
3096
3155
|
stopMetricsIngestion?.();
|