@standardagents/builder 0.41.7 → 0.41.9
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/built-in-routes.js +52 -40
- package/dist/built-in-routes.js.map +1 -1
- package/dist/{discovery-CIGNVJfG.d.ts → discovery-D5pmuazY.d.ts} +1 -1
- package/dist/{index-PSu6cMEz.d.ts → index-ChHAVogf.d.ts} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +367 -16
- package/dist/index.js.map +1 -1
- package/dist/packing.d.ts +3 -3
- package/dist/plugin.js +9 -1
- package/dist/plugin.js.map +1 -1
- package/dist/resolver-DWuGxI4w.d.ts +4557 -0
- package/dist/runtime.d.ts +10 -4519
- package/dist/runtime.js +359 -16
- package/dist/runtime.js.map +1 -1
- package/dist/test.d.ts +2 -2
- package/dist/{types-D1ABHvAH.d.ts → types-Dhd0M369.d.ts} +1 -1
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _standardagents_spec from '@standardagents/spec';
|
|
2
2
|
import { ThreadState, ErrorOrigin, ToolResult as ToolResult$1, VariableDefinition, HookName, HookSignatures, HookMessage, HookToolCall, HookToolResult, CodeExecutionOptions, CodeExecutionResult, InjectMessageInput, QueueMessageInput, SubagentRegistryEntry, SubagentRelationship, PackageSignature, ProviderUsagePricing, ThreadUser, AgentDefinition, SkillMetadata, SkillFileInput, ControllerContext, Controller, ThreadEndpointHandler } from '@standardagents/spec';
|
|
3
|
-
import { N as NamespacedRegistry } from './types-
|
|
3
|
+
import { N as NamespacedRegistry } from './types-Dhd0M369.js';
|
|
4
4
|
import { DurableObjectStorage } from '@cloudflare/workers-types';
|
|
5
5
|
import { ZodObject, ZodRawShape } from 'zod';
|
|
6
6
|
|
|
@@ -1979,4 +1979,4 @@ declare function defineController<Env extends ThreadEnv = ThreadEnv>(controller:
|
|
|
1979
1979
|
*/
|
|
1980
1980
|
declare function createThreadEndpointHandler<Env extends ThreadEnv = ThreadEnv>(handler: ThreadEndpointHandler): BuilderController<Env>;
|
|
1981
1981
|
|
|
1982
|
-
export { type AuthUser as A, type BuilderController as B, CLIENT_STREAM_CHUNK_BYTES as C, type FlowResult as D, type TelemetryEvent as E, type FlowState as F, type Env as G, type FileRecord as H, type ImageMetadata as I, type AttachmentRef as J, type GrepResult as K, type LLMResponse as L, type Message as M, type FileStats as N, type TextContentPart as O, type ImageContentPart as P, type MultimodalContent as Q, type RequestContext as R, type StorageBackend as S, type
|
|
1982
|
+
export { type AuthUser as A, type BuilderController as B, CLIENT_STREAM_CHUNK_BYTES as C, type FlowResult as D, type TelemetryEvent as E, type FlowState as F, type Env as G, type FileRecord as H, type ImageMetadata as I, type AttachmentRef as J, type GrepResult as K, type LLMResponse as L, type Message as M, type FileStats as N, type TextContentPart as O, type ImageContentPart as P, type MultimodalContent as Q, type RequestContext as R, type StorageBackend as S, type ThreadEndpointContext as T, type MessageContent as U, type ProxyStreamReceiver as V, type ProxyStreamFrame as W, type ProxyStreamTarget as X, type ProxyStreamOpenResult as Y, type SkillStorageScope as Z, type BuilderControllerContext as a, type ThreadEnv as b, createThreadEndpointHandler as c, defineController as d, type BuilderThreadEndpointHandler as e, authenticate as f, requireAdmin as g, endpointCallerFromAuth as h, type AuthContext as i, callClient as j, callClientStream as k, encodeStreamChunk as l, decodeStreamChunk as m, type ClientToolRequest as n, type ClientToolResponse as o, type ClientStreamRequest as p, type ClientStreamResponse as q, requireAuth as r, supportsClientForwarding as s, type ClientDuplex as t, type ClientForwarding as u, type Agent as v, type ThreadInstance as w, type ThreadMetadata as x, type ToolCall as y, type ToolResult as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ModelDefinition, AgentDefinition } from './
|
|
2
|
-
export { AgentBuilderEnv, AgentBuilderEvent, BroadcastOptions, CodeExecutionBridge, DurableAgentBuilder, DurableThread, DurableUserHub, FlowStateSdk, FlowStateWithSdk, HandlerSide, HubClientInfo, HumanSide, ImageContextConfig, InjectMessageOptions, LLMProviderInterface, NamespaceResolutionError, PromptContent, PromptDefinition, PromptIncludePart, PromptPart, PromptSide, PromptToolConfig, Provider, ProviderContentPart, ProviderFilePart, ProviderImagePart, ProviderRegistry, ProviderTextPart, SessionToolBinding, SessionToolConfig, SideConfig, SubagentRelationship, SubagentToolConfig, SubpromptConfig, ThreadEventPayload, ThreadRegistryEntry, ThreadWatchdog, ThreadWatchdogState, ToolConfig, UpdateThreadParams, User, WatchdogCheckin, WatchdogProbeResult, applyRuntimeCors, buildImageDescription, canAccessThread, cat, createNamespaceContext, emitThreadEvent, enhanceFlowState, exists, find, forceTurn, generateImageDescription, getFileStats, getMessages, getMessagesToSummarize, getShortName, getThumbnail, getUnsummarizedImageAttachments, getVisibleAgentNames, getVisibleModelNames, getVisiblePromptNames, getVisibleToolNames, grep, hasImageAttachments, head, injectMessage, isAdminOnlyApiRoute, isHostedInstance, isPublicApiRoute, isQualifiedName, isSameOriginBrowserRequest, isTrustedLocalDevelopmentRequest, linkFile, mkdir, move, optimizeImageContext, parseQualifiedName, qualifyName, queueTool, readFile, readdir, reloadHistory, rename, replaceImagesWithDescriptions, resolveAgent, resolveHook, resolveModel, resolvePrompt, resolveTool, rmdir, runtimeCorsHeaders, runtimePreflightResponse, stat, tail, unlink, updateThread, writeFile, writeImage } from './
|
|
1
|
+
import { M as ModelDefinition, A as AgentDefinition } from './resolver-DWuGxI4w.js';
|
|
2
|
+
export { m as AgentBuilderEnv, n as AgentBuilderEvent, ah as BroadcastOptions, C as CodeExecutionBridge, h as DurableAgentBuilder, D as DurableThread, l as DurableUserHub, af as FlowStateSdk, ai as FlowStateWithSdk, G as HandlerSide, H as HubClientInfo, E as HumanSide, aq as ImageContextConfig, ae as InjectMessageOptions, as as LLMProviderInterface, aL as NamespaceResolutionError, w as PromptContent, s as PromptDefinition, y as PromptIncludePart, x as PromptPart, F as PromptSide, t as PromptToolConfig, P as Provider, at as ProviderContentPart, aw as ProviderFilePart, av as ProviderImagePart, ar as ProviderRegistry, au as ProviderTextPart, B as SessionToolBinding, z as SessionToolConfig, I as SideConfig, J as SubagentRelationship, u as SubagentToolConfig, S as SubpromptConfig, o as ThreadEventPayload, p as ThreadRegistryEntry, T as ThreadWatchdog, j as ThreadWatchdogState, v as ToolConfig, U as UpdateThreadParams, q as User, k as WatchdogCheckin, W as WatchdogProbeResult, f as applyRuntimeCors, am as buildImageDescription, c as canAccessThread, a9 as cat, aJ as createNamespaceContext, Q as emitThreadEvent, ag as enhanceFlowState, a0 as exists, ad as find, R as forceTurn, ap as generateImageDescription, a7 as getFileStats, N as getMessages, al as getMessagesToSummarize, az as getShortName, a8 as getThumbnail, ak as getUnsummarizedImageAttachments, aG as getVisibleAgentNames, aI as getVisibleModelNames, aH as getVisiblePromptNames, aF as getVisibleToolNames, ac as grep, aj as hasImageAttachments, aa as head, L as injectMessage, d as isAdminOnlyApiRoute, i as isHostedInstance, b as isPublicApiRoute, aK as isQualifiedName, e as isSameOriginBrowserRequest, a as isTrustedLocalDevelopmentRequest, Z as linkFile, a2 as mkdir, a5 as move, ao as optimizeImageContext, ax as parseQualifiedName, ay as qualifyName, K as queueTool, _ as readFile, a3 as readdir, O as reloadHistory, a6 as rename, an as replaceImagesWithDescriptions, aD as resolveAgent, aE as resolveHook, aC as resolveModel, aB as resolvePrompt, aA as resolveTool, a4 as rmdir, r as runtimeCorsHeaders, g as runtimePreflightResponse, $ as stat, ab as tail, a1 as unlink, V as updateThread, X as writeFile, Y as writeImage } from './resolver-DWuGxI4w.js';
|
|
3
3
|
export { AgentPluginOptions, agentbuilder, builder } from './plugin.js';
|
|
4
|
-
export {
|
|
4
|
+
export { v as Agent, J as AttachmentRef, i as AuthContext, A as AuthUser, e as BuilderThreadEndpointHandler, C as CLIENT_STREAM_CHUNK_BYTES, t as ClientDuplex, u as ClientForwarding, p as ClientStreamRequest, q as ClientStreamResponse, n as ClientToolRequest, o as ClientToolResponse, B as Controller, a as ControllerContext, G as Env, H as FileRecord, N as FileStats, D as FlowResult, F as FlowState, K as GrepResult, P as ImageContentPart, I as ImageMetadata, L as LLMResponse, M as Message, U as MessageContent, Q as MultimodalContent, R as RequestContext, S as StorageBackend, E as TelemetryEvent, O as TextContentPart, T as ThreadEndpointContext, b as ThreadEnv, w as ThreadInstance, x as ThreadMetadata, y as ToolCall, z as ToolResult, f as authenticate, j as callClient, k as callClientStream, c as createThreadEndpointHandler, m as decodeStreamChunk, d as defineController, l as encodeStreamChunk, h as endpointCallerFromAuth, g as requireAdmin, r as requireAuth, s as supportsClientForwarding } from './index-ChHAVogf.js';
|
|
5
5
|
export { AgentType, DefineHandlerOptions, DefinitionLoader, GlobalNamespaceContext, HANDLER_SYMBOL, HandlerDefinition, HookExecutionContext, HookExecutionSource, HookSignatures, ImageContent, ModelCapabilities, ModelProvider, NamespaceContext, PackageSignature, PackedExports, PackedMeta, PackedMetadata, PackedNamespaceContext, ParentHookRequirement, ParentHookStage, PromptInput, PromptTextPart, ProviderAssistantMessage, ProviderConfigSlot, ProviderConfigSlots, ProviderConfigValueSource, ProviderDefinition, ProviderError, ProviderErrorCode, ProviderFactory, ProviderFactoryConfig, ProviderFinishReason, ProviderGeneratedImage, ProviderMessage, ProviderMessageContent, ProviderMethodOverrides, ModelCapabilities as ProviderModelCapabilities, ProviderOverride, ProviderOverrideContext, ProviderReasoningDetail, ProviderRequest, ProviderResponse, ProviderStreamChunk, ProviderSyncOverride, ProviderSystemMessage, ProviderTool, ProviderToolCallPart, ProviderToolMessage, ProviderToolResultContent, ProviderUsage, ProviderUserMessage, ReasoningConfig, StructuredPrompt, TextContent, ThreadHookName, Tool, ToolArgs, ToolArgsNode, ToolArgsRawShape, ToolContent, belongsToPackage, defineAgent, defineHandler, defineHook, defineModel, definePrompt, defineProvider, defineTool, isHandler, isPacked, isVisibleInNamespace, mapReasoningLevel, providerConst, providerEnv, providerValue } from '@standardagents/spec';
|
|
6
|
-
export { C as ConstituentItem, D as DiscoveredPackage, N as NamespacedRegistry, P as PackingAnalysis, a as PackingOptions, b as PackingResult,
|
|
7
|
-
export { D as DiscoveryConfig, P as PackageDiscoveryService, d as discoverPackages } from './discovery-
|
|
6
|
+
export { C as ConstituentItem, D as DiscoveredPackage, N as NamespacedRegistry, P as PackingAnalysis, a as PackingOptions, b as PackingResult, U as UnpackOptions, c as UnpackResult } from './types-Dhd0M369.js';
|
|
7
|
+
export { D as DiscoveryConfig, P as PackageDiscoveryService, d as discoverPackages } from './discovery-D5pmuazY.js';
|
|
8
8
|
import 'cloudflare:workers';
|
|
9
9
|
import 'vite';
|
|
10
10
|
import '@cloudflare/workers-types';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProviderModelPricing } from '@standardagents/provider-pricing';
|
|
2
2
|
import { jsonPathReader } from '@formkit/jsonreader';
|
|
3
|
-
import { ProviderError, mapReasoningLevel, validateAgentDefinition, THREAD_STREAM_REASONING_PARAM, isUserEndpoint } from '@standardagents/spec';
|
|
3
|
+
import { ProviderError, mapReasoningLevel, validateAgentDefinition, THREAD_STREAM_REASONING_PARAM, isUserRoom, isUserEndpoint } from '@standardagents/spec';
|
|
4
4
|
export { HANDLER_SYMBOL, ProviderError, belongsToPackage, defineAgent, defineHandler, defineHook, defineModel, definePrompt, defineProvider, defineTool, isHandler, isPacked, isVisibleInNamespace, mapReasoningLevel, providerConst, providerEnv, providerValue } from '@standardagents/spec';
|
|
5
5
|
import 'virtual:@standardagents-providers';
|
|
6
6
|
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
|
|
@@ -37709,6 +37709,8 @@ import {
|
|
|
37709
37709
|
applyRuntimeCors,
|
|
37710
37710
|
runtimeCorsHeaders,
|
|
37711
37711
|
runtimePreflightResponse,
|
|
37712
|
+
formatUpstreamTiming,
|
|
37713
|
+
withUpstreamTiming,
|
|
37712
37714
|
} from "@standardagents/builder/runtime";
|
|
37713
37715
|
import { isThreadEndpoint, getEndpointAccess } from "@standardagents/spec";
|
|
37714
37716
|
|
|
@@ -37833,13 +37835,19 @@ ${packedThreadRouteCode}
|
|
|
37833
37835
|
// thread id, for same-request reuse by controllers (AB-05). Never cached
|
|
37834
37836
|
// across requests.
|
|
37835
37837
|
let threadAccess = null;
|
|
37838
|
+
// The request handed to the controller. A protected API request carries
|
|
37839
|
+
// x-standard-upstream-timing with the authentication cost; the header is
|
|
37840
|
+
// overwritten, never merged, so a caller cannot forge upstream time.
|
|
37841
|
+
let controllerRequest = request;
|
|
37836
37842
|
|
|
37837
37843
|
// Require authentication for all API routes except public ones.
|
|
37838
37844
|
// WebSocket tickets are single-use and are consumed here. Controllers
|
|
37839
37845
|
// must reuse context.auth (resolveRequestAuth) rather than calling
|
|
37840
37846
|
// requireAuth again, or a ticket-only upgrade will 401.
|
|
37841
37847
|
if (isApiRoute && !publicRoute) {
|
|
37848
|
+
const authStartedAt = Date.now();
|
|
37842
37849
|
const authResult = await requireAuth(request, env);
|
|
37850
|
+
controllerRequest = withUpstreamTiming(request, formatUpstreamTiming('auth', Date.now() - authStartedAt));
|
|
37843
37851
|
|
|
37844
37852
|
// If requireAuth returns a Response, it's an error (401)
|
|
37845
37853
|
if (authResult instanceof Response) {
|
|
@@ -37896,7 +37904,7 @@ ${packedThreadRouteCode}
|
|
|
37896
37904
|
}
|
|
37897
37905
|
|
|
37898
37906
|
const context = {
|
|
37899
|
-
req:
|
|
37907
|
+
req: controllerRequest,
|
|
37900
37908
|
params: routeMatch.params || {},
|
|
37901
37909
|
env: env,
|
|
37902
37910
|
url: url,
|
|
@@ -39380,6 +39388,9 @@ var UserStateImpl = class {
|
|
|
39380
39388
|
host;
|
|
39381
39389
|
/** Caller descriptor for grant `allow` policy checks. */
|
|
39382
39390
|
caller;
|
|
39391
|
+
/** One identity lookup per execution: the singleton RPC runs at most once. */
|
|
39392
|
+
identityLookup = null;
|
|
39393
|
+
identityLookupMs = null;
|
|
39383
39394
|
constructor(userId, host, caller) {
|
|
39384
39395
|
this.userId = userId;
|
|
39385
39396
|
this.host = host;
|
|
@@ -39391,6 +39402,22 @@ var UserStateImpl = class {
|
|
|
39391
39402
|
return ns.get(ns.idFromName("singleton"));
|
|
39392
39403
|
}
|
|
39393
39404
|
async identity() {
|
|
39405
|
+
if (!this.identityLookup) {
|
|
39406
|
+
const started = Date.now();
|
|
39407
|
+
this.identityLookup = this.lookupIdentity().finally(() => {
|
|
39408
|
+
this.identityLookupMs = Date.now() - started;
|
|
39409
|
+
});
|
|
39410
|
+
this.identityLookup.catch(() => {
|
|
39411
|
+
this.identityLookup = null;
|
|
39412
|
+
});
|
|
39413
|
+
}
|
|
39414
|
+
return this.identityLookup;
|
|
39415
|
+
}
|
|
39416
|
+
/** Milliseconds the execution spent resolving identity, once it has. */
|
|
39417
|
+
timing() {
|
|
39418
|
+
return { identity_ms: this.identityLookupMs };
|
|
39419
|
+
}
|
|
39420
|
+
async lookupIdentity() {
|
|
39394
39421
|
const user = await this.singleton().getUserById(this.userId);
|
|
39395
39422
|
if (!user) throw new Error(`Unknown user: ${this.userId}`);
|
|
39396
39423
|
return {
|
|
@@ -39422,6 +39449,12 @@ var UserStateImpl = class {
|
|
|
39422
39449
|
emit(event, data) {
|
|
39423
39450
|
this.host.broadcastUserEvent(this.userId, event, data);
|
|
39424
39451
|
}
|
|
39452
|
+
async clients() {
|
|
39453
|
+
return this.host.listRoomClients(this.userId);
|
|
39454
|
+
}
|
|
39455
|
+
async send(clientId, frame) {
|
|
39456
|
+
return this.host.sendToRoomClient(this.userId, clientId, frame);
|
|
39457
|
+
}
|
|
39425
39458
|
async listThreads(options) {
|
|
39426
39459
|
const { threads, total } = await this.singleton().listThreads({
|
|
39427
39460
|
user_id: this.userId,
|
|
@@ -39489,6 +39522,107 @@ function createExecutionChain() {
|
|
|
39489
39522
|
};
|
|
39490
39523
|
}
|
|
39491
39524
|
|
|
39525
|
+
// src/users/upstream-timing.ts
|
|
39526
|
+
var UPSTREAM_TIMING_HEADER = "x-standard-upstream-timing";
|
|
39527
|
+
var MAX_HEADER_CHARS = 512;
|
|
39528
|
+
function formatUpstreamTiming(name, durationMs) {
|
|
39529
|
+
const safe = Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 0;
|
|
39530
|
+
return `${name};dur=${Math.round(safe * 10) / 10}`;
|
|
39531
|
+
}
|
|
39532
|
+
function appendUpstreamTiming(current, name, durationMs) {
|
|
39533
|
+
const entry = formatUpstreamTiming(name, durationMs);
|
|
39534
|
+
if (!current) return entry;
|
|
39535
|
+
const combined = `${current}, ${entry}`;
|
|
39536
|
+
return combined.length > MAX_HEADER_CHARS ? current : combined;
|
|
39537
|
+
}
|
|
39538
|
+
function withUpstreamTiming(request, value) {
|
|
39539
|
+
const headers = new Headers(request.headers);
|
|
39540
|
+
headers.set(UPSTREAM_TIMING_HEADER, value);
|
|
39541
|
+
return new Request(request, { headers });
|
|
39542
|
+
}
|
|
39543
|
+
function responseWithUpstreamTiming(response, name, durationMs) {
|
|
39544
|
+
if (response.status === 101) return response;
|
|
39545
|
+
const value = appendUpstreamTiming(response.headers.get(UPSTREAM_TIMING_HEADER), name, durationMs);
|
|
39546
|
+
try {
|
|
39547
|
+
response.headers.set(UPSTREAM_TIMING_HEADER, value);
|
|
39548
|
+
return response;
|
|
39549
|
+
} catch {
|
|
39550
|
+
const headers = new Headers(response.headers);
|
|
39551
|
+
headers.set(UPSTREAM_TIMING_HEADER, value);
|
|
39552
|
+
return new Response(response.body, {
|
|
39553
|
+
status: response.status,
|
|
39554
|
+
statusText: response.statusText,
|
|
39555
|
+
headers
|
|
39556
|
+
});
|
|
39557
|
+
}
|
|
39558
|
+
}
|
|
39559
|
+
|
|
39560
|
+
// src/durable-objects/DurableUserHub.ts
|
|
39561
|
+
init_bridge_owner();
|
|
39562
|
+
|
|
39563
|
+
// src/users/room.ts
|
|
39564
|
+
var ROOM_FRAME_MAX_BYTES = 1024 * 1024;
|
|
39565
|
+
var ROOM_FRAME_RATE_PER_SECOND = 200;
|
|
39566
|
+
var ROOM_FRAME_BURST = 400;
|
|
39567
|
+
var ROOM_SEND_BUDGET_BYTES = 8 * 1024 * 1024;
|
|
39568
|
+
var ROOM_SEND_BUDGET_WINDOW_MS = 1e4;
|
|
39569
|
+
var ROOM_PRESENCE_GRACE_MS = 3e4;
|
|
39570
|
+
var ROOM_CLOSE_SUPERSEDED = 4001;
|
|
39571
|
+
var ROOM_CLOSE_TOO_LARGE = 1009;
|
|
39572
|
+
var ROOM_CLOSE_TRY_AGAIN = 1013;
|
|
39573
|
+
function parseRoomFrame(message) {
|
|
39574
|
+
const size = typeof message === "string" ? message.length : message.byteLength;
|
|
39575
|
+
if (size > ROOM_FRAME_MAX_BYTES) return "too_large";
|
|
39576
|
+
if (typeof message !== "string") return null;
|
|
39577
|
+
let parsed;
|
|
39578
|
+
try {
|
|
39579
|
+
parsed = JSON.parse(message);
|
|
39580
|
+
} catch {
|
|
39581
|
+
return null;
|
|
39582
|
+
}
|
|
39583
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
39584
|
+
const frame = parsed;
|
|
39585
|
+
if (frame.type !== "room" || typeof frame.id !== "string" || frame.id.length === 0 || frame.id.length > 256) {
|
|
39586
|
+
return null;
|
|
39587
|
+
}
|
|
39588
|
+
return frame;
|
|
39589
|
+
}
|
|
39590
|
+
var RoomRateLimiter = class {
|
|
39591
|
+
constructor(ratePerSecond = ROOM_FRAME_RATE_PER_SECOND, burst = ROOM_FRAME_BURST, now = Date.now()) {
|
|
39592
|
+
this.ratePerSecond = ratePerSecond;
|
|
39593
|
+
this.burst = burst;
|
|
39594
|
+
this.tokens = burst;
|
|
39595
|
+
this.last = now;
|
|
39596
|
+
}
|
|
39597
|
+
tokens;
|
|
39598
|
+
last;
|
|
39599
|
+
take(now = Date.now()) {
|
|
39600
|
+
const elapsed = Math.max(0, now - this.last) / 1e3;
|
|
39601
|
+
this.tokens = Math.min(this.burst, this.tokens + elapsed * this.ratePerSecond);
|
|
39602
|
+
this.last = now;
|
|
39603
|
+
if (this.tokens < 1) return false;
|
|
39604
|
+
this.tokens -= 1;
|
|
39605
|
+
return true;
|
|
39606
|
+
}
|
|
39607
|
+
};
|
|
39608
|
+
var RoomSendBudget = class {
|
|
39609
|
+
constructor(budgetBytes = ROOM_SEND_BUDGET_BYTES, windowMs = ROOM_SEND_BUDGET_WINDOW_MS, now = Date.now()) {
|
|
39610
|
+
this.budgetBytes = budgetBytes;
|
|
39611
|
+
this.windowMs = windowMs;
|
|
39612
|
+
this.windowStart = now;
|
|
39613
|
+
}
|
|
39614
|
+
spent = 0;
|
|
39615
|
+
windowStart;
|
|
39616
|
+
charge(bytes, now = Date.now()) {
|
|
39617
|
+
if (now - this.windowStart >= this.windowMs) {
|
|
39618
|
+
this.windowStart = now;
|
|
39619
|
+
this.spent = 0;
|
|
39620
|
+
}
|
|
39621
|
+
this.spent += bytes;
|
|
39622
|
+
return this.spent <= this.budgetBytes;
|
|
39623
|
+
}
|
|
39624
|
+
};
|
|
39625
|
+
|
|
39492
39626
|
// src/users/grants.ts
|
|
39493
39627
|
var INSTANCE_GRANTS_MAX_BYTES = 64 * 1024;
|
|
39494
39628
|
var INSTANCE_GRANTS_MAX_COUNT = 64;
|
|
@@ -39644,6 +39778,12 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39644
39778
|
clientSockets = /* @__PURE__ */ new Set();
|
|
39645
39779
|
clientInfo = /* @__PURE__ */ new Map();
|
|
39646
39780
|
migrated = false;
|
|
39781
|
+
/** Inbound frame rate and outbound byte budget per room socket. */
|
|
39782
|
+
roomLimits = /* @__PURE__ */ new Map();
|
|
39783
|
+
/** Detach notices held for the presence grace window, by client id. */
|
|
39784
|
+
pendingDetach = /* @__PURE__ */ new Map();
|
|
39785
|
+
/** The project's room definition, loaded once per instance. */
|
|
39786
|
+
roomDefinition = null;
|
|
39647
39787
|
constructor(ctx, env) {
|
|
39648
39788
|
super(ctx, env);
|
|
39649
39789
|
ctx.blockConcurrencyWhile(async () => {
|
|
@@ -39656,7 +39796,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39656
39796
|
this.clientInfo.set(ws, {
|
|
39657
39797
|
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
39658
39798
|
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
39659
|
-
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null
|
|
39799
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
39800
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
39660
39801
|
});
|
|
39661
39802
|
}
|
|
39662
39803
|
}
|
|
@@ -39729,24 +39870,188 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39729
39870
|
if (request.headers.get("Upgrade") === "websocket") {
|
|
39730
39871
|
const pair = new WebSocketPair();
|
|
39731
39872
|
const [client, server] = [pair[0], pair[1]];
|
|
39732
|
-
const info = {
|
|
39733
|
-
clientId: url.searchParams.get("client_id"),
|
|
39734
|
-
clientName: url.searchParams.get("client_name"),
|
|
39735
|
-
clientKind: url.searchParams.get("client_kind")
|
|
39736
|
-
};
|
|
39873
|
+
const info = { ...parseBridgeClientParams(url).info, since: Date.now() };
|
|
39737
39874
|
this.ensureHeartbeatAutoResponse();
|
|
39738
39875
|
this.ctx.acceptWebSocket(server);
|
|
39739
39876
|
server.serializeAttachment({ type: "hub_client", ...info });
|
|
39877
|
+
const reconnecting = info.clientId !== null && this.supersedeClient(info.clientId, server);
|
|
39740
39878
|
this.clientSockets.add(server);
|
|
39741
39879
|
this.clientInfo.set(server, info);
|
|
39742
39880
|
try {
|
|
39743
39881
|
server.send(JSON.stringify({ type: "hub_ready" }));
|
|
39744
39882
|
} catch {
|
|
39745
39883
|
}
|
|
39884
|
+
if (info.clientId) {
|
|
39885
|
+
const connection = this.connectionOf(info);
|
|
39886
|
+
if (connection) this.observeAttach(connection, reconnecting);
|
|
39887
|
+
}
|
|
39746
39888
|
return new Response(null, { status: 101, webSocket: client });
|
|
39747
39889
|
}
|
|
39748
39890
|
return new Response("Not found", { status: 404 });
|
|
39749
39891
|
}
|
|
39892
|
+
// ─── Room connections (spec: Users rooms) ───────────────────────────────────
|
|
39893
|
+
connectionOf(info) {
|
|
39894
|
+
if (!info?.clientId) return null;
|
|
39895
|
+
return { clientId: info.clientId, clientKind: info.clientKind, since: info.since ?? 0 };
|
|
39896
|
+
}
|
|
39897
|
+
/** Close every other socket presenting `clientId`; true when one existed. */
|
|
39898
|
+
supersedeClient(clientId, keep) {
|
|
39899
|
+
let superseded = false;
|
|
39900
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
39901
|
+
if (ws === keep || this.clientInfo.get(ws)?.clientId !== clientId) continue;
|
|
39902
|
+
this.forgetSocket(ws);
|
|
39903
|
+
superseded = true;
|
|
39904
|
+
try {
|
|
39905
|
+
ws.send(JSON.stringify({ type: "superseded" }));
|
|
39906
|
+
} catch {
|
|
39907
|
+
}
|
|
39908
|
+
try {
|
|
39909
|
+
ws.close(ROOM_CLOSE_SUPERSEDED, "Superseded by a newer connection from this client");
|
|
39910
|
+
} catch {
|
|
39911
|
+
}
|
|
39912
|
+
}
|
|
39913
|
+
return superseded;
|
|
39914
|
+
}
|
|
39915
|
+
forgetSocket(ws) {
|
|
39916
|
+
this.clientSockets.delete(ws);
|
|
39917
|
+
this.clientInfo.delete(ws);
|
|
39918
|
+
this.roomLimits.delete(ws);
|
|
39919
|
+
}
|
|
39920
|
+
liveSocketFor(clientId) {
|
|
39921
|
+
for (const ws of this.clientSockets) {
|
|
39922
|
+
if (this.clientInfo.get(ws)?.clientId === clientId && ws.readyState === WebSocket.OPEN) return ws;
|
|
39923
|
+
}
|
|
39924
|
+
return null;
|
|
39925
|
+
}
|
|
39926
|
+
/** Live room connections, one per client id, oldest attachment first. */
|
|
39927
|
+
listRoomClients() {
|
|
39928
|
+
const byId = /* @__PURE__ */ new Map();
|
|
39929
|
+
for (const ws of this.clientSockets) {
|
|
39930
|
+
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
39931
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
39932
|
+
if (connection) byId.set(connection.clientId, connection);
|
|
39933
|
+
}
|
|
39934
|
+
return Array.from(byId.values()).sort((a, b) => a.since - b.since);
|
|
39935
|
+
}
|
|
39936
|
+
/** Deliver one frame to the socket carrying `clientId`, within its send budget. */
|
|
39937
|
+
sendToRoomClient(clientId, frame) {
|
|
39938
|
+
const ws = this.liveSocketFor(clientId);
|
|
39939
|
+
if (!ws) return false;
|
|
39940
|
+
return this.sendRoomFrame(ws, JSON.stringify(frame));
|
|
39941
|
+
}
|
|
39942
|
+
sendRoomFrame(ws, payload) {
|
|
39943
|
+
const limits = this.limitsFor(ws);
|
|
39944
|
+
if (!limits.send.charge(payload.length)) {
|
|
39945
|
+
this.forgetSocket(ws);
|
|
39946
|
+
try {
|
|
39947
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Slow consumer: send budget exceeded");
|
|
39948
|
+
} catch {
|
|
39949
|
+
}
|
|
39950
|
+
return false;
|
|
39951
|
+
}
|
|
39952
|
+
try {
|
|
39953
|
+
ws.send(payload);
|
|
39954
|
+
return true;
|
|
39955
|
+
} catch {
|
|
39956
|
+
this.forgetSocket(ws);
|
|
39957
|
+
return false;
|
|
39958
|
+
}
|
|
39959
|
+
}
|
|
39960
|
+
limitsFor(ws) {
|
|
39961
|
+
let limits = this.roomLimits.get(ws);
|
|
39962
|
+
if (!limits) {
|
|
39963
|
+
limits = { rate: new RoomRateLimiter(), send: new RoomSendBudget() };
|
|
39964
|
+
this.roomLimits.set(ws, limits);
|
|
39965
|
+
}
|
|
39966
|
+
return limits;
|
|
39967
|
+
}
|
|
39968
|
+
/** Load the project's room definition from the users registry, once. */
|
|
39969
|
+
async loadRoom() {
|
|
39970
|
+
if (!this.roomDefinition) {
|
|
39971
|
+
this.roomDefinition = (async () => {
|
|
39972
|
+
const registry = this.userEndpoints();
|
|
39973
|
+
for (const key of Object.keys(registry)) {
|
|
39974
|
+
if (!/(^|\/)room$/.test(key)) continue;
|
|
39975
|
+
try {
|
|
39976
|
+
const loaded = await registry[key]();
|
|
39977
|
+
const candidate = loaded.default ?? loaded;
|
|
39978
|
+
if (isUserRoom(candidate)) return candidate;
|
|
39979
|
+
} catch {
|
|
39980
|
+
}
|
|
39981
|
+
}
|
|
39982
|
+
return null;
|
|
39983
|
+
})();
|
|
39984
|
+
}
|
|
39985
|
+
return this.roomDefinition;
|
|
39986
|
+
}
|
|
39987
|
+
/** Whether `key` is room-owned state, exempt from `user_value_changed`. */
|
|
39988
|
+
async roomOwnsKey(key) {
|
|
39989
|
+
const room = await this.loadRoom();
|
|
39990
|
+
return Boolean(room?.valuePrefix && key.startsWith(room.valuePrefix));
|
|
39991
|
+
}
|
|
39992
|
+
/**
|
|
39993
|
+
* Presence runs inside the serialization chain, after any execution the
|
|
39994
|
+
* event raced with, so a room's presence write never interleaves with a
|
|
39995
|
+
* mutation. A reconnect inside the grace window is not a change.
|
|
39996
|
+
*/
|
|
39997
|
+
observeAttach(connection, reconnecting) {
|
|
39998
|
+
const pending = this.pendingDetach.get(connection.clientId);
|
|
39999
|
+
if (pending) {
|
|
40000
|
+
clearTimeout(pending);
|
|
40001
|
+
this.pendingDetach.delete(connection.clientId);
|
|
40002
|
+
return;
|
|
40003
|
+
}
|
|
40004
|
+
if (reconnecting) return;
|
|
40005
|
+
this.dispatchRoomPresence({ kind: "attached", connection, at: Date.now() });
|
|
40006
|
+
}
|
|
40007
|
+
observeDetach(connection) {
|
|
40008
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
40009
|
+
const existing = this.pendingDetach.get(connection.clientId);
|
|
40010
|
+
if (existing) clearTimeout(existing);
|
|
40011
|
+
this.pendingDetach.set(
|
|
40012
|
+
connection.clientId,
|
|
40013
|
+
setTimeout(() => {
|
|
40014
|
+
this.pendingDetach.delete(connection.clientId);
|
|
40015
|
+
if (this.liveSocketFor(connection.clientId)) return;
|
|
40016
|
+
this.dispatchRoomPresence({ kind: "detached", connection, at: Date.now() });
|
|
40017
|
+
}, ROOM_PRESENCE_GRACE_MS)
|
|
40018
|
+
);
|
|
40019
|
+
}
|
|
40020
|
+
dispatchRoomPresence(event) {
|
|
40021
|
+
const run = this.executionChain.run(async () => {
|
|
40022
|
+
const room = await this.loadRoom();
|
|
40023
|
+
if (!room?.onPresence) return;
|
|
40024
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: "presence" });
|
|
40025
|
+
try {
|
|
40026
|
+
await room.onPresence(user, event);
|
|
40027
|
+
} catch (error) {
|
|
40028
|
+
console.error("[DurableUserHub] room presence handler failed:", error);
|
|
40029
|
+
}
|
|
40030
|
+
});
|
|
40031
|
+
this.ctx.waitUntil(run);
|
|
40032
|
+
}
|
|
40033
|
+
/** The hub is one object per user; its name is the user id. */
|
|
40034
|
+
roomUserId() {
|
|
40035
|
+
return this.ctx.id.name ?? this.ctx.id.toString();
|
|
40036
|
+
}
|
|
40037
|
+
async dispatchRoomFrame(ws, frame) {
|
|
40038
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
40039
|
+
if (!connection) return;
|
|
40040
|
+
await this.executionChain.run(async () => {
|
|
40041
|
+
const room = await this.loadRoom();
|
|
40042
|
+
if (!room) {
|
|
40043
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "no_room" }));
|
|
40044
|
+
return;
|
|
40045
|
+
}
|
|
40046
|
+
const user = new UserStateImpl(this.roomUserId(), this.scopeHost(), { kind: "userRoom", name: "frame" });
|
|
40047
|
+
try {
|
|
40048
|
+
await room.onFrame(user, connection, frame);
|
|
40049
|
+
} catch (error) {
|
|
40050
|
+
console.error("[DurableUserHub] room frame handler failed:", error);
|
|
40051
|
+
this.sendRoomFrame(ws, JSON.stringify({ type: "room", id: frame.id, error: "handler_failed" }));
|
|
40052
|
+
}
|
|
40053
|
+
});
|
|
40054
|
+
}
|
|
39750
40055
|
// ─── User scope: event stream (spec: Users §4) ─────────────────────────────
|
|
39751
40056
|
/** Idle sockets answer heartbeats at the edge without waking the object.
|
|
39752
40057
|
* Registered at accept time rather than in the constructor so a hub that
|
|
@@ -39784,8 +40089,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39784
40089
|
return new Response(null, { status: 101, webSocket: client });
|
|
39785
40090
|
}
|
|
39786
40091
|
/**
|
|
39787
|
-
* Deliver an event frame to every socket attached to this user's stream
|
|
39788
|
-
* At-most-once, live-only (spec: Users §4) —
|
|
40092
|
+
* Deliver an event frame to every socket attached to this user's stream
|
|
40093
|
+
* and to every room socket. At-most-once, live-only (spec: Users §4) —
|
|
40094
|
+
* no history, no acks.
|
|
39789
40095
|
*/
|
|
39790
40096
|
broadcastUserEvent(userId, event, data) {
|
|
39791
40097
|
const frame = JSON.stringify({ event, data });
|
|
@@ -39797,6 +40103,10 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39797
40103
|
} catch {
|
|
39798
40104
|
}
|
|
39799
40105
|
}
|
|
40106
|
+
for (const ws of Array.from(this.clientSockets)) {
|
|
40107
|
+
if (ws.readyState !== WebSocket.OPEN || !this.clientInfo.get(ws)?.clientId) continue;
|
|
40108
|
+
this.sendRoomFrame(ws, frame);
|
|
40109
|
+
}
|
|
39800
40110
|
}
|
|
39801
40111
|
// ─── User scope: serialized endpoint execution (spec: Users §2.1, §3) ─────
|
|
39802
40112
|
/** Per-user execution chain (spec: Users §2.1) — see users/serialize.ts. */
|
|
@@ -39807,6 +40117,8 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39807
40117
|
setUserValue: (userId, key, value) => this.setUserValue(userId, key, value),
|
|
39808
40118
|
listUserValues: (userId, options) => this.listUserValues(userId, options ?? {}),
|
|
39809
40119
|
broadcastUserEvent: (userId, event, data) => this.broadcastUserEvent(userId, event, data),
|
|
40120
|
+
listRoomClients: () => this.listRoomClients(),
|
|
40121
|
+
sendToRoomClient: (_userId, clientId, frame) => this.sendToRoomClient(clientId, frame),
|
|
39810
40122
|
grantFetch: (userId, grant, req) => this.grantFetch(userId, grant, req),
|
|
39811
40123
|
env: this.env
|
|
39812
40124
|
};
|
|
@@ -39826,19 +40138,28 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39826
40138
|
}
|
|
39827
40139
|
const module = loaded;
|
|
39828
40140
|
const handler = module.default ?? loaded;
|
|
40141
|
+
if (isUserRoom(handler)) return Response.json({ error: "Not found" }, { status: 404 });
|
|
39829
40142
|
if (!isUserEndpoint(handler)) {
|
|
39830
40143
|
return Response.json(
|
|
39831
40144
|
{ error: `${match.key} is not a defineUserEndpoint export` },
|
|
39832
40145
|
{ status: 500 }
|
|
39833
40146
|
);
|
|
39834
40147
|
}
|
|
40148
|
+
const queuedAt = Date.now();
|
|
39835
40149
|
return this.executionChain.run(async () => {
|
|
40150
|
+
const queueMs = Date.now() - queuedAt;
|
|
40151
|
+
const timed = withUpstreamTiming(
|
|
40152
|
+
request,
|
|
40153
|
+
appendUpstreamTiming(request.headers.get(UPSTREAM_TIMING_HEADER), "queue", queueMs)
|
|
40154
|
+
);
|
|
39836
40155
|
const user = new UserStateImpl(userId, this.scopeHost(), {
|
|
39837
40156
|
kind: "userEndpoint",
|
|
39838
40157
|
name: match.key
|
|
39839
40158
|
});
|
|
39840
40159
|
try {
|
|
39841
|
-
|
|
40160
|
+
const response = await handler.__handler(timed, user, match.params);
|
|
40161
|
+
const identityMs = user.timing().identity_ms;
|
|
40162
|
+
return identityMs === null ? response : responseWithUpstreamTiming(response, "identity", identityMs);
|
|
39842
40163
|
} catch (error) {
|
|
39843
40164
|
return Response.json(
|
|
39844
40165
|
{ error: error instanceof Error ? error.message : "User endpoint failed" },
|
|
@@ -39861,6 +40182,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39861
40182
|
const names = Object.keys(registry);
|
|
39862
40183
|
if (names.length === 0) return;
|
|
39863
40184
|
const run = async () => {
|
|
40185
|
+
if (hookType === "user_value_changed" && await this.roomOwnsKey(String(args[0]))) return;
|
|
39864
40186
|
const user = new UserStateImpl(userId, this.scopeHost(), {
|
|
39865
40187
|
kind: "userHook",
|
|
39866
40188
|
name: hookType
|
|
@@ -39885,6 +40207,9 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39885
40207
|
this.ctx.waitUntil(run());
|
|
39886
40208
|
}
|
|
39887
40209
|
async webSocketClose(ws) {
|
|
40210
|
+
const connection = this.connectionOf(this.clientInfo.get(ws));
|
|
40211
|
+
this.forgetSocket(ws);
|
|
40212
|
+
if (connection) this.observeDetach(connection);
|
|
39888
40213
|
const attachment = ws.deserializeAttachment();
|
|
39889
40214
|
if (attachment?.type === "user_stream") {
|
|
39890
40215
|
this.dispatchUserHooks(String(attachment.userId), "user_client_detached", [
|
|
@@ -39893,10 +40218,7 @@ var DurableUserHub = class extends DurableObject {
|
|
|
39893
40218
|
kind: typeof attachment.clientKind === "string" ? attachment.clientKind : void 0
|
|
39894
40219
|
}
|
|
39895
40220
|
]);
|
|
39896
|
-
return;
|
|
39897
40221
|
}
|
|
39898
|
-
this.clientSockets.delete(ws);
|
|
39899
|
-
this.clientInfo.delete(ws);
|
|
39900
40222
|
}
|
|
39901
40223
|
// ─── User scope: credential grants (spec: Users §6) ───────────────────────
|
|
39902
40224
|
/**
|
|
@@ -40016,8 +40338,37 @@ var DurableUserHub = class extends DurableObject {
|
|
|
40016
40338
|
caller: body.caller ?? { kind: "unknown", name: "unknown" }
|
|
40017
40339
|
});
|
|
40018
40340
|
}
|
|
40019
|
-
async webSocketMessage(ws,
|
|
40020
|
-
|
|
40341
|
+
async webSocketMessage(ws, message) {
|
|
40342
|
+
const attachment = ws.deserializeAttachment();
|
|
40343
|
+
if (attachment?.type !== "hub_client") return;
|
|
40344
|
+
if (!this.clientSockets.has(ws)) {
|
|
40345
|
+
this.clientSockets.add(ws);
|
|
40346
|
+
this.clientInfo.set(ws, {
|
|
40347
|
+
clientId: typeof attachment.clientId === "string" ? attachment.clientId : null,
|
|
40348
|
+
clientName: typeof attachment.clientName === "string" ? attachment.clientName : null,
|
|
40349
|
+
clientKind: typeof attachment.clientKind === "string" ? attachment.clientKind : null,
|
|
40350
|
+
since: typeof attachment.since === "number" ? attachment.since : void 0
|
|
40351
|
+
});
|
|
40352
|
+
}
|
|
40353
|
+
if (!this.limitsFor(ws).rate.take()) {
|
|
40354
|
+
this.forgetSocket(ws);
|
|
40355
|
+
try {
|
|
40356
|
+
ws.close(ROOM_CLOSE_TRY_AGAIN, "Frame rate exceeded");
|
|
40357
|
+
} catch {
|
|
40358
|
+
}
|
|
40359
|
+
return;
|
|
40360
|
+
}
|
|
40361
|
+
const frame = parseRoomFrame(message);
|
|
40362
|
+
if (frame === "too_large") {
|
|
40363
|
+
this.forgetSocket(ws);
|
|
40364
|
+
try {
|
|
40365
|
+
ws.close(ROOM_CLOSE_TOO_LARGE, "Frame exceeds 1 MiB");
|
|
40366
|
+
} catch {
|
|
40367
|
+
}
|
|
40368
|
+
return;
|
|
40369
|
+
}
|
|
40370
|
+
if (frame === null || !this.clientInfo.get(ws)?.clientId) return;
|
|
40371
|
+
await this.dispatchRoomFrame(ws, frame);
|
|
40021
40372
|
}
|
|
40022
40373
|
async webSocketError(ws) {
|
|
40023
40374
|
await this.webSocketClose(ws);
|