1use noxid_execution_ir::{
2 EndpointExecutionBoundary, ExecutionBoundary, ExecutionProgram, QueueExecutionBoundary,
3 TaskExecutionBoundary,
4};
5use noxid_ir::{
6 EndpointInputSection, EndpointKind, EndpointLimitScope, EndpointLimitWindow, SemanticBinaryOp,
7 SemanticExpr, SemanticExprKind, SemanticId, SemanticTemplatePart,
8};
9use noxid_source::js_escape;
10
11mod agents;
12
13pub use agents::AgentRuntimeOptions;
14
15pub const HANDLER_SCHEMA_VERSION: u32 = 8;
16
17const SERVER_LIFECYCLE_EXPORTS: &str =
22 include_str!("../embedded/tools/server-lifecycle-exports.txt");
23
24pub fn server_lifecycle_exports() -> impl Iterator<Item = &'static str> {
28 SERVER_LIFECYCLE_EXPORTS
29 .lines()
30 .filter(|line| !line.is_empty())
31}
32
33pub fn server_lifecycle_export_required(name: &str, tracing_export: ServerTracingExport) -> bool {
34 tracing_export == ServerTracingExport::Otlp
35 || !matches!(
36 name,
37 "flushNoxidTracing" | "abandonNoxidTracing" | "noxidTracingExporterSnapshot"
38 )
39}
40
41fn missing_server_lifecycle_exports(
42 handler: &str,
43 tracing_export: ServerTracingExport,
44) -> Vec<&'static str> {
45 server_lifecycle_exports()
46 .filter(|name| server_lifecycle_export_required(name, tracing_export))
47 .filter(|name| {
48 ![
49 format!("export const {name} "),
50 format!("export function {name}("),
51 format!("export async function {name}("),
52 ]
53 .iter()
54 .any(|declaration| handler.contains(declaration))
55 })
56 .collect()
57}
58
59const LANGUAGE_VALUE_EQUALITY_FUNCTION: &str = r#"function $noxEqual($noxLeft, $noxRight) {
60 if ($noxLeft === $noxRight) return true;
61 if (Array.isArray($noxLeft) || Array.isArray($noxRight)) return Array.isArray($noxLeft) && Array.isArray($noxRight) && $noxLeft.length === $noxRight.length && $noxLeft.every(($noxValue, $noxIndex) => $noxEqual($noxValue, $noxRight[$noxIndex]));
62 if ($noxLeft === null || $noxRight === null || typeof $noxLeft !== "object" || typeof $noxRight !== "object") return false;
63 const $noxLeftKeys = Object.keys($noxLeft).sort();
64 const $noxRightKeys = Object.keys($noxRight).sort();
65 return $noxLeftKeys.length === $noxRightKeys.length && $noxLeftKeys.every(($noxKey, $noxIndex) => $noxKey === $noxRightKeys[$noxIndex] && $noxEqual($noxLeft[$noxKey], $noxRight[$noxKey]));
66}
67"#;
68
69#[derive(Clone, Debug)]
70pub struct ServerJavaScriptOutput {
71 pub handler: String,
72 pub manifest: String,
73 pub server_actions: usize,
74 pub edge_actions: usize,
75}
76
77#[derive(Clone, Copy, Debug, Default)]
78pub struct AgentSurfaceOptions<'a> {
79 pub openapi_json: Option<&'a str>,
80 pub serve_openapi: bool,
81 pub mcp: bool,
82 pub principal_authority_import: Option<&'a str>,
83}
84
85#[derive(Clone, Debug)]
86pub struct ServerRuntimeOptions {
87 pub db_pool: u64,
88 pub pubsub: Option<PubSubRuntimeOptions>,
89 pub development_trace_capture: bool,
90 pub application_namespace: Option<String>,
91 pub models: ModelRuntimeOptions,
94 pub tracing_export: ServerTracingExport,
95 pub tracing_service_name: String,
96 pub otlp_endpoint_secret: bool,
97 pub otlp_headers_secret: bool,
98 pub agents: AgentRuntimeOptions,
101}
102
103#[derive(Clone, Debug, Default, PartialEq, Eq)]
108pub struct ModelRuntimeOptions {
109 pub models: Vec<noxid_model_ir::LoweredModel>,
110 pub type_schemas: Vec<noxid_model_ir::ModelTypeSchema>,
111}
112
113impl ModelRuntimeOptions {
114 pub fn is_empty(&self) -> bool {
115 self.models.is_empty()
116 }
117}
118
119impl Default for ServerRuntimeOptions {
120 fn default() -> Self {
121 Self {
122 db_pool: 10,
123 pubsub: None,
124 development_trace_capture: false,
125 application_namespace: None,
126 models: ModelRuntimeOptions::default(),
127 tracing_export: ServerTracingExport::Stdout,
128 tracing_service_name: "noxid.application".into(),
129 otlp_endpoint_secret: false,
130 otlp_headers_secret: false,
131 agents: AgentRuntimeOptions::default(),
132 }
133 }
134}
135
136#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
137pub enum ServerTracingExport {
138 #[default]
139 Stdout,
140 Otlp,
141}
142
143impl ServerTracingExport {
144 pub fn as_str(self) -> &'static str {
145 match self {
146 Self::Stdout => "stdout",
147 Self::Otlp => "otlp",
148 }
149 }
150}
151
152pub fn server_secrets_prelude_javascript(secrets: &[String]) -> String {
156 if secrets.is_empty() {
157 return "const __NOXID_TRACING_SECRET_FAILURE__ = false;\nconst __noxidServerEnvironment = (environment) => environment;\n".into();
158 }
159 let names = secrets
160 .iter()
161 .map(|name| format!("\"{}\"", js_escape(name)))
162 .collect::<Vec<_>>()
163 .join(", ");
164 format!(
165 r#"const __NOXID_DECLARED_SECRETS__ = Object.freeze([{names}]);
166const __NOXID_SECRET_CACHE__ = Object.create(null);
167let __NOXID_TRACING_SECRET_FAILURE__ = false;
168let __NOXID_TRACING_SECRET_ERROR_LOGGED__ = false;
169function __noxidResolveSecret(name) {{
170 if (Object.hasOwn(__NOXID_SECRET_CACHE__, name)) return __NOXID_SECRET_CACHE__[name];
171 const value = globalThis.process?.env?.[name];
172 if (typeof value !== "string" || value.length === 0) {{
173 if (name === "OTEL_EXPORTER_OTLP_ENDPOINT" || name === "OTEL_EXPORTER_OTLP_HEADERS") {{
174 __NOXID_TRACING_SECRET_FAILURE__ = true;
175 if (!__NOXID_TRACING_SECRET_ERROR_LOGGED__) {{
176 __NOXID_TRACING_SECRET_ERROR_LOGGED__ = true;
177 try {{ console.error(JSON.stringify(Object.freeze({{ schema: "noxid.tracing.error.v1", event: "tracing.export.disabled", code: "TRACING_EXPORT_SECRET_MISSING", exporter: "otlp" }}))); }} catch {{}}
178 }}
179 }}
180 throw new Error("error[SERVER_SECRET_MISSING]: declared server secret " + name + " is missing from the environment");
181 }}
182 Object.defineProperty(__NOXID_SECRET_CACHE__, name, {{ value, enumerable: true }});
183 return value;
184}}
185const __NOXID_SECRET_VALUES__ = Object.create(null);
186for (const name of __NOXID_DECLARED_SECRETS__) {{
187 Object.defineProperty(__NOXID_SECRET_VALUES__, name, {{ get: () => __noxidResolveSecret(name), enumerable: true }});
188}}
189Object.freeze(__NOXID_SECRET_VALUES__);
190function __noxidServerEnvironment(environment) {{
191 if (environment !== null && typeof environment === "object" && Object.getOwnPropertyDescriptor(environment, "secrets")) return environment;
192 const enriched = Object.create(environment ?? null);
193 Object.defineProperty(enriched, "secrets", {{ value: __NOXID_SECRET_VALUES__, enumerable: false }});
194 return enriched;
195}}
196"#
197 )
198}
199
200pub fn otel_span_attribute_mapping() -> &'static [(&'static str, &'static str, Option<&'static str>)]
205{
206 &[
207 ("method", "http.request.method", None),
208 ("route", "http.route", None),
209 ("status", "http.response.status_code", None),
210 ("durationMs", "noxid.duration_ms", None),
211 ("code", "error.type", None),
212 ("semanticId", "noxid.semantic_id", None),
213 ("code", "noxid.diagnostic_code", None),
214 ("capability", "noxid.capability", None),
215 ("state", "noxid.queue.state", Some("queue.state")),
216 ]
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
220pub enum PubSubDriver {
221 Memory,
222 Postgres,
223 Redis,
224}
225
226impl PubSubDriver {
227 fn as_str(self) -> &'static str {
228 match self {
229 Self::Memory => "memory",
230 Self::Postgres => "postgres",
231 Self::Redis => "redis",
232 }
233 }
234}
235
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub struct PubSubRuntimeOptions {
238 pub driver: PubSubDriver,
239 pub coalescing_ms: u64,
240}
241
242#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
243pub enum ServerTracingMode {
244 Off,
245 #[default]
246 Requests,
247 Full,
248}
249
250impl ServerTracingMode {
251 pub fn as_str(self) -> &'static str {
252 match self {
253 Self::Off => "off",
254 Self::Requests => "requests",
255 Self::Full => "full",
256 }
257 }
258}
259
260pub fn generate(
261 program: &ExecutionProgram,
262 host_import: &str,
263 validator_import: &str,
264 middleware_import: &str,
265 base_path: &str,
266 server_secrets: &[String],
267) -> Result<ServerJavaScriptOutput, String> {
268 generate_with_startup(
269 program,
270 host_import,
271 validator_import,
272 middleware_import,
273 None,
274 base_path,
275 server_secrets,
276 )
277}
278
279pub fn generate_with_startup(
280 program: &ExecutionProgram,
281 host_import: &str,
282 validator_import: &str,
283 middleware_import: &str,
284 startup_import: Option<&str>,
285 base_path: &str,
286 server_secrets: &[String],
287) -> Result<ServerJavaScriptOutput, String> {
288 generate_with_agent_surfaces(
289 program,
290 host_import,
291 validator_import,
292 middleware_import,
293 startup_import,
294 base_path,
295 server_secrets,
296 AgentSurfaceOptions::default(),
297 )
298}
299
300#[allow(clippy::too_many_arguments)]
301pub fn generate_with_agent_surfaces(
302 program: &ExecutionProgram,
303 host_import: &str,
304 validator_import: &str,
305 middleware_import: &str,
306 startup_import: Option<&str>,
307 base_path: &str,
308 server_secrets: &[String],
309 agent_surfaces: AgentSurfaceOptions<'_>,
310) -> Result<ServerJavaScriptOutput, String> {
311 generate_with_tracing(
312 program,
313 host_import,
314 validator_import,
315 middleware_import,
316 startup_import,
317 base_path,
318 server_secrets,
319 agent_surfaces,
320 ServerTracingMode::Requests,
321 )
322}
323
324#[allow(clippy::too_many_arguments)]
328pub fn generate_with_tracing(
329 program: &ExecutionProgram,
330 host_import: &str,
331 validator_import: &str,
332 middleware_import: &str,
333 startup_import: Option<&str>,
334 base_path: &str,
335 server_secrets: &[String],
336 agent_surfaces: AgentSurfaceOptions<'_>,
337 tracing_mode: ServerTracingMode,
338) -> Result<ServerJavaScriptOutput, String> {
339 generate_with_runtime_options(
340 program,
341 host_import,
342 validator_import,
343 middleware_import,
344 startup_import,
345 base_path,
346 server_secrets,
347 agent_surfaces,
348 tracing_mode,
349 ServerRuntimeOptions::default(),
350 )
351}
352
353#[allow(clippy::too_many_arguments)]
354pub fn generate_with_runtime_options(
355 program: &ExecutionProgram,
356 host_import: &str,
357 validator_import: &str,
358 middleware_import: &str,
359 startup_import: Option<&str>,
360 base_path: &str,
361 server_secrets: &[String],
362 agent_surfaces: AgentSurfaceOptions<'_>,
363 tracing_mode: ServerTracingMode,
364 runtime_options: ServerRuntimeOptions,
365) -> Result<ServerJavaScriptOutput, String> {
366 let callable = program
367 .boundaries
368 .iter()
369 .filter(|boundary| matches!(boundary.target.as_str(), "server" | "edge"))
370 .collect::<Vec<_>>();
371 let schemas = callable
372 .iter()
373 .map(|boundary| schema_javascript(boundary))
374 .collect::<Vec<_>>()
375 .join(",\n ");
376 let compiled_actions = callable
377 .iter()
378 .filter_map(|boundary| {
379 boundary.body.as_ref().map(|body| {
380 Ok(format!(
381 "\"{}\": async (args) => ({})",
382 js_escape(boundary.action.as_str()),
383 compiler_body_javascript(body)?,
384 ))
385 })
386 })
387 .collect::<Result<Vec<_>, String>>()?
388 .join(",\n ");
389 let endpoint_schemas = program
390 .endpoints
391 .iter()
392 .map(endpoint_schema_javascript)
393 .collect::<Vec<_>>()
394 .join(",\n ");
395 let uses_uploads = program
396 .endpoints
397 .iter()
398 .flat_map(|endpoint| &endpoint.inputs)
399 .any(|input| input.file.is_some());
400 let compiled_endpoints = program
401 .endpoints
402 .iter()
403 .filter_map(|endpoint| {
404 if endpoint.statements.is_empty() {
405 return None;
406 }
407 Some(
408 noxid_ir::computational_statements_javascript(
409 &endpoint.statements,
410 2,
411 &compiler_body_javascript,
412 )
413 .map(|body| {
414 format!(
418 "\"{}\": async (args, context) => {{\n{}{}}}",
419 js_escape(endpoint.id.as_str()),
420 body,
421 " ",
422 )
423 }),
424 )
425 })
426 .collect::<Result<Vec<_>, String>>()?
427 .join(",\n ");
428 let task_schemas = program
429 .tasks
430 .iter()
431 .map(task_schema_javascript)
432 .collect::<Vec<_>>()
433 .join(",\n ");
434 let compiled_tasks = program
435 .tasks
436 .iter()
437 .filter_map(|task| {
438 if task.host_key.is_some() {
439 return None;
440 }
441 Some(
442 compiler_statements_javascript(&task.statements, 2).map(|body| {
443 format!(
444 "\"{}\": async (_args, context) => {{\n{body} }}",
445 js_escape(task.id.as_str()),
446 )
447 }),
448 )
449 })
450 .collect::<Result<Vec<_>, String>>()?
451 .join(",\n ");
452 let queue_schemas = program
453 .queues
454 .iter()
455 .map(queue_schema_javascript)
456 .collect::<Vec<_>>()
457 .join(",\n ");
458 let live_resource_schemas = program
459 .live_resources
460 .iter()
461 .map(|resource| {
462 let capabilities = resource
463 .capabilities
464 .iter()
465 .map(|capability| format!("\"{}\"", js_escape(capability)))
466 .collect::<Vec<_>>()
467 .join(", ");
468 let route_scopes = resource
469 .route_scopes
470 .iter()
471 .map(|scope| {
472 let parameters = scope
473 .parameters
474 .iter()
475 .map(|parameter| format!(
476 "Object.freeze({{ name: \"{}\", type: \"{}\", catchAll: {} }})",
477 js_escape(¶meter.name),
478 js_escape(¶meter.ty),
479 parameter.catch_all,
480 ))
481 .collect::<Vec<_>>()
482 .join(", ");
483 let middleware = scope
484 .middleware
485 .iter()
486 .filter_map(|id| id.as_str().strip_prefix("middleware:"))
487 .map(|name| format!("\"{}\"", js_escape(name)))
488 .collect::<Vec<_>>()
489 .join(", ");
490 format!(
491 "Object.freeze({{ id: \"{}\", pattern: \"{}\", parameters: Object.freeze([{}]), middleware: Object.freeze([{}]) }})",
492 js_escape(scope.route.as_str()),
493 js_escape(&scope.pattern),
494 parameters,
495 middleware,
496 )
497 })
498 .collect::<Vec<_>>()
499 .join(", ");
500 format!(
501 "Object.freeze({{ id: \"{}\", name: \"{}\", capabilities: Object.freeze([{}]), routeScopes: Object.freeze([{}]) }})",
502 js_escape(resource.id.as_str()),
503 js_escape(&resource.name),
504 capabilities,
505 route_scopes,
506 )
507 })
508 .collect::<Vec<_>>()
509 .join(",\n ");
510 let presence_schemas = program
511 .presences
512 .iter()
513 .map(|presence| {
514 let capabilities = presence
515 .capabilities
516 .iter()
517 .map(|capability| format!("\"{}\"", js_escape(capability)))
518 .collect::<Vec<_>>()
519 .join(", ");
520 let route_scopes = presence
521 .route_scopes
522 .iter()
523 .map(|scope| {
524 let parameters = scope
525 .parameters
526 .iter()
527 .map(|parameter| format!(
528 "Object.freeze({{ name: \"{}\", type: \"{}\", catchAll: {} }})",
529 js_escape(¶meter.name),
530 js_escape(¶meter.ty),
531 parameter.catch_all,
532 ))
533 .collect::<Vec<_>>()
534 .join(", ");
535 let middleware = scope
536 .middleware
537 .iter()
538 .filter_map(|id| id.as_str().strip_prefix("middleware:"))
539 .map(|name| format!("\"{}\"", js_escape(name)))
540 .collect::<Vec<_>>()
541 .join(", ");
542 format!(
543 "Object.freeze({{ id: \"{}\", pattern: \"{}\", parameters: Object.freeze([{}]), middleware: Object.freeze([{}]) }})",
544 js_escape(scope.route.as_str()),
545 js_escape(&scope.pattern),
546 parameters,
547 middleware,
548 )
549 })
550 .collect::<Vec<_>>()
551 .join(", ");
552 format!(
553 "Object.freeze({{ id: \"{}\", component: \"{}\", stream: \"{}\", recordType: \"{}\", memberType: \"{}\", snapshotType: \"{}\", capabilities: Object.freeze([{}]), routeScopes: Object.freeze([{}]), ttlMilliseconds: {}, heartbeatMilliseconds: {} }})",
554 js_escape(presence.id.as_str()),
555 js_escape(&presence.component_name),
556 js_escape(presence.stream.as_str()),
557 js_escape(presence.record_type.as_str()),
558 js_escape(presence.member_type.as_str()),
559 js_escape(presence.snapshot_type.as_str()),
560 capabilities,
561 route_scopes,
562 presence.ttl_ms,
563 presence.heartbeat_ms,
564 )
565 })
566 .collect::<Vec<_>>()
567 .join(",\n ");
568 let live_surface = !program.live_resources.is_empty() || !program.presences.is_empty();
569 let application_namespace = runtime_options
570 .application_namespace
571 .as_deref()
572 .unwrap_or("codegen_test");
573 let pubsub_options = runtime_options.pubsub.or_else(|| {
574 live_surface.then_some(PubSubRuntimeOptions {
575 driver: PubSubDriver::Memory,
576 coalescing_ms: 250,
577 })
578 });
579 let compiled_queues = program
580 .queues
581 .iter()
582 .filter_map(|queue| {
583 if queue.host_key.is_some() {
584 return None;
585 }
586 Some(
587 compiler_statements_javascript(&queue.statements, 2).map(|body| {
588 format!(
589 "\"{}\": async (args, context) => {{\n{body} }}",
590 js_escape(queue.id.as_str()),
591 )
592 }),
593 )
594 })
595 .collect::<Result<Vec<_>, String>>()?
596 .join(",\n ");
597 let compiled_server_bodies =
600 format!("{compiled_actions}\n{compiled_endpoints}\n{compiled_tasks}\n{compiled_queues}");
601 let date_helpers = if compiled_server_bodies.contains("$noxDate.") {
602 format!("{}\n", noxid_ir::DATE_HELPERS_JS)
603 } else {
604 String::new()
605 };
606 let endpoint = if base_path == "/" {
607 "/_noxid/actions/".to_string()
608 } else {
609 format!("{base_path}/_noxid/actions/")
610 };
611 let invalidation_endpoint = if base_path == "/" {
612 "/_noxid/revalidate".to_string()
613 } else {
614 format!("{base_path}/_noxid/revalidate")
615 };
616 let task_endpoint = if base_path == "/" {
617 "/_noxid/tasks/".to_string()
618 } else {
619 format!("{base_path}/_noxid/tasks/")
620 };
621 let queue_drain_endpoint = if base_path == "/" {
622 "/_noxid/queue/drain".to_string()
623 } else {
624 format!("{base_path}/_noxid/queue/drain")
625 };
626 let live_resource_endpoint = if base_path == "/" {
627 "/_noxid/live".to_string()
628 } else {
629 format!("{base_path}/_noxid/live")
630 };
631 let presence_write_endpoint = if base_path == "/" {
632 "/_noxid/presence".to_string()
633 } else {
634 format!("{base_path}/_noxid/presence")
635 };
636 let equality_function = if compiled_server_bodies.contains("$noxEqual(") {
637 LANGUAGE_VALUE_EQUALITY_FUNCTION
638 } else {
639 ""
640 };
641 let uses_endpoint_storage = program
642 .endpoints
643 .iter()
644 .any(|endpoint| endpoint.limit.is_some() || endpoint.idempotent);
645 let uses_redis_pubsub =
646 pubsub_options.is_some_and(|options| options.driver == PubSubDriver::Redis);
647 let uses_presence_storage = !program.presences.is_empty();
648 let uses_agent_storage = !runtime_options.agents.is_empty();
651 let storage_import_javascript = if uses_endpoint_storage
652 || uses_presence_storage
653 || uses_redis_pubsub
654 || uses_uploads
655 || uses_agent_storage
656 {
657 format!(
658 "import * as __noxidServerStorageRuntime from \"./noxid-server.js\";\n{}{}{}",
659 if uses_endpoint_storage || uses_presence_storage || uses_agent_storage {
660 "const __noxidStorage = __noxidServerStorageRuntime.storage;\nconst __noxidSharedRateLimit = __noxidServerStorageRuntime.__noxidEndpointRateLimit;\nconst __noxidSharedIdempotencyPrepare = __noxidServerStorageRuntime.__noxidEndpointIdempotencyPrepare;\nconst __noxidSharedIdempotencyComplete = __noxidServerStorageRuntime.__noxidEndpointIdempotencyComplete;\nconst __noxidSharedIdempotencyRelease = __noxidServerStorageRuntime.__noxidEndpointIdempotencyRelease;\n"
661 } else {
662 ""
663 },
664 if uses_redis_pubsub {
665 "const __noxidRedisPubSubPublish = __noxidServerStorageRuntime.__noxidRedisPubSubPublish;\nconst __noxidRedisPubSubSubscribe = __noxidServerStorageRuntime.__noxidRedisPubSubSubscribe;\n"
666 } else {
667 ""
668 },
669 if uses_uploads {
670 "const __noxidCreateUploadSink = __noxidServerStorageRuntime.__noxidCreateUploadSink;\n"
671 } else {
672 ""
673 },
674 )
675 } else {
676 String::new()
677 };
678 let startup_import_javascript = startup_import.map_or_else(String::new, |specifier| {
679 format!(
680 "import {{ startServerPlugins as __noxidStartServerPlugins }} from \"{}\";\n",
681 js_escape(specifier)
682 )
683 });
684 let principal_authority_import_javascript = agent_surfaces
685 .principal_authority_import
686 .map_or_else(|| "const __noxidCloseDatabase = async () => {};\n".into(), |specifier| {
687 format!(
688 "import {{ __installNoxidPrincipalAuthority, closeDatabase as __noxidCloseDatabase }} from \"{}\";\n",
689 js_escape(specifier)
690 )
691 });
692 let otlp_import_javascript = match runtime_options.tracing_export {
693 ServerTracingExport::Stdout => String::new(),
694 ServerTracingExport::Otlp => {
695 "import { createOtlpTraceExporter } from \"../assets/noxid-runtime.js\";\n".into()
696 }
697 };
698 let principal_authority_import_javascript =
699 format!("{principal_authority_import_javascript}{otlp_import_javascript}");
700 let live_resource_runtime = if live_surface {
701 format!(
702 "const liveResourcePath = \"{}\";\nconst presenceWritePath = \"{}\";\nconst liveResourceSchemas = Object.freeze([\n {}\n]);\nconst presenceSchemas = Object.freeze([\n {}\n]);\n{}",
703 js_escape(&live_resource_endpoint),
704 js_escape(&presence_write_endpoint),
705 live_resource_schemas,
706 presence_schemas,
707 LIVE_RESOURCE_RUNTIME,
708 )
709 } else {
710 String::new()
711 };
712 let handler_runtime = HANDLER_RUNTIME
713 .replace(
714 " /* noxid-server:startup */",
715 if startup_import.is_some() {
716 " await __noxidStartServerPlugins(environment);"
717 } else {
718 ""
719 },
720 )
721 .replace(
722 "/* noxid-server:live-invalidation */",
723 if pubsub_options.is_some() {
724 r#"async function __noxidPublishLiveInvalidations(resources, principal) {
725 for (const semanticId of resources) {
726 await __noxidPubSubPublish(__noxidPubSubEvent("invalidation", semanticId, principal));
727 }
728}"#
729 } else {
730 "async function __noxidPublishLiveInvalidations() {}"
731 },
732 )
733 .replace(
734 "/* noxid-server:live-resource-transport-runtime */",
735 &live_resource_runtime,
736 )
737 .replace(
738 " /* noxid-server:live-resource-request */",
739 if live_surface {
740 " const liveResourceResponse = await handleLiveResourceRequest(request, url, environment, executionContext);\n if (liveResourceResponse !== null) return liveResourceResponse;"
741 } else {
742 ""
743 },
744 );
745 let trace_export_javascript = match runtime_options.tracing_export {
746 ServerTracingExport::Stdout => {
747 "if (loggable) { try { console.log(JSON.stringify(record)); } catch {} }"
748 }
749 ServerTracingExport::Otlp => {
750 "if (loggable && event !== \"request.start\") { try { __noxidOtlpExporter.enqueue(Object.freeze({ ...record, traceFlags: trace.traceFlags })); } catch {} }"
751 }
752 };
753 let trace_runtime = TRACE_RUNTIME
754 .replace(
755 " /* noxid-server:development-trace-context */",
756 if runtime_options.development_trace_capture {
757 " developmentCapture = true;"
758 } else {
759 ""
760 },
761 )
762 .replace(
763 " /* noxid-server:development-trace-capture */",
764 if runtime_options.development_trace_capture {
765 r#" try {
766 const capture = globalThis.__NOXID_DEV_TRACE_CAPTURE__;
767 if (typeof capture === "function") capture(Object.freeze({ ...record }));
768 } catch {}"#
769 } else {
770 ""
771 },
772 )
773 .replace(
774 " /* noxid-server:trace-span-id */",
775 if runtime_options.tracing_export == ServerTracingExport::Otlp {
776 " try { return __noxidOtlpExporter.nextSpanId(); } catch {}"
777 } else {
778 ""
779 },
780 )
781 .replace(" /* noxid-server:trace-export */", trace_export_javascript);
782 let trace_runtime = if runtime_options.tracing_export == ServerTracingExport::Otlp {
783 let attribute_mapping = otel_span_attribute_mapping()
784 .iter()
785 .map(|(source, target, event)| {
786 format!(
787 "Object.freeze({{ source: \"{}\", target: \"{}\", event: {} }})",
788 js_escape(source),
789 js_escape(target),
790 event
791 .map(|event| format!("\"{}\"", js_escape(event)))
792 .unwrap_or_else(|| "null".into()),
793 )
794 })
795 .collect::<Vec<_>>()
796 .join(", ");
797 format!(
798 r#"const __noxidOtlpExporter = createOtlpTraceExporter({{
799 endpoint: __noxidTracingEnvironmentValue(globalThis.process?.env, "OTEL_EXPORTER_OTLP_ENDPOINT", tracingOtlpEndpointSecret),
800 headers: __noxidTracingEnvironmentValue(globalThis.process?.env, "OTEL_EXPORTER_OTLP_HEADERS", tracingOtlpHeadersSecret),
801 serviceName: "{}",
802 disabled: __NOXID_TRACING_SECRET_FAILURE__,
803 attributeMapping: Object.freeze([{attribute_mapping}]),
804}});
805export async function flushNoxidTracing() {{ try {{ await __noxidOtlpExporter.flush(); }} catch {{}} }}
806export function abandonNoxidTracing() {{ try {{ return __noxidOtlpExporter.abandon(); }} catch {{ return 0; }} }}
807export function noxidTracingExporterSnapshot() {{
808 try {{ return __noxidOtlpExporter.snapshot(); }}
809 catch {{ return Object.freeze({{ capacity: 0, batchSize: 0, queued: 0, inFlight: 0, dropped: 0, closed: true }}); }}
810}}
811{trace_runtime}"#,
812 js_escape(&runtime_options.tracing_service_name),
813 )
814 } else {
815 trace_runtime.to_string()
816 };
817 let model_runtime = if runtime_options.models.is_empty() {
820 String::new()
821 } else {
822 let declarations = runtime_options
823 .models
824 .models
825 .iter()
826 .map(|model| {
827 format!(
828 " \"{}\": Object.freeze({{ id: \"{}\", name: \"{}\", provider: \"{}\", modelId: \"{}\", baseUrl: Object.freeze({{ kind: \"{}\", value: \"{}\" }}), temperature: {}, maxTokens: {}, retries: {}, secret: \"{}\" }}),",
829 js_escape(&model.name),
830 js_escape(model.id.as_str()),
831 js_escape(&model.name),
832 model.provider,
833 js_escape(&model.model_id),
834 model.base_url.kind(),
835 js_escape(model.base_url.value()),
836 model
837 .temperature
838 .clone()
839 .unwrap_or_else(|| "null".to_string()),
840 model
841 .max_tokens
842 .map(|value| value.to_string())
843 .unwrap_or_else(|| "null".into()),
844 model.retries,
845 js_escape(&model.secret),
846 )
847 })
848 .collect::<Vec<_>>()
849 .join("\n");
850 format!(
851 "\n// noxid-runtime:feature-start:models\nconst modelDeclarations = Object.freeze({{\n{declarations}\n}});\nconst modelTypeSchemas = {};\n{MODEL_SCENARIO_RUNTIME}{MODEL_RUNTIME}// noxid-runtime:feature-end:models\n",
852 noxid_model_ir::type_schema_registry_javascript(&runtime_options.models.type_schemas),
853 )
854 };
855 let agent_runtime = agents::agents_runtime_javascript(
859 &runtime_options.agents,
860 base_path,
861 noxid_ir::DEFAULT_ENDPOINT_TIMEOUT_MS,
862 );
863 let endpoint_runtime = ENDPOINT_RUNTIME.replace(
864 " /* noxid-server:agent-run-request */",
865 if agent_runtime.is_empty() {
866 ""
867 } else {
868 " const agentRunResponse = await handleAgentRunRequest(request, url, environment, executionContext);\n if (agentRunResponse !== null) return agentRunResponse;"
869 },
870 );
871 let queue_runtime = QUEUE_RUNTIME.replace(
872 " /* noxid-server:agent-run-reconcile */",
873 if agent_runtime.is_empty() {
874 ""
875 } else {
876 " void __noxidReconcileAgentRuns().catch((cause) => notify(cause));"
877 },
878 );
879 let (pubsub_configuration, pubsub_runtime) = pubsub_options.map_or_else(
880 || (String::new(), ""),
881 |options| {
882 (
883 format!(
884 "const applicationNamespace = \"{}\";\nconst pubSubDriver = \"{}\";\nconst pubSubCoalescingMs = {};\n",
885 js_escape(application_namespace),
886 options.driver.as_str(),
887 options.coalescing_ms,
888 ),
889 PUBSUB_RUNTIME,
890 )
891 },
892 );
893 let validator_symbols = if uses_uploads {
894 "typeValidators, __noxidCreateFileValidationState, __noxidValidateFileChunk, __noxidFinalizeFileValidation, __noxidCreateFileRef"
895 } else {
896 "typeValidators"
897 };
898 let handler = format!(
899 "import * as hostModule from \"{}\";\nimport {{ {} }} from \"{}\";\nimport * as middlewareRegistry from \"{}\";\n{}{}{}\n{}{}{}{}const databasePoolSize = {};\n{}const __noxidConfiguredEnvironmentProxies = new WeakSet();\nfunction __noxidConfiguredServerEnvironment(environment) {{\n environment = __noxidServerEnvironment(environment);\n if (__noxidConfiguredEnvironmentProxies.has(environment)) return environment;\n const pool = Object.getOwnPropertyDescriptor(environment, \"dbPool\");\n if (pool?.value === databasePoolSize && pool.enumerable === false && pool.writable === false && pool.configurable === false) return environment;\n const processEnvironment = globalThis.process?.env;\n if (environment !== processEnvironment && Object.isExtensible(environment) && (pool === undefined || pool.configurable)) {{\n Object.defineProperty(environment, \"dbPool\", {{ value: databasePoolSize, enumerable: false, writable: false, configurable: false }});\n return environment;\n }}\n const enriched = new Proxy(environment, {{\n get(target, property, receiver) {{ return property === \"dbPool\" ? databasePoolSize : Reflect.get(target, property, receiver); }},\n set(target, property, value) {{ return property === \"dbPool\" ? false : Reflect.set(target, property, value, target); }},\n defineProperty(target, property, descriptor) {{ return property === \"dbPool\" ? false : Reflect.defineProperty(target, property, descriptor); }},\n deleteProperty(target, property) {{ return property === \"dbPool\" ? false : Reflect.deleteProperty(target, property); }},\n }});\n __noxidConfiguredEnvironmentProxies.add(enriched);\n return enriched;\n}}\nconst tracingExport = \"{}\";\nconst tracingOtlpEndpointSecret = {};\nconst tracingOtlpHeadersSecret = {};\nfunction __noxidTracingEnvironmentValue(environment, name, secret) {{\n try {{\n const configured = __noxidServerEnvironment(environment);\n const value = secret ? configured?.secrets?.[name] : globalThis.process?.env?.[name];\n return typeof value === \"string\" && value.length > 0 ? value : null;\n }} catch {{ return null; }}\n}}\nconst middlewareHandlers = typeof middlewareRegistry === \"undefined\" ? Object.freeze({{}}) : middlewareRegistry.middleware ?? Object.freeze({{}});\nconst globalMiddlewareHandlers = typeof middlewareRegistry === \"undefined\" ? Object.freeze({{}}) : middlewareRegistry.globalMiddlewareHandlers ?? Object.freeze({{}});\nconst globalMiddleware = typeof middlewareRegistry === \"undefined\" ? Object.freeze([]) : middlewareRegistry.globalMiddleware ?? Object.freeze([]);\nconst hostActions = hostModule.actions ?? hostModule.default ?? Object.create(null);\nconst hostEndpoints = hostModule.endpoints ?? hostActions;\nconst hostTasks = hostModule.tasks ?? Object.create(null);\nconst hostQueues = hostModule.queues ?? Object.create(null);\nconst compiledActions = Object.freeze({{\n {}\n}});\nconst compiledEndpoints = Object.freeze({{\n {}\n}});\nconst compiledTasks = Object.freeze({{\n {}\n}});\nconst compiledQueues = Object.freeze({{\n {}\n}});\nconst endpointSchemas = Object.freeze([\n {}\n]);\nconst taskSchemas = Object.freeze([\n {}\n]);\nconst queueSchemas = Object.freeze([\n {}\n]);\nexport const taskSchedules = Object.freeze(taskSchemas.map((task) => Object.freeze({{ name: task.name, schedule: task.schedule }})));\nexport const closeDatabase = __noxidCloseDatabase;\nconst authorize = hostModule.authorize;\nconst invalidateCache = hostModule.invalidateCache;\nconst schemas = Object.freeze({{\n {}\n}});\nconst endpointPrefix = \"{}\";\nconst invalidationEndpoint = \"{}\";\nconst taskPrefix = \"{}\";\nconst queueDrainPath = \"{}\";\nconst applicationBasePath = \"{}\";\nconst openapiDocument = {};\nconst openapiEnabled = {};\nconst mcpEnabled = {};\nconst tracingMode = \"{}\";\n\n{}{}{}{}{}{}{}{}",
900 js_escape(host_import),
901 validator_symbols,
902 js_escape(validator_import),
903 js_escape(middleware_import),
904 storage_import_javascript,
905 startup_import_javascript,
906 principal_authority_import_javascript,
907 server_secrets_prelude_javascript(server_secrets),
908 date_helpers,
909 equality_function,
910 noxid_ir::SERVER_MIDDLEWARE_RESULT_JAVASCRIPT,
911 runtime_options.db_pool,
912 pubsub_configuration,
913 runtime_options.tracing_export.as_str(),
914 runtime_options.otlp_endpoint_secret,
915 runtime_options.otlp_headers_secret,
916 compiled_actions,
917 compiled_endpoints,
918 compiled_tasks,
919 compiled_queues,
920 endpoint_schemas,
921 task_schemas,
922 queue_schemas,
923 schemas,
924 js_escape(&endpoint),
925 js_escape(&invalidation_endpoint),
926 js_escape(&task_endpoint),
927 js_escape(&queue_drain_endpoint),
928 js_escape(base_path),
929 agent_surfaces
930 .openapi_json
931 .map(|document| format!("\"{}\"", js_escape(document)))
932 .unwrap_or_else(|| "null".into()),
933 agent_surfaces.serve_openapi,
934 agent_surfaces.mcp,
935 tracing_mode.as_str(),
936 PRINCIPAL_RUNTIME,
937 trace_runtime,
938 pubsub_runtime,
939 endpoint_multipart_limits() + &endpoint_runtime,
940 queue_runtime,
941 model_runtime,
942 agent_runtime,
943 handler_runtime,
944 );
945 let handler = format!("{handler}\nexport {{ __noxidConfiguredServerEnvironment }};\n");
946 let missing_lifecycle_exports =
947 missing_server_lifecycle_exports(&handler, runtime_options.tracing_export);
948 if !missing_lifecycle_exports.is_empty() {
949 return Err(format!(
950 "internal error: generated server handler is missing compiler-owned lifecycle exports: {}",
951 missing_lifecycle_exports.join(", ")
952 ));
953 }
954 Ok(ServerJavaScriptOutput {
955 handler,
956 manifest: program.to_json(),
957 server_actions: callable
958 .iter()
959 .filter(|boundary| boundary.target.as_str() == "server")
960 .count(),
961 edge_actions: callable
962 .iter()
963 .filter(|boundary| boundary.target.as_str() == "edge")
964 .count(),
965 })
966}
967
968pub fn compiler_body_javascript(expression: &SemanticExpr) -> Result<String, String> {
974 Ok(match &expression.kind {
975 SemanticExprKind::Int(value) => value.to_string(),
976 SemanticExprKind::Float(value) => value.to_string(),
977 SemanticExprKind::String(value) => format!("\"{}\"", js_escape(value)),
978 SemanticExprKind::Boolean(value) => value.to_string(),
979 SemanticExprKind::Array(values) => format!(
980 "[{}]",
981 values
982 .iter()
983 .map(compiler_body_javascript)
984 .collect::<Result<Vec<_>, _>>()?
985 .join(", ")
986 ),
987 SemanticExprKind::Struct { fields, .. } => format!(
988 "Object.freeze({{ {} }})",
989 fields
990 .iter()
991 .map(|field| {
992 Ok(format!(
993 "\"{}\": {}",
994 js_escape(&field.name),
995 compiler_body_javascript(&field.value)?
996 ))
997 })
998 .collect::<Result<Vec<_>, String>>()?
999 .join(", ")
1000 ),
1001 SemanticExprKind::FieldAccess { base, name, .. } => {
1002 format!(
1003 "({})[\"{}\"]",
1004 compiler_body_javascript(base)?,
1005 js_escape(name)
1006 )
1007 }
1008 SemanticExprKind::Reference(id) => {
1009 let name = id.as_str().rsplit('.').next().unwrap_or(id.as_str());
1010 if id.as_str().starts_with("local:") {
1011 name.to_string()
1012 } else {
1013 format!("args[\"{}\"]", js_escape(name))
1014 }
1015 }
1016 SemanticExprKind::Variant {
1017 variant, payload, ..
1018 } => {
1019 let tag = variant
1020 .as_str()
1021 .rsplit('.')
1022 .next()
1023 .unwrap_or(variant.as_str());
1024 match payload {
1025 Some(payload) => format!(
1026 "Object.freeze({{ tag: \"{}\", value: {} }})",
1027 js_escape(tag),
1028 compiler_body_javascript(payload)?
1029 ),
1030 None => format!("Object.freeze({{ tag: \"{}\" }})", js_escape(tag)),
1031 }
1032 }
1033 SemanticExprKind::Binary { left, op, right } => {
1034 if matches!(op, SemanticBinaryOp::Equal | SemanticBinaryOp::NotEqual)
1035 && has_language_value_equality(&left.ty)
1036 {
1037 let equality = format!(
1038 "$noxEqual({}, {})",
1039 compiler_body_javascript(left)?,
1040 compiler_body_javascript(right)?
1041 );
1042 return Ok(if matches!(op, SemanticBinaryOp::NotEqual) {
1043 format!("(!{equality})")
1044 } else {
1045 equality
1046 });
1047 }
1048 let operator = match op {
1049 SemanticBinaryOp::Add => "+",
1050 SemanticBinaryOp::Subtract => "-",
1051 SemanticBinaryOp::Multiply => "*",
1052 SemanticBinaryOp::Divide => "/",
1053 SemanticBinaryOp::Equal => "===",
1054 SemanticBinaryOp::NotEqual => "!==",
1055 SemanticBinaryOp::Less => "<",
1056 SemanticBinaryOp::LessEqual => "<=",
1057 SemanticBinaryOp::Greater => ">",
1058 SemanticBinaryOp::GreaterEqual => ">=",
1059 SemanticBinaryOp::And => "&&",
1060 SemanticBinaryOp::Or => "||",
1061 SemanticBinaryOp::Coalesce => "??",
1062 };
1063 format!(
1064 "({} {operator} {})",
1065 compiler_body_javascript(left)?,
1066 compiler_body_javascript(right)?
1067 )
1068 }
1069 SemanticExprKind::Unary { op, operand } => {
1070 format!("({}{})", op.as_str(), compiler_body_javascript(operand)?)
1071 }
1072 SemanticExprKind::StringTemplate(parts) => {
1073 let mut pieces = vec!["\"\"".to_string()];
1074 for part in parts {
1075 pieces.push(match part {
1076 SemanticTemplatePart::Literal(value) => format!("\"{}\"", js_escape(value)),
1077 SemanticTemplatePart::Expression(expression) => {
1078 format!("({})", compiler_body_javascript(expression)?)
1079 }
1080 });
1081 }
1082 format!("({})", pieces.join(" + "))
1083 }
1084 SemanticExprKind::CollectionQuery {
1085 base,
1086 kind,
1087 field,
1088 value,
1089 } => {
1090 let value = value.as_deref().map(compiler_body_javascript).transpose()?;
1091 noxid_ir::collection_query_javascript(
1092 *kind,
1093 &compiler_body_javascript(base)?,
1094 field.as_ref().map(|segment| segment.name.as_str()),
1095 value.as_deref(),
1096 match &base.ty {
1097 noxid_types::Type::Map(key, _) => Some(key.as_ref()),
1098 _ => None,
1099 },
1100 )
1101 }
1102 SemanticExprKind::FunctionCall {
1103 function,
1104 name,
1105 arguments,
1106 } => match emit_distinct_identity_call(function, arguments)? {
1107 Some(javascript) => javascript,
1108 None => match emit_builtin_call(function, arguments)? {
1109 Some(javascript) => javascript,
1110 None => return Err(rejected_remote_call(name, arguments.len())),
1111 },
1112 },
1113 SemanticExprKind::Call {
1114 name, arguments, ..
1115 } => return Err(rejected_remote_call(name, arguments.len())),
1116 })
1117}
1118
1119fn rejected_remote_call(name: &str, arity: usize) -> String {
1122 noxid_ir::emitter_rejection(
1123 "REMOTE_ACTION_CALL_UNSUPPORTED",
1124 format_args!(
1125 "the call `{name}({} argument(s))` has no compiler-owned server lowering; compiler-owned remote bodies emit only literals, typed parameters, field access, constructors, operators, templates, pure builtins, and collection queries — move this call into a host-implemented body keyed by the action or endpoint, or replace it with that subset",
1126 arity
1127 ),
1128 )
1129}
1130
1131pub fn compiler_statements_javascript(
1132 statements: &[noxid_ir::SemanticStatement],
1133 indent: usize,
1134) -> Result<String, String> {
1135 noxid_ir::computational_statements_javascript(statements, indent, &compiler_body_javascript)
1136}
1137
1138fn emit_distinct_identity_call(
1145 function: &noxid_ir::SemanticId,
1146 arguments: &[SemanticExpr],
1147) -> Result<Option<String>, String> {
1148 if !function.is_distinct_call() {
1149 return Ok(None);
1150 }
1151 match arguments.first() {
1152 Some(argument) => compiler_body_javascript(argument).map(Some),
1153 None => Ok(Some("undefined".into())),
1154 }
1155}
1156
1157fn emit_builtin_call(
1161 function: &noxid_ir::SemanticId,
1162 arguments: &[SemanticExpr],
1163) -> Result<Option<String>, String> {
1164 let Some(name) = function.as_str().strip_prefix("fn:@builtin.") else {
1165 return Ok(None);
1166 };
1167 let emitted = arguments
1168 .iter()
1169 .map(compiler_body_javascript)
1170 .collect::<Result<Vec<_>, _>>()?;
1171 noxid_ir::builtin_javascript(name, &emitted)
1172 .map(Some)
1173 .ok_or_else(|| noxid_ir::rejected_builtin_call(name, emitted.len()))
1174}
1175
1176fn has_language_value_equality(ty: &noxid_types::Type) -> bool {
1177 use noxid_types::Type;
1178 match ty {
1179 Type::Array(_)
1180 | Type::Map(_, _)
1181 | Type::MapEntry(_, _)
1182 | Type::Result(_, _)
1183 | Type::Named(_) => true,
1184 Type::Optional(inner)
1185 | Type::Static(inner)
1186 | Type::Reactive(inner)
1187 | Type::Binding(inner) => has_language_value_equality(inner),
1188 Type::File
1192 | Type::Int
1193 | Type::String
1194 | Type::Boolean
1195 | Type::Number
1196 | Type::Float
1197 | Type::Date
1198 | Type::Function(_, _)
1199 | Type::Unknown => false,
1200 }
1201}
1202
1203fn schema_javascript(boundary: &ExecutionBoundary) -> String {
1204 let parameters = boundary
1205 .parameters
1206 .iter()
1207 .map(|parameter| {
1208 format!(
1209 "Object.freeze({{ name: \"{}\", type: \"{}\", typeId: {} }})",
1210 js_escape(¶meter.name),
1211 js_escape(¶meter.ty),
1212 optional_js_string(parameter.type_id.as_ref().map(|id| id.as_str())),
1213 )
1214 })
1215 .collect::<Vec<_>>()
1216 .join(", ");
1217 let capabilities = boundary
1218 .capabilities
1219 .iter()
1220 .map(|capability| format!("\"{}\"", js_escape(capability)))
1221 .collect::<Vec<_>>()
1222 .join(", ");
1223 let route_scopes = boundary
1224 .route_scopes
1225 .iter()
1226 .map(|scope| {
1227 let middleware = scope
1228 .middleware
1229 .iter()
1230 .filter_map(|id| id.as_str().strip_prefix("middleware:"))
1231 .map(|name| format!("\"{}\"", js_escape(name)))
1232 .collect::<Vec<_>>()
1233 .join(", ");
1234 format!(
1235 "Object.freeze({{ id: \"{}\", pattern: \"{}\", middleware: Object.freeze([{}]) }})",
1236 js_escape(scope.route.as_str()),
1237 js_escape(&scope.pattern),
1238 middleware,
1239 )
1240 })
1241 .collect::<Vec<_>>()
1242 .join(", ");
1243 let invalidates = semantic_ids_javascript(&boundary.invalidates);
1244 format!(
1245 "\"{}\": Object.freeze({{ id: \"{}\", boundary: \"{}\", target: \"{}\", parameters: Object.freeze([{}]), result: Object.freeze({{ id: \"{}\", type: \"{}\", typeId: {} }}), capabilities: Object.freeze([{}]), routeScopes: Object.freeze([{}]), invalidates: Object.freeze([{}]) }})",
1246 js_escape(boundary.action.as_str()),
1247 js_escape(boundary.action.as_str()),
1248 js_escape(boundary.id.as_str()),
1249 boundary.target.as_str(),
1250 parameters,
1251 js_escape(boundary.result.id.as_str()),
1252 js_escape(&boundary.result.ty),
1253 optional_js_string(boundary.result.type_id.as_ref().map(|id| id.as_str())),
1254 capabilities,
1255 route_scopes,
1256 invalidates,
1257 )
1258}
1259
1260fn endpoint_schema_javascript(endpoint: &EndpointExecutionBoundary) -> String {
1261 let fields = |section| {
1262 endpoint
1263 .inputs
1264 .iter()
1265 .filter(|input| input.section == section)
1266 .map(|input| {
1267 let upload = input.file.as_ref().map_or_else(
1268 || "null".to_string(),
1269 |file| {
1270 let types = file
1271 .types
1272 .iter()
1273 .map(|media_type| format!("\"{}\"", js_escape(media_type)))
1274 .collect::<Vec<_>>()
1275 .join(", ");
1276 format!(
1277 "Object.freeze({{ maxSizeBytes: {}, types: Object.freeze([{}]), multiple: {} }})",
1278 file.max_size_bytes, types, file.multiple,
1279 )
1280 },
1281 );
1282 format!(
1283 "Object.freeze({{ name: \"{}\", type: \"{}\", typeId: {}, upload: {} }})",
1284 js_escape(&input.name),
1285 js_escape(&input.ty),
1286 optional_js_string(input.type_id.as_ref().map(|id| id.as_str())),
1287 upload,
1288 )
1289 })
1290 .collect::<Vec<_>>()
1291 .join(", ")
1292 };
1293 let capabilities = endpoint
1294 .capabilities
1295 .iter()
1296 .map(|capability| format!("\"{}\"", js_escape(capability)))
1297 .collect::<Vec<_>>()
1298 .join(", ");
1299 let middleware = endpoint
1300 .middleware
1301 .iter()
1302 .map(|name| format!("\"{}\"", js_escape(name)))
1303 .collect::<Vec<_>>()
1304 .join(", ");
1305 let limit = endpoint.limit.map_or_else(
1306 || "null".to_string(),
1307 |limit| {
1308 format!(
1309 "Object.freeze({{ requests: {}, window: \"{}\", scope: \"{}\" }})",
1310 limit.requests,
1311 match limit.window {
1312 EndpointLimitWindow::Minute => "minute",
1313 EndpointLimitWindow::Hour => "hour",
1314 },
1315 match limit.scope {
1316 EndpointLimitScope::Session => "session",
1317 EndpointLimitScope::Ip => "ip",
1318 }
1319 )
1320 },
1321 );
1322 let cache = endpoint.cache.as_ref().map_or_else(
1323 || "null".to_string(),
1324 |cache| {
1325 let tags = cache
1326 .tags
1327 .iter()
1328 .map(|tag| format!("\"{}\"", js_escape(tag)))
1329 .collect::<Vec<_>>()
1330 .join(", ");
1331 format!(
1332 "Object.freeze({{ id: \"{}\", mode: \"{}\", seconds: {}, tags: Object.freeze([{}]) }})",
1333 js_escape(cache.id.as_str()),
1334 cache.mode.as_str(),
1335 cache.seconds,
1336 tags,
1337 )
1338 },
1339 );
1340 let path = endpoint.path.as_deref().unwrap_or("");
1341 let method = endpoint
1342 .method
1343 .map(|method| method.as_str().to_uppercase())
1344 .unwrap_or_default();
1345 let result_validator = format!("validator:endpoint.{}.result", endpoint.name);
1346 let error_validator = (endpoint.kind == EndpointKind::RequestResponse
1347 && endpoint.result.ty.starts_with("Result<"))
1348 .then(|| format!("validator:endpoint.{}.error", endpoint.name));
1349 format!(
1350 "Object.freeze({{ id: \"{}\", name: \"{}\", version: {}, description: {}, kind: \"{}\", method: \"{}\", path: \"{}\", params: Object.freeze([{}]), query: Object.freeze([{}]), body: Object.freeze([{}]), result: Object.freeze({{ id: \"{}\", type: \"{}\", typeId: {}, validator: \"{}\", errorValidator: {} }}), capabilities: Object.freeze([{}]), timeoutMs: {}, limit: {}, cache: {}, idempotent: {}, middleware: Object.freeze([{}]), invalidates: Object.freeze([{}]) }})",
1351 js_escape(endpoint.id.as_str()),
1352 js_escape(&endpoint.name),
1353 endpoint.version,
1354 endpoint
1355 .description
1356 .as_deref()
1357 .map(|description| format!("\"{}\"", js_escape(description)))
1358 .unwrap_or_else(|| "null".into()),
1359 endpoint.kind.as_str(),
1360 js_escape(&method),
1361 js_escape(path),
1362 fields(EndpointInputSection::Params),
1363 fields(EndpointInputSection::Query),
1364 fields(EndpointInputSection::Body),
1365 js_escape(endpoint.result.id.as_str()),
1366 js_escape(&endpoint.result.ty),
1367 optional_js_string(endpoint.result.type_id.as_ref().map(|id| id.as_str())),
1368 js_escape(&result_validator),
1369 error_validator
1370 .as_deref()
1371 .map(|value| format!("\"{}\"", js_escape(value)))
1372 .unwrap_or_else(|| "null".into()),
1373 capabilities,
1374 endpoint.timeout_ms,
1375 limit,
1376 cache,
1377 endpoint.idempotent,
1378 middleware,
1379 semantic_ids_javascript(&endpoint.invalidates),
1380 )
1381}
1382
1383fn task_schema_javascript(task: &TaskExecutionBoundary) -> String {
1384 format!(
1385 "Object.freeze({{ id: \"{}\", name: \"{}\", schedule: \"{}\", hostKey: {} }})",
1386 js_escape(task.id.as_str()),
1387 js_escape(&task.name),
1388 js_escape(&task.schedule),
1389 optional_js_string(task.host_key.as_ref().map(|id| id.as_str())),
1390 )
1391}
1392
1393fn queue_schema_javascript(queue: &QueueExecutionBoundary) -> String {
1394 let payload = queue
1395 .payload
1396 .iter()
1397 .map(|field| {
1398 let type_ids = field
1399 .type_ids
1400 .iter()
1401 .map(|(name, id)| {
1402 format!("\"{}\": \"{}\"", js_escape(name), js_escape(id.as_str()))
1403 })
1404 .collect::<Vec<_>>()
1405 .join(", ");
1406 format!(
1407 "Object.freeze({{ name: \"{}\", type: \"{}\", typeId: {}, typeIds: Object.freeze({{{}}}) }})",
1408 js_escape(&field.name),
1409 js_escape(&field.ty),
1410 optional_js_string(field.type_id.as_ref().map(|id| id.as_str())),
1411 type_ids,
1412 )
1413 })
1414 .collect::<Vec<_>>()
1415 .join(", ");
1416 format!(
1417 "Object.freeze({{ id: \"{}\", name: \"{}\", hostKey: {}, payload: Object.freeze([{}]), retry: {}, backoffMs: {}, invalidates: Object.freeze([{}]) }})",
1418 js_escape(queue.id.as_str()),
1419 js_escape(&queue.name),
1420 optional_js_string(queue.host_key.as_ref().map(|id| id.as_str())),
1421 payload,
1422 queue.retry,
1423 queue.backoff_ms,
1424 semantic_ids_javascript(&queue.invalidates),
1425 )
1426}
1427
1428fn semantic_ids_javascript(ids: &[SemanticId]) -> String {
1429 ids.iter()
1430 .map(|id| format!("\"{}\"", js_escape(id.as_str())))
1431 .collect::<Vec<_>>()
1432 .join(", ")
1433}
1434
1435fn optional_js_string(value: Option<&str>) -> String {
1436 value
1437 .map(|value| format!("\"{}\"", js_escape(value)))
1438 .unwrap_or_else(|| "null".into())
1439}
1440
1441const PRINCIPAL_RUNTIME: &str = r##"const __noxidPrincipalAuthority = typeof __installNoxidPrincipalAuthority === "function" ? __installNoxidPrincipalAuthority(__noxidTraceDataAccess) : null;
1442const __noxidPrincipalValues = new WeakSet();
1443function __noxidTrustedPrincipal(value) { __noxidPrincipalValues.add(value); return value; }
1444const __NOXID_SYSTEM_PRINCIPAL = __noxidTrustedPrincipal(Object.freeze({ kind: "system", canonical: "system", scope: null, agent: null }));
1445
1446function __noxidPrincipalPart(value) {
1447 return encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
1448}
1449
1450function __noxidPrincipal(middlewareContext, environment, agent = null) {
1451 const identity = middlewareContext?.userId ?? middlewareContext?.sessionId ?? middlewareContext?.session?.id ?? environment?.sessionId;
1452 const scope = typeof identity === "string" && identity.length > 0 ? identity : null;
1453 if (agent !== null) {
1454 const acting = scope === null ? "system" : `session:${__noxidPrincipalPart(scope)}`;
1455 return __noxidTrustedPrincipal(Object.freeze({ kind: "agent", canonical: `agent:${__noxidPrincipalPart(agent)}:acting:${acting}`, scope, agent }));
1456 }
1457 if (scope === null) return __NOXID_SYSTEM_PRINCIPAL;
1458 return __noxidTrustedPrincipal(Object.freeze({ kind: "user", canonical: `session:${__noxidPrincipalPart(scope)}`, scope, agent: null }));
1459}
1460
1461export function __noxidLiveConnectionPrincipal(middlewareContext, environment) {
1462 return __noxidPrincipal(middlewareContext, environment);
1463}
1464
1465function __noxidPrincipalFromCanonical(value) {
1466 if (value === null || value === "system") return __NOXID_SYSTEM_PRINCIPAL;
1467 if (typeof value !== "string") throw Object.assign(new Error("persisted queue principal is invalid"), { code: "QUEUE_PRINCIPAL_DRIFT" });
1468 try {
1469 if (value.startsWith("session:")) {
1470 const scope = decodeURIComponent(value.slice(8));
1471 const principal = __noxidPrincipal({ userId: scope }, null);
1472 if (principal.canonical === value) return principal;
1473 }
1474 const marker = ":acting:session:";
1475 if (value.startsWith("agent:") && value.includes(marker)) {
1476 const split = value.indexOf(marker);
1477 const agent = decodeURIComponent(value.slice(6, split));
1478 const scope = decodeURIComponent(value.slice(split + marker.length));
1479 const principal = __noxidPrincipal({ userId: scope }, null, agent);
1480 if (principal.canonical === value) return principal;
1481 }
1482 if (value.startsWith("agent:") && value.endsWith(":acting:system")) {
1483 const agent = decodeURIComponent(value.slice(6, -14));
1484 const principal = __noxidPrincipal(null, null, agent);
1485 if (principal.canonical === value) return principal;
1486 }
1487 } catch {}
1488 throw Object.assign(new Error("persisted queue principal is invalid"), { code: "QUEUE_PRINCIPAL_DRIFT" });
1489}
1490
1491const __noxidRuntimePrincipals = new WeakMap();
1492function __noxidDataContext(fields, principal) {
1493 const context = Object.freeze({ ...fields, principal });
1494 __noxidRuntimePrincipals.set(context, principal);
1495 __noxidTraceBindPrincipal(context, principal);
1496 return __noxidPrincipalAuthority === null ? context : __noxidPrincipalAuthority.bind(context, principal);
1497}
1498
1499const __noxidAgentRequests = new WeakMap();
1500function __noxidAgentForRequest(request) { return __noxidAgentRequests.get(request) ?? null; }
1501"##;
1502
1503const TRACE_RUNTIME: &str = r##"const NOXID_TRACE_SCHEMA = "noxid.trace.v1";
1504const NOXID_TRACE_ID = /^[A-Za-z0-9_-]{16,128}$/;
1505const NOXID_TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(.*)$/i;
1506const NOXID_DIAGNOSTIC_CODE = /^[A-Z][A-Z0-9_]{0,127}$/;
1507const NOXID_SEMANTIC_ID = /^[A-Za-z0-9][A-Za-z0-9._:@/+-]{0,255}$/;
1508const NOXID_CAPABILITY = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/;
1509const NOXID_ROUTE = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/\[\]-]{0,1023}$/;
1510const NOXID_PRINCIPAL = /^(?:system|session:[A-Za-z0-9._~%!*'()-]{1,768})$/;
1511const NOXID_DATA_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]{0,127}$/;
1512const NOXID_MODEL_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
1513const NOXID_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
1514const NOXID_MODEL_PROVIDERS = new Set(["anthropic", "openai", "openai-compatible"]);
1515const NOXID_AGENT_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
1516const NOXID_AGENT_RUN = /^[A-Za-z0-9_-]{16,128}$/;
1517const __noxidRequestTraces = new WeakMap();
1518const __noxidFailureSpans = new WeakMap();
1519let __noxidTraceCounter = 0;
1520
1521function __noxidGeneratedTraceId() {
1522 try {
1523 const value = globalThis.crypto?.randomUUID?.();
1524 if (typeof value === "string" && NOXID_TRACE_ID.test(value)) return value;
1525 } catch {}
1526 __noxidTraceCounter = (__noxidTraceCounter + 1) % Number.MAX_SAFE_INTEGER;
1527 return `noxid_${Date.now().toString(36)}_${__noxidTraceCounter.toString(36).padStart(10, "0")}`;
1528}
1529
1530function __noxidRequestTraceContext(request) {
1531 let traceparent = null;
1532 let inbound = null;
1533 try {
1534 traceparent = request.headers.get("traceparent");
1535 inbound = request.headers.get("x-noxid-trace");
1536 } catch {}
1537 const parsed = typeof traceparent === "string" ? NOXID_TRACEPARENT.exec(traceparent) : null;
1538 const futureSuffixValid = parsed !== null && (parsed[1].toLowerCase() === "00" ? parsed[5] === "" : parsed[5] === "" || /^-(?:[0-9a-f]{2})+$/i.test(parsed[5]));
1539 if (parsed !== null && parsed[1].toLowerCase() !== "ff" && futureSuffixValid && !/^0+$/.test(parsed[2]) && !/^0+$/.test(parsed[3])) {
1540 return { id: parsed[2].toLowerCase(), parentSpanId: parsed[3].toLowerCase(), traceFlags: Number.parseInt(parsed[4], 16) };
1541 }
1542 return { id: typeof inbound === "string" && NOXID_TRACE_ID.test(inbound) ? inbound : __noxidGeneratedTraceId() };
1543}
1544
1545function __noxidTraceContext(parent = null) {
1546 // One sequence counter per trace, and one deferred `request.start` per trace.
1547 // A nested scope carries a reference to the trace's shared cell instead of
1548 // instantiating its own, so `sequence` starts at one per trace and follows
1549 // emission order across every nested scope, and the agent door's deferred
1550 // start is still the trace's first record even when the first thing the trace
1551 // emits comes from a subrequest scope. docs/language-reference.md states the
1552 // contract; evaluator ruling 2026-09-03.
1553 const inherited = parent !== null && typeof parent === "object" ? parent.shared : null;
1554 const shared = inherited !== null && typeof inherited === "object" ? inherited : { sequence: 0, pendingStartMethod: null, root: null, agentSemanticId: null, actingPrincipal: null };
1555 const trace = { id: typeof parent === "string" ? parent : parent?.id ?? __noxidGeneratedTraceId(), shared };
1556 if (shared.root === null) shared.root = trace;
1557 if (typeof parent?.parentSpanId === "string") trace.parentSpanId = parent.parentSpanId;
1558 if (Number.isInteger(parent?.traceFlags)) trace.traceFlags = parent.traceFlags;
1559 return trace;
1560}
1561
1562function __noxidTraceNow() {
1563 try {
1564 const value = globalThis.performance?.now?.();
1565 if (Number.isFinite(value)) return value;
1566 } catch {}
1567 return Date.now();
1568}
1569
1570function __noxidTraceNewSpanId() {
1571 /* noxid-server:trace-span-id */
1572 return null;
1573}
1574
1575function __noxidTraceDuration(startedAt) {
1576 return Math.max(0, __noxidTraceNow() - startedAt);
1577}
1578
1579function __noxidTraceBeginSpan(trace) {
1580 if (trace === null) return null;
1581 return Object.freeze({
1582 trace,
1583 startedAt: __noxidTraceNow(),
1584 spanId: __noxidTraceNewSpanId(),
1585 parentSpanId: trace.requestSpanId ?? trace.parentSpanId,
1586 });
1587}
1588
1589function __noxidTraceFinishSpan(span, event, fields = null) {
1590 if (span === null) return;
1591 __noxidTraceEmit(span.trace, event, {
1592 ...(fields ?? Object.create(null)),
1593 durationMs: __noxidTraceDuration(span.startedAt),
1594 spanId: span.spanId,
1595 parentSpanId: span.parentSpanId,
1596 });
1597}
1598
1599function __noxidTraceEmit(trace, event, fields = null) {
1600 let developmentCapture = false;
1601 /* noxid-server:development-trace-context */
1602 const loggable = tracingMode !== "off" && (tracingMode !== "requests" || event === "request.start" || event === "request.end");
1603 if (trace === null && developmentCapture) trace = __noxidTraceContext();
1604 if (trace === null || !loggable && !developmentCapture) return;
1605 if (event !== "request.start" && typeof trace.shared.pendingStartMethod === "string") {
1606 const method = trace.shared.pendingStartMethod;
1607 trace.shared.pendingStartMethod = null;
1608 // The start belongs to the request root, not to whichever nested scope
1609 // happened to emit first.
1610 __noxidTraceEmit(trace.shared.root ?? trace, "request.start", { method });
1611 }
1612 const record = Object.create(null);
1613 record.schema = NOXID_TRACE_SCHEMA;
1614 record.traceId = trace.id;
1615 record.sequence = ++trace.shared.sequence;
1616 record.event = event;
1617 record.timestampMs = Date.now();
1618 const tracedAgentSemanticId = typeof trace.agentSemanticId === "string" ? trace.agentSemanticId : trace.shared.agentSemanticId;
1619 const tracedActingPrincipal = typeof trace.actingPrincipal === "string" ? trace.actingPrincipal : trace.shared.actingPrincipal;
1620 if (typeof tracedAgentSemanticId === "string" && NOXID_SEMANTIC_ID.test(tracedAgentSemanticId)) record.agentSemanticId = tracedAgentSemanticId;
1621 if (typeof tracedActingPrincipal === "string" && NOXID_PRINCIPAL.test(tracedActingPrincipal)) record.actingPrincipal = tracedActingPrincipal;
1622 if (fields !== null) {
1623 if (typeof fields.semanticId === "string" && NOXID_SEMANTIC_ID.test(fields.semanticId)) record.semanticId = fields.semanticId;
1624 if (typeof fields.code === "string" && NOXID_DIAGNOSTIC_CODE.test(fields.code)) record.code = fields.code;
1625 if (typeof fields.capability === "string" && NOXID_CAPABILITY.test(fields.capability)) record.capability = fields.capability;
1626 if (typeof fields.route === "string" && NOXID_ROUTE.test(fields.route)) {
1627 record.route = fields.route;
1628 trace.route = fields.route;
1629 } else if (typeof trace.route === "string") record.route = trace.route;
1630 if (Number.isInteger(fields.status) && fields.status >= 100 && fields.status <= 599) record.status = fields.status;
1631 if (typeof fields.method === "string" && /^[A-Z]{1,16}$/.test(fields.method)) record.method = fields.method;
1632 if (typeof fields.state === "string" && /^[A-Za-z][A-Za-z-]{0,31}$/.test(fields.state)) record.state = fields.state;
1633 if (typeof fields.transition === "string" && /^[A-Za-z][A-Za-z]{0,31}$/.test(fields.transition)) record.transition = fields.transition;
1634 if (typeof fields.jobId === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(fields.jobId)) record.jobId = fields.jobId;
1635 if (Number.isSafeInteger(fields.attempts) && fields.attempts >= 0) record.attempts = fields.attempts;
1636 if (Number.isFinite(fields.durationMs) && fields.durationMs >= 0) record.durationMs = fields.durationMs;
1637 if (typeof fields.spanId === "string" && /^[0-9a-f]{16}$/.test(fields.spanId) && !/^0+$/.test(fields.spanId)) record.spanId = fields.spanId;
1638 if (typeof fields.parentSpanId === "string" && /^[0-9a-f]{16}$/.test(fields.parentSpanId) && !/^0+$/.test(fields.parentSpanId)) record.parentSpanId = fields.parentSpanId;
1639 if (typeof fields.spanName === "string" && /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/.test(fields.spanName)) record.spanName = fields.spanName;
1640 if (typeof fields.driver === "string" && new Set(["memory", "postgres", "redis"]).has(fields.driver)) record.driver = fields.driver;
1641 if (typeof fields.kind === "string" && new Set(["invalidation", "presence"]).has(fields.kind)) record.kind = fields.kind;
1642 if (typeof fields.agentSemanticId === "string" && NOXID_SEMANTIC_ID.test(fields.agentSemanticId)) record.agentSemanticId = fields.agentSemanticId;
1643 if (typeof fields.actingPrincipal === "string" && NOXID_PRINCIPAL.test(fields.actingPrincipal)) record.actingPrincipal = fields.actingPrincipal;
1644 if (typeof fields.dataTable === "string" && NOXID_DATA_IDENTIFIER.test(fields.dataTable)) record.dataTable = fields.dataTable;
1645 if (typeof fields.scopeColumn === "string" && NOXID_DATA_IDENTIFIER.test(fields.scopeColumn)) record.scopeColumn = fields.scopeColumn;
1646 // WO-30 model spans. Identity, tokens and retries only: a prompt or a
1647 // completion is never presented to this serializer, so it cannot leak.
1648 if (typeof fields.model === "string" && NOXID_MODEL_NAME.test(fields.model)) record.model = fields.model;
1649 if (typeof fields.modelProvider === "string" && NOXID_MODEL_PROVIDERS.has(fields.modelProvider)) record.modelProvider = fields.modelProvider;
1650 if (typeof fields.modelId === "string" && NOXID_MODEL_ID.test(fields.modelId)) record.modelId = fields.modelId;
1651 if (Number.isSafeInteger(fields.tokensInput) && fields.tokensInput >= 0) record.tokensInput = fields.tokensInput;
1652 if (Number.isSafeInteger(fields.tokensOutput) && fields.tokensOutput >= 0) record.tokensOutput = fields.tokensOutput;
1653 if (Number.isSafeInteger(fields.modelRetries) && fields.modelRetries >= 0 && fields.modelRetries <= 5) record.modelRetries = fields.modelRetries;
1654 // WO-31 agent spans. `agent` is the declared agent name (`noxid.agent`),
1655 // `agentRun` the run id (`noxid.agent.run`), `agentTurn` the zero-based
1656 // turn index (`noxid.agent.turn`), and `toolEndpoint` the dispatched
1657 // endpoint's semantic id (`noxid.tool.endpoint`). Token counts reuse the
1658 // model fields above. No prompt, instruction, argument, or tool-result
1659 // content is ever presented to this serializer.
1660 if (typeof fields.agent === "string" && NOXID_AGENT_NAME.test(fields.agent)) record.agent = fields.agent;
1661 if (typeof fields.agentRun === "string" && NOXID_AGENT_RUN.test(fields.agentRun)) record.agentRun = fields.agentRun;
1662 if (Number.isSafeInteger(fields.agentTurn) && fields.agentTurn >= 0) record.agentTurn = fields.agentTurn;
1663 if (typeof fields.toolEndpoint === "string" && NOXID_SEMANTIC_ID.test(fields.toolEndpoint)) record.toolEndpoint = fields.toolEndpoint;
1664 }
1665 if (record.parentSpanId === undefined && event !== "request.start" && event !== "request.end" && typeof trace.requestSpanId === "string") {
1666 record.parentSpanId = trace.requestSpanId;
1667 }
1668 /* noxid-server:trace-export */
1669 /* noxid-server:development-trace-capture */
1670}
1671
1672function __noxidActingPrincipal(principal) {
1673 return principal?.scope === null ? "system" : typeof principal?.scope === "string" ? `session:${__noxidPrincipalPart(principal.scope)}` : null;
1674}
1675
1676function __noxidTraceBindPrincipal(context, principal) {
1677 if (principal?.kind !== "agent" || typeof principal.agent !== "string") return;
1678 const trace = __noxidTraceForRequest(context?.request);
1679 if (trace === null) return;
1680 trace.agentSemanticId = principal.agent;
1681 trace.actingPrincipal = __noxidActingPrincipal(principal);
1682 // The acting identity belongs to the trace, not to the scope that happened to
1683 // learn it. An agent door binds inside its endpoint subrequest, so without
1684 // this the request's own start and end records would lose the identity the
1685 // door was dispatched under (evaluator ruling 2026-09-03).
1686 trace.shared.agentSemanticId = trace.agentSemanticId;
1687 trace.shared.actingPrincipal = trace.actingPrincipal;
1688}
1689
1690function __noxidTraceDataAccess(context, fields) {
1691 if (tracingMode !== "full") return;
1692 const principal = context?.principal;
1693 const requestTrace = __noxidTraceForRequest(context?.request);
1694 const trace = requestTrace ?? __noxidTraceContext(
1695 typeof context?.traceId === "string" && NOXID_TRACE_ID.test(context.traceId) ? context.traceId : null,
1696 );
1697 if (principal?.kind === "agent" && typeof principal.agent === "string") {
1698 trace.agentSemanticId = principal.agent;
1699 trace.actingPrincipal = __noxidActingPrincipal(principal);
1700 }
1701 __noxidTraceEmit(trace, "data.access", {
1702 semanticId: context?.semanticId,
1703 agentSemanticId: principal?.agent,
1704 actingPrincipal: principal?.kind === "agent" ? __noxidActingPrincipal(principal) : null,
1705 dataTable: fields?.table,
1706 scopeColumn: fields?.principalColumn,
1707 });
1708}
1709
1710function __noxidTraceForRequest(request) {
1711 return request !== null && typeof request === "object" ? __noxidRequestTraces.get(request) ?? null : null;
1712}
1713
1714function __noxidTraceRoute(request, route) {
1715 const trace = __noxidTraceForRequest(request);
1716 if (trace !== null && typeof route === "string" && NOXID_ROUTE.test(route)) trace.route = route;
1717}
1718
1719function __noxidTraceIdForRequest(request) {
1720 return __noxidTraceForRequest(request)?.id ?? null;
1721}
1722
1723export function noxidTraceId(request) {
1724 return __noxidTraceIdForRequest(request);
1725}
1726
1727export function inheritNoxidRequestTrace(parent, child, semanticId = null) {
1728 const parentTrace = __noxidTraceForRequest(parent);
1729 if (parentTrace === null || child === null || typeof child !== "object") return;
1730 const trace = __noxidTraceContext({
1731 id: parentTrace.id,
1732 // The subrequest is a nested scope of the same trace, so it shares the
1733 // parent's sequence counter and deferred start rather than starting its own.
1734 shared: parentTrace.shared,
1735 parentSpanId: parentTrace.requestSpanId ?? parentTrace.parentSpanId,
1736 traceFlags: parentTrace.traceFlags,
1737 });
1738 trace.inheritedRequest = true;
1739 if (typeof semanticId === "string" && NOXID_SEMANTIC_ID.test(semanticId)) trace.subrequestSemanticId = semanticId;
1740 if (typeof parentTrace.agentSemanticId === "string") trace.agentSemanticId = parentTrace.agentSemanticId;
1741 if (typeof parentTrace.actingPrincipal === "string") trace.actingPrincipal = parentTrace.actingPrincipal;
1742 __noxidRequestTraces.set(child, trace);
1743}
1744
1745function __noxidTraceBeginSemantic(request) {
1746 let trace = __noxidTraceForRequest(request);
1747 if (trace === null && tracingMode === "full") trace = __noxidTraceContext();
1748 return __noxidTraceBeginSpan(trace);
1749}
1750
1751function __noxidTraceFinishSemantic(span, event, semanticId, fields = null) {
1752 __noxidTraceFinishSpan(span, event, fields === null ? { semanticId } : { semanticId, ...fields });
1753}
1754
1755function __noxidTraceSemantic(request, event, semanticId, fields = null) {
1756 const span = __noxidTraceBeginSemantic(request);
1757 __noxidTraceFinishSemantic(span, event, semanticId, fields);
1758 return span?.trace?.id ?? null;
1759}
1760
1761export function traceNoxidSemantic(request, event, semanticId) {
1762 if (!new Set(["middleware", "endpoint", "action", "task"]).has(event)) return null;
1763 return __noxidTraceSemantic(request, event, semanticId);
1764}
1765
1766export function traceNoxidFailure(response, code, semanticId) {
1767 return __noxidTraceFailure(response, code, semanticId);
1768}
1769
1770function __noxidTraceFailure(response, code, semanticId, capability = null) {
1771 if (response !== null && typeof response === "object") {
1772 __noxidFailureSpans.set(response, { code, semanticId, capability, emitted: false });
1773 }
1774 return response;
1775}
1776
1777function __noxidTraceCopyFailure(source, target) {
1778 const failure = __noxidFailureSpans.get(source);
1779 if (failure !== undefined && target !== null && typeof target === "object") __noxidFailureSpans.set(target, failure);
1780 return target;
1781}
1782
1783function __noxidTraceResponseFailure(request, response) {
1784 const failure = __noxidFailureSpans.get(response);
1785 if (failure === undefined || failure.emitted) return;
1786 failure.emitted = true;
1787 const capability = (failure.code.includes("CAPABILITY") && failure.code.endsWith("_DENIED")) || failure.code === "CACHE_INVALIDATION_DENIED";
1788 __noxidTraceEmit(__noxidTraceForRequest(request), capability ? "capability.denied" : "validation.refused", failure);
1789}
1790
1791function __noxidTraceFinishRequest(trace, event, status) {
1792 __noxidTraceEmit(trace, event, {
1793 status,
1794 method: trace.requestMethod,
1795 semanticId: trace.subrequestSemanticId,
1796 durationMs: __noxidTraceDuration(trace.requestStartedAt),
1797 spanId: trace.requestSpanId,
1798 parentSpanId: trace.requestParentSpanId,
1799 spanName: event === "request.end" ? "request" : event,
1800 });
1801}
1802
1803function __noxidTraceStreamingResponse(request, trace, response, event = "request.end") {
1804 const reader = response.body.getReader();
1805 let finished = false;
1806 const finish = (status) => {
1807 if (finished) return;
1808 finished = true;
1809 __noxidTraceFinishRequest(trace, event, status);
1810 if (request !== null && typeof request === "object") __noxidRequestTraces.delete(request);
1811 };
1812 const body = new ReadableStream({
1813 async pull(controller) {
1814 try {
1815 const chunk = await reader.read();
1816 if (chunk.done) {
1817 finish(response.status);
1818 controller.close();
1819 } else {
1820 controller.enqueue(chunk.value);
1821 }
1822 } catch (cause) {
1823 finish(500);
1824 controller.error(cause);
1825 }
1826 },
1827 async cancel(reason) {
1828 try { await reader.cancel(reason); }
1829 finally { finish(response.status); }
1830 },
1831 });
1832 return __noxidTraceCopyFailure(response, new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }));
1833}
1834
1835export async function withNoxidRequestTrace(request, operation) {
1836 const inherited = __noxidTraceForRequest(request);
1837 if (inherited !== null) {
1838 if (inherited.requestActive === true || inherited.inheritedRequest !== true) {
1839 const response = await operation(inherited);
1840 __noxidTraceResponseFailure(request, response);
1841 return response;
1842 }
1843 inherited.requestActive = true;
1844 inherited.requestStartedAt = __noxidTraceNow();
1845 inherited.requestSpanId = __noxidTraceNewSpanId();
1846 inherited.requestParentSpanId = inherited.parentSpanId;
1847 try { inherited.requestMethod = request.method.toUpperCase(); } catch {}
1848 const event = typeof inherited.subrequestSemanticId === "string" && inherited.subrequestSemanticId.startsWith("route-loader:") ? "loader" : "subrequest";
1849 let streaming = false;
1850 try {
1851 const response = await operation(inherited);
1852 __noxidTraceResponseFailure(request, response);
1853 streaming = response?.body !== null && (response?.headers?.get("x-noxid-ssr-stream") === "1" || response?.headers?.get("content-type")?.toLowerCase().startsWith("text/event-stream"));
1854 if (streaming) return __noxidTraceStreamingResponse(request, inherited, response, event);
1855 __noxidTraceFinishRequest(inherited, event, response?.status);
1856 return response;
1857 } catch (cause) {
1858 __noxidTraceFinishRequest(inherited, event, 500);
1859 throw cause;
1860 } finally {
1861 if (!streaming && request !== null && typeof request === "object") __noxidRequestTraces.delete(request);
1862 }
1863 }
1864 if (tracingMode === "off") return operation(null);
1865 const trace = __noxidTraceContext(__noxidRequestTraceContext(request));
1866 trace.requestActive = true;
1867 trace.requestStartedAt = __noxidTraceNow();
1868 trace.requestSpanId = __noxidTraceNewSpanId();
1869 trace.requestParentSpanId = trace.parentSpanId;
1870 if (request !== null && typeof request === "object") __noxidRequestTraces.set(request, trace);
1871 let method = null;
1872 let deferredAgentStart = false;
1873 let streaming = false;
1874 try { method = request.method.toUpperCase(); } catch {}
1875 trace.requestMethod = method;
1876 try { deferredAgentStart = new URL(request.url).pathname.endsWith("/_noxid/mcp"); } catch {}
1877 if (deferredAgentStart) trace.shared.pendingStartMethod = method;
1878 else __noxidTraceEmit(trace, "request.start", { method });
1879 try {
1880 const response = await operation(trace);
1881 __noxidTraceResponseFailure(request, response);
1882 streaming = response?.body !== null && (response?.headers?.get("x-noxid-ssr-stream") === "1" || response?.headers?.get("content-type")?.toLowerCase().startsWith("text/event-stream"));
1883 if (streaming) return __noxidTraceStreamingResponse(request, trace, response);
1884 __noxidTraceFinishRequest(trace, "request.end", response?.status);
1885 return response;
1886 } catch (cause) {
1887 __noxidTraceFinishRequest(trace, "request.end", 500);
1888 throw cause;
1889 } finally {
1890 // Streaming responses retain request ownership until their body closes or
1891 // is cancelled; ordinary responses release it here.
1892 if (!streaming && request !== null && typeof request === "object") __noxidRequestTraces.delete(request);
1893 }
1894}
1895"##;
1896
1897const PUBSUB_RUNTIME: &str = r##"
1898const NOXID_PUBSUB_SCHEMA = "noxid.pubsub.v1";
1899const NOXID_PUBSUB_KINDS = new Set(["invalidation", "presence"]);
1900const NOXID_PUBSUB_CHANNEL = `noxid_pubsub_v1_${applicationNamespace}`;
1901const NOXID_PUBSUB_MAX_BYTES = 7_000;
1902const __noxidMemoryPubSubKey = Symbol.for("noxid.pubsub.memory.v1");
1903
1904function __noxidPubSubNow() {
1905 try { return performance.now(); } catch { return Date.now(); }
1906}
1907
1908function __noxidPubSubDuration(trace, event, startedAt, envelope, state) {
1909 __noxidTraceEmit(trace, event, {
1910 semanticId: envelope.semanticId,
1911 driver: pubSubDriver,
1912 kind: envelope.kind,
1913 state,
1914 durationMs: Math.max(0, __noxidPubSubNow() - startedAt),
1915 });
1916}
1917
1918function __noxidPubSubFailure(code, message, cause) {
1919 return Object.assign(new Error(message, cause === undefined ? undefined : { cause }), { code });
1920}
1921
1922function __noxidPubSubPrincipal(principal) {
1923 if (principal === null || typeof principal !== "object" || !__noxidPrincipalValues.has(principal)) {
1924 throw __noxidPubSubFailure("PUBSUB_PRINCIPAL_INVALID", "live events require a compiler-owned canonical principal");
1925 }
1926 let canonical;
1927 try { canonical = principal.canonical; } catch {}
1928 return __noxidPubSubCanonical(canonical);
1929}
1930
1931function __noxidPubSubCanonical(canonical) {
1932 if (typeof canonical !== "string" || canonical.length === 0 || canonical.length > 1024) {
1933 throw __noxidPubSubFailure("PUBSUB_PRINCIPAL_INVALID", "live events require the stable canonical principal serialization from the request context");
1934 }
1935 return canonical;
1936}
1937
1938function __noxidPubSubIdentity(kind, semanticId, principal) {
1939 if (!NOXID_PUBSUB_KINDS.has(kind)) throw __noxidPubSubFailure("PUBSUB_KIND_INVALID", "compiler-owned pub/sub kind is invalid");
1940 if (typeof semanticId !== "string" || !NOXID_SEMANTIC_ID.test(semanticId)) throw __noxidPubSubFailure("PUBSUB_SEMANTIC_ID_INVALID", "compiler-owned pub/sub semantic id is invalid");
1941 return Object.freeze({ kind, semanticId, principal: __noxidPubSubPrincipal(principal) });
1942}
1943
1944export function __noxidPubSubTopic(kind, semanticId, principal) {
1945 const identity = __noxidPubSubIdentity(kind, semanticId, principal);
1946 return __noxidPubSubTopicFromCanonical(identity.kind, identity.semanticId, identity.principal);
1947}
1948
1949function __noxidPubSubTopicFromCanonical(kind, semanticId, principal) {
1950 __noxidPubSubCanonical(principal);
1951 return `noxid:live:v1:${applicationNamespace}:${kind}:${encodeURIComponent(semanticId)}:${encodeURIComponent(principal)}`;
1952}
1953
1954function __noxidPubSubEventId(value) {
1955 if (value === undefined) {
1956 try { value = globalThis.crypto?.randomUUID?.().replaceAll("-", "_"); } catch {}
1957 }
1958 if (typeof value !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(value)) {
1959 throw __noxidPubSubFailure("PUBSUB_EVENT_ID_INVALID", "compiler-owned pub/sub event id must be a bounded stable identifier");
1960 }
1961 return value;
1962}
1963
1964export function __noxidPubSubEvent(kind, semanticId, principal, body = null, id = undefined) {
1965 const identity = __noxidPubSubIdentity(kind, semanticId, principal);
1966 return __noxidPubSubEventFromCanonical(identity.kind, identity.semanticId, identity.principal, body, id);
1967}
1968
1969function __noxidPubSubEventFromCanonical(kind, semanticId, principal, body = null, id = undefined) {
1970 if (!NOXID_PUBSUB_KINDS.has(kind) || typeof semanticId !== "string" || !NOXID_SEMANTIC_ID.test(semanticId)) {
1971 throw __noxidPubSubFailure("PUBSUB_EVENT_DRIFT", "compiler-owned pub/sub event identity is invalid");
1972 }
1973 principal = __noxidPubSubCanonical(principal);
1974 if (kind === "invalidation" && body !== null) {
1975 throw __noxidPubSubFailure("PUBSUB_INVALIDATION_PAYLOAD", "live invalidations carry no payload; publish only the resource identity and refetch through its validated path");
1976 }
1977 const event = Object.freeze({
1978 schema: NOXID_PUBSUB_SCHEMA,
1979 application: applicationNamespace,
1980 id: __noxidPubSubEventId(id),
1981 kind,
1982 semanticId,
1983 principal,
1984 body,
1985 });
1986 let encoded;
1987 try { encoded = JSON.stringify(event); }
1988 catch (cause) { throw __noxidPubSubFailure("PUBSUB_EVENT_INVALID", "compiler-owned pub/sub event must be serializable typed data", cause); }
1989 if (new TextEncoder().encode(encoded).byteLength > NOXID_PUBSUB_MAX_BYTES) throw __noxidPubSubFailure("PUBSUB_EVENT_TOO_LARGE", "compiler-owned pub/sub event exceeds the 7000-byte cross-driver bound");
1990 return event;
1991}
1992
1993function __noxidPubSubDecode(raw) {
1994 let value;
1995 try { value = typeof raw === "string" ? JSON.parse(raw) : raw; }
1996 catch (cause) { throw __noxidPubSubFailure("PUBSUB_EVENT_DRIFT", "pub/sub delivered malformed JSON", cause); }
1997 if (value === null || typeof value !== "object" || Array.isArray(value) || value.schema !== NOXID_PUBSUB_SCHEMA || value.application !== applicationNamespace) {
1998 throw __noxidPubSubFailure("PUBSUB_EVENT_DRIFT", "pub/sub delivered an unsupported event envelope");
1999 }
2000 if (!NOXID_PUBSUB_KINDS.has(value.kind) || typeof value.semanticId !== "string" || !NOXID_SEMANTIC_ID.test(value.semanticId)) {
2001 throw __noxidPubSubFailure("PUBSUB_EVENT_DRIFT", "pub/sub delivered an invalid typed event identity");
2002 }
2003 const identity = Object.freeze({ kind: value.kind, semanticId: value.semanticId, principal: __noxidPubSubCanonical(value.principal) });
2004 if (value.kind === "invalidation" && value.body !== null) throw __noxidPubSubFailure("PUBSUB_EVENT_DRIFT", "pub/sub delivered an invalidation payload");
2005 return Object.freeze({ schema: NOXID_PUBSUB_SCHEMA, application: applicationNamespace, id: __noxidPubSubEventId(value.id), ...identity, body: value.body ?? null });
2006}
2007
2008function __noxidMemoryPubSub() {
2009 let registry = globalThis[__noxidMemoryPubSubKey];
2010 if (!(registry instanceof Map)) {
2011 registry = new Map();
2012 Object.defineProperty(globalThis, __noxidMemoryPubSubKey, { value: registry, configurable: true });
2013 }
2014 return Object.freeze({
2015 async publish(topic, encoded) {
2016 for (const receive of [...(registry.get(topic) ?? [])]) receive(encoded);
2017 },
2018 async subscribe(topic, receive) {
2019 let subscribers = registry.get(topic);
2020 if (subscribers === undefined) registry.set(topic, subscribers = new Set());
2021 subscribers.add(receive);
2022 return async () => {
2023 subscribers.delete(receive);
2024 if (subscribers.size === 0) registry.delete(topic);
2025 };
2026 },
2027 });
2028}
2029
2030let __noxidPubSubPostgresPromise;
2031async function __noxidPostgresPubSub() {
2032 if (__noxidPubSubPostgresPromise !== undefined) return __noxidPubSubPostgresPromise;
2033 __noxidPubSubPostgresPromise = (async () => {
2034 const url = queueDatabaseUrl();
2035 if (url === null) throw __noxidPubSubFailure("PUBSUB_POSTGRES_URL_REQUIRED", "DATABASE_URL is required for the Postgres live publisher");
2036 let postgres;
2037 try { postgres = (await import("postgres")).default; }
2038 catch { throw __noxidPubSubFailure("PUBSUB_POSTGRES_DRIVER_MISSING", "the admitted Postgres driver is unavailable for live publishing"); }
2039 const sql = postgres(url, { max: Math.max(1, Math.min(databasePoolSize, 2)) });
2040 const subscribers = new Map();
2041 let listening;
2042 const ensureListening = async () => {
2043 if (listening === undefined) {
2044 listening = Promise.resolve(sql.listen(NOXID_PUBSUB_CHANNEL, (encoded) => {
2045 let decoded;
2046 try { decoded = __noxidPubSubDecode(encoded); } catch { return; }
2047 const topic = __noxidPubSubTopicFromCanonical(decoded.kind, decoded.semanticId, decoded.principal);
2048 for (const receive of [...(subscribers.get(topic) ?? [])]) receive(encoded);
2049 }));
2050 }
2051 await listening;
2052 };
2053 const stopListeningIfIdle = async () => {
2054 if (subscribers.size !== 0 || listening === undefined) return;
2055 const active = listening;
2056 listening = undefined;
2057 const listener = await active;
2058 if (typeof listener === "function") await listener();
2059 else if (typeof listener?.unlisten === "function") await listener.unlisten();
2060 };
2061 return Object.freeze({
2062 async publish(_topic, encoded) {
2063 await sql.notify(NOXID_PUBSUB_CHANNEL, encoded);
2064 },
2065 async subscribe(topic, receive) {
2066 await ensureListening();
2067 let listeners = subscribers.get(topic);
2068 if (listeners === undefined) subscribers.set(topic, listeners = new Set());
2069 listeners.add(receive);
2070 return async () => {
2071 listeners.delete(receive);
2072 if (listeners.size === 0) subscribers.delete(topic);
2073 await stopListeningIfIdle();
2074 };
2075 },
2076 });
2077 })();
2078 try { return await __noxidPubSubPostgresPromise; }
2079 catch (cause) { __noxidPubSubPostgresPromise = undefined; throw cause; }
2080}
2081
2082function __noxidRedisPubSub() {
2083 if (typeof __noxidRedisPubSubPublish !== "function" || typeof __noxidRedisPubSubSubscribe !== "function") {
2084 throw __noxidPubSubFailure("PUBSUB_REDIS_DRIVER_MISSING", "the admitted WO-39 RESP pub/sub extension is unavailable");
2085 }
2086 return Object.freeze({
2087 publish: (topic, encoded) => __noxidRedisPubSubPublish(topic, encoded),
2088 subscribe: (topic, receive) => __noxidRedisPubSubSubscribe(topic, receive),
2089 });
2090}
2091
2092let __noxidPubSubAdapterPromise;
2093async function __noxidPubSubAdapter() {
2094 if (__noxidPubSubAdapterPromise === undefined) {
2095 __noxidPubSubAdapterPromise = Promise.resolve(
2096 pubSubDriver === "memory" ? __noxidMemoryPubSub()
2097 : pubSubDriver === "postgres" ? __noxidPostgresPubSub()
2098 : pubSubDriver === "redis" ? __noxidRedisPubSub()
2099 : Promise.reject(__noxidPubSubFailure("PUBSUB_DRIVER_INVALID", "compiler emitted an unsupported pub/sub driver")),
2100 );
2101 }
2102 return __noxidPubSubAdapterPromise;
2103}
2104
2105export async function __noxidPubSubPublish(event) {
2106 const envelope = __noxidPubSubDecode(event);
2107 const topic = __noxidPubSubTopicFromCanonical(envelope.kind, envelope.semanticId, envelope.principal);
2108 const encoded = JSON.stringify(envelope);
2109 const trace = tracingMode === "full" ? __noxidTraceContext() : null;
2110 const startedAt = __noxidPubSubNow();
2111 try {
2112 await (await __noxidPubSubAdapter()).publish(topic, encoded);
2113 __noxidPubSubDuration(trace, "pubsub.publish", startedAt, envelope, "delivered");
2114 } catch (cause) {
2115 __noxidPubSubDuration(trace, "pubsub.publish", startedAt, envelope, "retry");
2116 throw cause;
2117 }
2118}
2119
2120export async function __noxidPubSubSubscribe(kind, semanticId, principal, deliver, options = Object.create(null)) {
2121 if (typeof deliver !== "function") throw __noxidPubSubFailure("PUBSUB_SUBSCRIBER_INVALID", "pub/sub subscriber must be a compiler-owned delivery function");
2122 const identity = __noxidPubSubIdentity(kind, semanticId, principal);
2123 const topic = __noxidPubSubTopic(kind, semanticId, principal);
2124 const schedule = typeof options.schedule === "function" ? options.schedule : globalThis.setTimeout;
2125 const cancel = typeof options.cancel === "function" ? options.cancel : globalThis.clearTimeout;
2126 const coalesceKey = typeof options.coalesceKey === "function"
2127 ? options.coalesceKey
2128 : (event) => event.kind === "invalidation" ? `${event.kind}:${event.semanticId}:${event.principal}` : event.id;
2129 const pending = new Map();
2130 let stopped = false;
2131 const run = async (key) => {
2132 const entry = pending.get(key);
2133 if (entry === undefined || stopped) return;
2134 entry.timer = null;
2135 const version = entry.version;
2136 const trace = tracingMode === "full" ? __noxidTraceContext() : null;
2137 const startedAt = __noxidPubSubNow();
2138 try {
2139 await deliver(entry.event);
2140 __noxidPubSubDuration(trace, "pubsub.deliver", startedAt, entry.event, "delivered");
2141 if (entry.version === version) pending.delete(key);
2142 else entry.timer = schedule(() => run(key), pubSubCoalescingMs);
2143 } catch {
2144 __noxidPubSubDuration(trace, "pubsub.deliver", startedAt, entry.event, "retry");
2145 if (!stopped) entry.timer = schedule(() => run(key), pubSubCoalescingMs);
2146 }
2147 };
2148 const receive = (raw) => {
2149 if (stopped) return;
2150 let event;
2151 try { event = __noxidPubSubDecode(raw); } catch { return; }
2152 if (event.kind !== identity.kind || event.semanticId !== identity.semanticId || event.principal !== identity.principal) return;
2153 let key;
2154 try { key = coalesceKey(event); } catch { return; }
2155 if (typeof key !== "string" || key.length === 0 || key.length > 2048) return;
2156 const entry = pending.get(key);
2157 if (entry === undefined) {
2158 const created = { event, version: 1, timer: null };
2159 created.timer = schedule(() => run(key), pubSubCoalescingMs);
2160 pending.set(key, created);
2161 } else {
2162 entry.event = event;
2163 entry.version += 1;
2164 }
2165 };
2166 const unsubscribe = await (await __noxidPubSubAdapter()).subscribe(topic, receive);
2167 return async () => {
2168 if (stopped) return;
2169 stopped = true;
2170 for (const entry of pending.values()) if (entry.timer !== null) cancel(entry.timer);
2171 pending.clear();
2172 await unsubscribe();
2173 };
2174}
2175"##;
2176
2177fn endpoint_multipart_limits() -> String {
2182 format!(
2183 "const ENDPOINT_MULTIPART_HEADER_MAX_BYTES = {};\nconst ENDPOINT_MULTIPART_SCALAR_MAX_BYTES = {};\nconst ENDPOINT_MULTIPART_MAX_PARTS = {};\nconst ENDPOINT_UPLOAD_NAME_MAX_BYTES = {};\n",
2184 noxid_ir::MULTIPART_PART_HEADER_MAX_BYTES,
2185 noxid_ir::MULTIPART_SCALAR_MAX_BYTES,
2186 noxid_ir::MULTIPART_MAX_PARTS,
2187 noxid_ir::UPLOAD_FILENAME_MAX_BYTES,
2188 )
2189}
2190
2191const ENDPOINT_RUNTIME: &str = r##"const endpointRateStorage = typeof __noxidStorage === "function" ? __noxidStorage("noxid:endpoint-rate") : null;
2192const endpointIdempotencyStorage = typeof __noxidStorage === "function" ? __noxidStorage("noxid:endpoint-idempotency") : null;
2193const endpointRateLocks = new Map();
2194const endpointIdempotencyLocks = new Map();
2195const endpointIdempotencyInFlight = new Map();
2196const endpointStreamHistories = new Map();
2197const ENDPOINT_RATE_BUCKET_MAX_ENTRIES = 10_000;
2198const ENDPOINT_IDEMPOTENCY_MAX_ENTRIES = 1024;
2199const ENDPOINT_IDEMPOTENCY_TTL_MS = 86_400_000;
2200const ENDPOINT_STREAM_HEARTBEAT_MS = 15_000;
2201const ENDPOINT_STREAM_MAX_HISTORIES = 128;
2202const ENDPOINT_STREAM_MAX_EVENTS = 256;
2203const ENDPOINT_STREAM_MAX_EVENT_BYTES = 262_144;
2204const ENDPOINT_STREAM_MAX_HISTORY_BYTES = 1_048_576;
2205
2206function streamError(schema, code, message, details = null) {
2207 return Object.freeze({ ok: false, error: Object.freeze({ code, message, semanticId: schema.id, details }) });
2208}
2209
2210function streamFrame(event, data, id = null) {
2211 const lines = [];
2212 if (id !== null) lines.push(`id: ${id}`);
2213 lines.push(`event: ${event}`);
2214 const encoded = JSON.stringify(data);
2215 for (const line of encoded.split("\n")) lines.push(`data: ${line}`);
2216 return `${lines.join("\n")}\n\n`;
2217}
2218
2219function streamErrorFrame(schema, code, message, details = null) {
2220 return streamFrame("noxid-error", streamError(schema, code, message, details));
2221}
2222
2223function parseStreamResumeId(value) {
2224 if (value === null) return null;
2225 if (value.length > 256) return Object.freeze({ invalid: true });
2226 const match = /^([A-Za-z0-9_-]{16,128}):([1-9][0-9]{0,15})$/.exec(value);
2227 if (match === null) return Object.freeze({ invalid: true });
2228 const sequence = Number(match[2]);
2229 return Number.isSafeInteger(sequence)
2230 ? Object.freeze({ token: match[1], sequence })
2231 : Object.freeze({ invalid: true });
2232}
2233
2234function newStreamToken() {
2235 if (typeof globalThis.crypto?.randomUUID !== "function" || typeof globalThis.crypto?.subtle?.digest !== "function") return null;
2236 return globalThis.crypto.randomUUID().replace(/-/g, "_");
2237}
2238
2239function stableStreamRequestValue(value, seen = new Set()) {
2240 if (value === null) return "null";
2241 if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
2242 if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value);
2243 if (typeof value !== "object" || seen.has(value)) throw new Error("unsupported stream request value");
2244 seen.add(value);
2245 let output;
2246 if (Array.isArray(value)) {
2247 output = `[${value.map((item) => stableStreamRequestValue(item, seen)).join(",")}]`;
2248 } else {
2249 output = `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStreamRequestValue(value[key], seen)}`).join(",")}}`;
2250 }
2251 seen.delete(value);
2252 return output;
2253}
2254
2255function endpointStreamPolicyIdentity(request, environment, middlewareContext) {
2256 const session = middlewareContext?.sessionId ?? middlewareContext?.session?.id ?? environment?.sessionId;
2257 if (typeof session === "string" && session.length > 0) return `session:${session}`;
2258 const ip = environment?.requestIdentity?.ip ?? environment?.ip ?? request.headers.get("cf-connecting-ip") ?? request.headers.get("x-real-ip");
2259 return typeof ip === "string" && ip.length > 0 ? `ip:${ip}` : null;
2260}
2261
2262async function endpointStreamRequestBinding(token, request, args, environment, middlewareContext) {
2263 const identity = endpointStreamPolicyIdentity(request, environment, middlewareContext);
2264 const contract = stableStreamRequestValue(args);
2265 const bytes = new TextEncoder().encode(`${token}\n${identity ?? "token-possession"}\n${contract}`);
2266 const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
2267 let binary = "";
2268 for (const byte of digest) binary += String.fromCharCode(byte);
2269 return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2270}
2271
2272function reserveStreamHistory(schema, token, requestBinding) {
2273 while (endpointStreamHistories.size >= ENDPOINT_STREAM_MAX_HISTORIES) {
2274 const removable = [...endpointStreamHistories.values()].find((history) => history.completed);
2275 if (!removable) return null;
2276 endpointStreamHistories.delete(removable.token);
2277 }
2278 const history = { token, schemaId: schema.id, requestBinding, events: [], bytes: 0, nextSequence: 1, completed: false, terminal: null };
2279 endpointStreamHistories.set(token, history);
2280 return history;
2281}
2282
2283function recordStreamEvent(history, frame, bytes) {
2284 const sequence = history.nextSequence;
2285 history.nextSequence += 1;
2286 history.events.push(Object.freeze({ sequence, frame, bytes }));
2287 history.bytes += bytes;
2288 while (history.events.length > ENDPOINT_STREAM_MAX_EVENTS || history.bytes > ENDPOINT_STREAM_MAX_HISTORY_BYTES) {
2289 const removed = history.events.shift();
2290 history.bytes -= removed.bytes;
2291 }
2292 return sequence;
2293}
2294
2295function endpointFullPath(schema) {
2296 return applicationBasePath === "/" ? schema.path : `${applicationBasePath.replace(/\/$/, "")}${schema.path}`;
2297}
2298
2299function endpointSegments(pathname) {
2300 const segments = pathname.split("/");
2301 if (segments[0] === "") segments.shift();
2302 return segments;
2303}
2304
2305function matchEndpointPath(schema, pathname) {
2306 const expected = endpointSegments(endpointFullPath(schema));
2307 const actual = endpointSegments(pathname);
2308 if (expected.length !== actual.length) return Object.freeze({ kind: "miss" });
2309 const params = Object.create(null);
2310 let malformed = false;
2311 for (let index = 0; index < expected.length; index += 1) {
2312 const segment = expected[index];
2313 if (segment.startsWith("[") && segment.endsWith("]")) {
2314 let value;
2315 try { value = decodeURIComponent(actual[index]); }
2316 catch { malformed = true; continue; }
2317 if (value.length === 0) return Object.freeze({ kind: "miss" });
2318 params[segment.slice(1, -1)] = value;
2319 } else {
2320 let value;
2321 try { value = decodeURIComponent(actual[index]); }
2322 catch { return Object.freeze({ kind: "miss" }); }
2323 if (value !== segment) return Object.freeze({ kind: "miss" });
2324 }
2325 }
2326 return malformed
2327 ? Object.freeze({ kind: "malformed" })
2328 : Object.freeze({ kind: "match", params: Object.freeze(params) });
2329}
2330
2331function endpointMatches(pathname) {
2332 const matches = [];
2333 const malformed = [];
2334 for (const schema of endpointSchemas) {
2335 const candidate = matchEndpointPath(schema, pathname);
2336 if (candidate.kind === "match") matches.push(Object.freeze({ schema, params: candidate.params }));
2337 if (candidate.kind === "malformed") malformed.push(schema);
2338 }
2339 matches.sort((left, right) => {
2340 const leftDynamic = (left.schema.path.match(/\[/g) ?? []).length;
2341 const rightDynamic = (right.schema.path.match(/\[/g) ?? []).length;
2342 return leftDynamic - rightDynamic || left.schema.path.localeCompare(right.schema.path) || left.schema.method.localeCompare(right.schema.method);
2343 });
2344 malformed.sort((left, right) => left.path.localeCompare(right.path) || left.method.localeCompare(right.method));
2345 return Object.freeze({ matches: Object.freeze(matches), malformed: Object.freeze(malformed) });
2346}
2347
2348function endpointValidator(id) {
2349 const validator = typeValidators[id];
2350 return typeof validator === "function" ? validator : null;
2351}
2352
2353function isStrictUtcIsoDate(value) {
2354 if (typeof value !== "string") return false;
2355 const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec(value);
2356 if (match === null) return false;
2357 const year = Number(match[1]);
2358 const month = Number(match[2]);
2359 const day = Number(match[3]);
2360 const hour = Number(match[4]);
2361 const minute = Number(match[5]);
2362 const second = Number(match[6]);
2363 if (month < 1 || month > 12 || hour > 23 || minute > 59 || second > 59) return false;
2364 const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
2365 const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
2366 return day >= 1 && day <= days[month - 1];
2367}
2368
2369function optionalWireType(type) {
2370 return type.startsWith("Optional<") && type.endsWith(">") ? type.slice(9, -1) : null;
2371}
2372
2373function arrayWireType(type) {
2374 const optional = optionalWireType(type);
2375 if (optional !== null) return arrayWireType(optional);
2376 return type.startsWith("Array<") && type.endsWith(">") ? type.slice(6, -1) : null;
2377}
2378
2379function decodeEndpointScalar(raw, type) {
2380 const optional = optionalWireType(type);
2381 if (optional !== null) return decodeEndpointScalar(raw, optional);
2382 if (type === "String") return raw;
2383 if (type === "Date") {
2384 if (!isStrictUtcIsoDate(raw)) throw new Error("Date");
2385 return raw;
2386 }
2387 if (type === "Boolean") {
2388 if (raw === "true") return true;
2389 if (raw === "false") return false;
2390 throw new Error("Boolean");
2391 }
2392 if (type === "Int") {
2393 if (!/^-?(0|[1-9][0-9]*)$/.test(raw)) throw new Error("Int");
2394 const value = Number(raw);
2395 if (!Number.isSafeInteger(value)) throw new Error("Int");
2396 return value;
2397 }
2398 if (type === "Float") {
2399 if (!/^-?(0|[1-9][0-9]*)\.[0-9]+$/.test(raw)) throw new Error("Float");
2400 const value = Number(raw);
2401 if (!Number.isFinite(value)) throw new Error("Float");
2402 return value;
2403 }
2404 if (type === "Number") {
2405 if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(raw)) throw new Error("Number");
2406 const value = Number(raw);
2407 if (!Number.isFinite(value)) throw new Error("Number");
2408 return value;
2409 }
2410 try { return JSON.parse(raw); } catch { throw new Error(type); }
2411}
2412
2413function validateEndpointSection(schema, section, value) {
2414 const validator = endpointValidator(`validator:endpoint.${schema.name}.${section}`);
2415 if (validator === null) return Object.freeze(value);
2416 try { return validator(value, true); }
2417 catch (cause) {
2418 throw Object.assign(new Error(`Endpoint ${section} violates its declared schema`), {
2419 code: "ENDPOINT_INPUT_TYPE",
2420 details: typeof cause?.toJSON === "function" ? cause.toJSON() : null,
2421 });
2422 }
2423}
2424
2425function decodeEndpointParams(schema, raw) {
2426 const values = Object.create(null);
2427 for (const field of schema.params) {
2428 if (!Object.hasOwn(raw, field.name)) throw Object.assign(new Error(`Missing path parameter ${field.name}`), { code: "ENDPOINT_PARAM_MISSING" });
2429 try { values[field.name] = decodeEndpointScalar(raw[field.name], field.type); }
2430 catch { throw Object.assign(new Error(`Path parameter ${field.name} must be ${field.type}`), { code: "ENDPOINT_PARAM_TYPE", details: { field: field.name, expected: field.type } }); }
2431 }
2432 return validateEndpointSection(schema, "params", values);
2433}
2434
2435function decodeEndpointQueryPart(raw) {
2436 return decodeURIComponent(raw.replace(/\+/g, " "));
2437}
2438
2439function endpointQueryTransport(url) {
2440 const grouped = Object.create(null);
2441 let malformed = false;
2442 const search = url.search.startsWith("?") ? url.search.slice(1) : url.search;
2443 if (search.length > 0) {
2444 for (const pair of search.split("&")) {
2445 if (pair.length === 0) continue;
2446 const separator = pair.indexOf("=");
2447 const rawName = separator < 0 ? pair : pair.slice(0, separator);
2448 const rawValue = separator < 0 ? "" : pair.slice(separator + 1);
2449 let name, value;
2450 try { name = decodeEndpointQueryPart(rawName); }
2451 catch { name = rawName; malformed = true; }
2452 try { value = decodeEndpointQueryPart(rawValue); }
2453 catch { value = rawValue; malformed = true; }
2454 const previous = grouped[name];
2455 if (previous === undefined) grouped[name] = value;
2456 else if (Array.isArray(previous)) grouped[name] = Object.freeze([...previous, value]);
2457 else grouped[name] = Object.freeze([previous, value]);
2458 }
2459 }
2460 return Object.freeze({ malformed, values: Object.freeze(grouped) });
2461}
2462
2463function decodeEndpointQuery(schema, transport) {
2464 if (transport.malformed) throw Object.assign(new Error("Endpoint query contains invalid percent encoding or UTF-8"), { status: 400, code: "ENDPOINT_QUERY_ENCODING_INVALID" });
2465 const expected = new Set(schema.query.map((field) => field.name));
2466 for (const name of Object.keys(transport.values)) {
2467 if (!expected.has(name)) throw Object.assign(new Error(`Unknown query field ${name}`), { code: "ENDPOINT_QUERY_UNKNOWN", details: { field: name } });
2468 }
2469 const values = Object.create(null);
2470 for (const field of schema.query) {
2471 const candidate = transport.values[field.name];
2472 const raw = candidate === undefined ? [] : Array.isArray(candidate) ? candidate : [candidate];
2473 const optional = optionalWireType(field.type) !== null;
2474 const inner = arrayWireType(field.type);
2475 if (raw.length === 0) {
2476 if (optional) { values[field.name] = null; continue; }
2477 if (inner !== null) { values[field.name] = Object.freeze([]); continue; }
2478 throw Object.assign(new Error(`Missing query field ${field.name}`), { code: "ENDPOINT_QUERY_MISSING", details: { field: field.name } });
2479 }
2480 try {
2481 if (inner !== null) {
2482 if (raw.length !== 1) throw new Error("repeated-array");
2483 const decoded = JSON.parse(raw[0]);
2484 if (!Array.isArray(decoded)) throw new Error("array");
2485 values[field.name] = decoded;
2486 } else {
2487 if (raw.length !== 1) throw new Error("repeated");
2488 values[field.name] = decodeEndpointScalar(raw[0], field.type);
2489 }
2490 } catch {
2491 throw Object.assign(new Error(`Query field ${field.name} must be ${field.type}`), { code: "ENDPOINT_QUERY_TYPE", details: { field: field.name, expected: field.type } });
2492 }
2493 }
2494 return validateEndpointSection(schema, "query", values);
2495}
2496
2497function endpointTimeoutFailure(schema) {
2498 return failure(504, "ENDPOINT_TIMEOUT", "Endpoint exceeded its declared timeout", schema.id, { timeoutMs: schema.timeoutMs });
2499}
2500
2501function endpointTimeoutError(schema) {
2502 return Object.assign(new Error("Endpoint exceeded its declared timeout"), { status: 504, code: "ENDPOINT_TIMEOUT", details: { timeoutMs: schema.timeoutMs } });
2503}
2504
2505async function readEndpointBodyBytes(request, schema, signal) {
2506 if (request.body === null) return new Uint8Array();
2507 const reader = request.body.getReader();
2508 const chunks = [];
2509 let total = 0;
2510 const cancel = () => { void reader.cancel("endpoint timeout").catch(() => {}); };
2511 signal.addEventListener("abort", cancel, { once: true });
2512 try {
2513 while (true) {
2514 if (signal.aborted) throw endpointTimeoutError(schema);
2515 const { done, value } = await reader.read();
2516 if (signal.aborted) throw endpointTimeoutError(schema);
2517 if (done) break;
2518 if (!(value instanceof Uint8Array)) throw Object.assign(new Error("Endpoint request body stream did not yield bytes"), { status: 400, code: "ENDPOINT_BODY_INVALID" });
2519 total += value.byteLength;
2520 if (total > 1_048_576) {
2521 await reader.cancel("endpoint body too large").catch(() => {});
2522 throw Object.assign(new Error("Endpoint request body exceeds 1 MiB"), { status: 413, code: "ENDPOINT_BODY_TOO_LARGE" });
2523 }
2524 chunks.push(value);
2525 }
2526 } finally {
2527 signal.removeEventListener("abort", cancel);
2528 try { reader.releaseLock(); } catch {}
2529 }
2530 const bytes = new Uint8Array(total);
2531 let offset = 0;
2532 for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
2533 return bytes;
2534}
2535
2536const endpointUploadCleanups = new WeakMap();
2537
2538function multipartFailure(status, code, message, details = null) {
2539 return Object.assign(new Error(message), { status, code, details });
2540}
2541
2542function multipartConcat(left, right) {
2543 if (left.byteLength === 0) return right.slice();
2544 if (right.byteLength === 0) return left;
2545 const joined = new Uint8Array(left.byteLength + right.byteLength);
2546 joined.set(left);
2547 joined.set(right, left.byteLength);
2548 return joined;
2549}
2550
2551function multipartIndexOf(haystack, needle, start = 0) {
2552 outer: for (let index = start; index + needle.byteLength <= haystack.byteLength; index += 1) {
2553 for (let offset = 0; offset < needle.byteLength; offset += 1) if (haystack[index + offset] !== needle[offset]) continue outer;
2554 return index;
2555 }
2556 return -1;
2557}
2558
2559function multipartParameters(value) {
2560 if (typeof value !== "string" || /[\r\n]/.test(value)) throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart header parameters are malformed");
2561 const entries = [];
2562 let current = "";
2563 let quoted = false;
2564 let escaped = false;
2565 for (const character of value) {
2566 if (escaped) { current += character; escaped = false; continue; }
2567 if (quoted && character === "\\") { current += character; escaped = true; continue; }
2568 if (character === '"') { quoted = !quoted; current += character; continue; }
2569 if (character === ";" && !quoted) { entries.push(current.trim()); current = ""; continue; }
2570 current += character;
2571 }
2572 if (quoted || escaped) throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart header quoting is incomplete");
2573 entries.push(current.trim());
2574 const kind = entries.shift()?.toLowerCase() ?? "";
2575 const parameters = Object.create(null);
2576 for (const entry of entries) {
2577 const separator = entry.indexOf("=");
2578 if (separator <= 0) throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart header parameter requires a name and value");
2579 const name = entry.slice(0, separator).trim().toLowerCase();
2580 let parameter = entry.slice(separator + 1).trim();
2581 if (parameter.startsWith('"')) {
2582 if (!parameter.endsWith('"')) throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart quoted parameter is incomplete");
2583 parameter = parameter.slice(1, -1).replace(/\\(.)/g, "$1");
2584 }
2585 if (name.length === 0 || Object.hasOwn(parameters, name)) throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart header parameter is duplicated or unnamed");
2586 parameters[name] = parameter;
2587 }
2588 return { kind, parameters };
2589}
2590
2591function endpointMultipartBoundary(contentType) {
2592 const parsed = multipartParameters(contentType);
2593 const boundary = parsed.parameters.boundary;
2594 if (parsed.kind !== "multipart/form-data" || typeof boundary !== "string" || !/^[0-9A-Za-z'()+_,.\/:=?-]{1,70}$/.test(boundary)) {
2595 throw multipartFailure(415, "ENDPOINT_MULTIPART_BOUNDARY", "multipart/form-data requires one valid boundary parameter of at most 70 ASCII characters");
2596 }
2597 return boundary;
2598}
2599
2600function endpointMultipartHeaders(bytes) {
2601 let text;
2602 try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
2603 catch { throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart part headers are not valid UTF-8"); }
2604 const headers = Object.create(null);
2605 for (const line of text.split("\r\n")) {
2606 const separator = line.indexOf(":");
2607 if (separator <= 0) throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart part header requires `name: value`");
2608 const name = line.slice(0, separator).trim().toLowerCase();
2609 const value = line.slice(separator + 1).trim();
2610 if (!/^[a-z0-9-]+$/.test(name) || Object.hasOwn(headers, name)) throw multipartFailure(400, "MULTIPART_HEADER_INVALID", "Multipart part header is duplicated or malformed");
2611 headers[name] = value;
2612 }
2613 return headers;
2614}
2615
2616// `filenameMaxBytes` is published in bytes, so it is enforced in bytes. The
2617// cut is moved back to the nearest code-point boundary — UTF-8 continuation
2618// bytes are 10xxxxxx — so a truncated name is never half a sequence and never
2619// half a surrogate pair: an astral character is kept whole or dropped whole.
2620// The limit is a ceiling on metadata, never a refusal.
2621function truncateUploadNameBytes(value) {
2622 const bytes = new TextEncoder().encode(value);
2623 if (bytes.byteLength <= ENDPOINT_UPLOAD_NAME_MAX_BYTES) return value;
2624 let end = ENDPOINT_UPLOAD_NAME_MAX_BYTES;
2625 while (end > 0 && (bytes[end] & 0xc0) === 0x80) end -= 1;
2626 return new TextDecoder("utf-8").decode(bytes.subarray(0, end));
2627}
2628
2629function sanitizeUploadName(value) {
2630 const leaf = String(value).split(/[\\/]/).pop() ?? "";
2631 // Normalize before measuring. NFKC folds compatibility spellings to one
2632 // canonically composed form, so the bytes counted against the ceiling are
2633 // the bytes the name is finally reported as.
2634 const sanitized = truncateUploadNameBytes(leaf.normalize("NFKC").replace(/[\u0000-\u001f\u007f]/g, "_").replace(/^\.+/, ""));
2635 return sanitized.length === 0 ? "upload" : sanitized;
2636}
2637
2638function decodeMultipartScalar(bytes, field) {
2639 let text;
2640 try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
2641 catch { throw multipartFailure(400, "ENDPOINT_BODY_INVALID", `Multipart body field ${field.name} is not valid UTF-8`, { field: field.name }); }
2642 let wireType = field.type;
2643 while (optionalWireType(wireType) !== null) wireType = optionalWireType(wireType);
2644 if (wireType === "String") return text;
2645 if (["Int", "Number", "Float", "Boolean", "Date"].includes(wireType)) {
2646 try { return decodeEndpointScalar(text, wireType); }
2647 catch { throw multipartFailure(422, "ENDPOINT_BODY_TYPE", `Body field ${field.name} must be ${field.type}`, { field: field.name, expected: field.type }); }
2648 }
2649 try { return JSON.parse(text); }
2650 catch { throw multipartFailure(422, "ENDPOINT_BODY_TYPE", `Body field ${field.name} must contain JSON matching ${field.type}`, { field: field.name, expected: field.type }); }
2651}
2652
2653async function decodeEndpointMultipartBody(request, schema, signal, contentType) {
2654 const boundary = endpointMultipartBoundary(contentType);
2655 if (request.body === null) throw multipartFailure(400, "ENDPOINT_BODY_MISSING", "Multipart endpoint request body is missing");
2656 const encoder = new TextEncoder();
2657 const firstBoundary = encoder.encode(`--${boundary}`);
2658 const bodyBoundary = encoder.encode(`\r\n--${boundary}`);
2659 const headerTerminator = Uint8Array.from([13, 10, 13, 10]);
2660 const fields = new Map(schema.body.map((field) => [field.name, field]));
2661 const values = Object.create(null);
2662 const seen = new Set();
2663 const activeSinks = new Set();
2664 const completedAccess = [];
2665 let pending = new Uint8Array();
2666 let phase = "start";
2667 let part = null;
2668 let parts = 0;
2669 let scalarBytes = 0;
2670
2671 const beginPart = async (headerBytes) => {
2672 // One completed header block is one part, counted before the block is
2673 // parsed, so the count is a property of the body rather than of how the
2674 // body was delivered.
2675 parts += 1;
2676 if (parts > ENDPOINT_MULTIPART_MAX_PARTS) throw multipartFailure(413, "MULTIPART_PART_LIMIT", `Multipart request exceeds ${ENDPOINT_MULTIPART_MAX_PARTS} parts`);
2677 const headers = endpointMultipartHeaders(headerBytes);
2678 const disposition = multipartParameters(headers["content-disposition"]);
2679 const name = disposition.parameters.name;
2680 if (disposition.kind !== "form-data" || typeof name !== "string" || name.length === 0) throw multipartFailure(400, "MULTIPART_DISPOSITION_INVALID", "Multipart part requires Content-Disposition: form-data with a name");
2681 const field = fields.get(name);
2682 if (!field) throw multipartFailure(400, "ENDPOINT_BODY_UNKNOWN", `Unknown body field ${name}`, { field: name });
2683 const filename = disposition.parameters.filename;
2684 if (field.upload !== null) {
2685 if (typeof filename !== "string") throw multipartFailure(400, "MULTIPART_FILE_REQUIRED", `Body field ${name} requires a file part`, { field: name });
2686 if (!field.upload.multiple && seen.has(name)) throw multipartFailure(400, "ENDPOINT_BODY_DUPLICATE", `Body field ${name} may appear only once`, { field: name });
2687 const sink = await __noxidCreateUploadSink(field.upload.maxSizeBytes);
2688 activeSinks.add(sink);
2689 part = {
2690 kind: "file",
2691 field,
2692 name: sanitizeUploadName(filename),
2693 sink,
2694 validation: __noxidCreateFileValidationState(field.upload.maxSizeBytes, field.upload.types, schema.id, [name]),
2695 };
2696 } else {
2697 if (filename !== undefined) throw multipartFailure(400, "MULTIPART_SCALAR_REQUIRED", `Body field ${name} is scalar and cannot receive a file part`, { field: name });
2698 if (seen.has(name)) throw multipartFailure(400, "ENDPOINT_BODY_DUPLICATE", `Body field ${name} may appear only once`, { field: name });
2699 part = { kind: "scalar", field, chunks: [], size: 0 };
2700 }
2701 };
2702
2703 const writePart = async (chunk) => {
2704 if (chunk.byteLength === 0) return;
2705 if (part.kind === "file") {
2706 __noxidValidateFileChunk(part.validation, chunk);
2707 await part.sink.write(chunk);
2708 return;
2709 }
2710 part.size += chunk.byteLength;
2711 // A running total over the decoded span, so the verdict is the same
2712 // whether the field arrived in one read or a thousand.
2713 scalarBytes += chunk.byteLength;
2714 if (scalarBytes > ENDPOINT_MULTIPART_SCALAR_MAX_BYTES) throw multipartFailure(413, "ENDPOINT_BODY_TOO_LARGE", `Multipart scalar fields exceed ${ENDPOINT_MULTIPART_SCALAR_MAX_BYTES} bytes`);
2715 part.chunks.push(chunk.slice());
2716 };
2717
2718 const finishPart = async () => {
2719 const name = part.field.name;
2720 if (part.kind === "file") {
2721 const verdict = __noxidFinalizeFileValidation(part.validation);
2722 const staged = await part.sink.finish();
2723 activeSinks.delete(part.sink);
2724 completedAccess.push(staged.access);
2725 const file = __noxidCreateFileRef(
2726 { sniffedType: verdict.sniffedType, size: verdict.size, sha256: staged.sha256, name: part.name, maxSizeBytes: part.field.upload.maxSizeBytes },
2727 { stream: () => staged.access.stream(), bytes: () => staged.access.bytes(), store: (namespace) => staged.access.store(namespace) },
2728 );
2729 if (part.field.upload.multiple) {
2730 if (!Object.hasOwn(values, name)) values[name] = [];
2731 values[name].push(file);
2732 } else {
2733 values[name] = file;
2734 }
2735 } else {
2736 const bytes = new Uint8Array(part.size);
2737 let offset = 0;
2738 for (const chunk of part.chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
2739 values[name] = decodeMultipartScalar(bytes, part.field);
2740 }
2741 seen.add(name);
2742 part = null;
2743 };
2744
2745 const validBoundaryAt = (index) => {
2746 const suffix = index + bodyBoundary.byteLength;
2747 if (pending.byteLength < suffix + 2) return false;
2748 return (pending[suffix] === 13 && pending[suffix + 1] === 10) || (pending[suffix] === 45 && pending[suffix + 1] === 45);
2749 };
2750
2751 const processPending = async (eof) => {
2752 while (true) {
2753 if (phase === "done") {
2754 if (pending.byteLength === 0 || (pending.byteLength === 2 && pending[0] === 13 && pending[1] === 10)) { pending = new Uint8Array(); return; }
2755 throw multipartFailure(400, "MULTIPART_EPILOGUE_INVALID", "Multipart body contains bytes after the closing boundary");
2756 }
2757 if (phase === "start") {
2758 if (pending.byteLength < firstBoundary.byteLength + 2) { if (eof) throw multipartFailure(400, "MULTIPART_TRUNCATED", "Multipart body ended before its first boundary"); return; }
2759 // RFC 2046 permits a preamble before the first boundary. It belongs to
2760 // no part, carries no field, and is discarded — but it is still bytes
2761 // a client can send, so it is bounded exactly like a part header block
2762 // and refused past that with its own code rather than being read
2763 // forever.
2764 if (multipartIndexOf(pending, firstBoundary) !== 0) {
2765 const delimiter = multipartIndexOf(pending, bodyBoundary);
2766 // Measure the span, never the buffer. Once the delimiter is found
2767 // the preamble is exactly `delimiter` bytes; while it is absent
2768 // every buffered byte is preamble except a possible partial
2769 // delimiter at the tail, so the decided-so-far length is
2770 // `byteLength - (needle - 1)`. Counting the whole buffer instead
2771 // would refuse a preamble at the ceiling whenever a read happened
2772 // to stop inside the delimiter.
2773 const preambleBytes = delimiter < 0 ? pending.byteLength - (bodyBoundary.byteLength - 1) : delimiter;
2774 if (preambleBytes > ENDPOINT_MULTIPART_HEADER_MAX_BYTES) throw multipartFailure(413, "MULTIPART_PREAMBLE_TOO_LARGE", `Multipart preamble exceeds ${ENDPOINT_MULTIPART_HEADER_MAX_BYTES} bytes`);
2775 if (delimiter < 0) {
2776 if (eof) throw multipartFailure(400, "MULTIPART_BOUNDARY_INVALID", "Multipart body contains no declared boundary");
2777 return;
2778 }
2779 pending = pending.slice(delimiter + 2);
2780 continue;
2781 }
2782 if (pending[firstBoundary.byteLength] !== 13 || pending[firstBoundary.byteLength + 1] !== 10) throw multipartFailure(400, "MULTIPART_BOUNDARY_INVALID", "Multipart body does not start with its declared boundary");
2783 pending = pending.slice(firstBoundary.byteLength + 2);
2784 phase = "headers";
2785 continue;
2786 }
2787 if (phase === "headers") {
2788 const end = multipartIndexOf(pending, headerTerminator);
2789 // The ceiling belongs to the header block, not to whatever the
2790 // transport handed us. A found terminator makes the block exactly
2791 // `end` bytes, and it is measured before `beginPart` sees it: an
2792 // oversized block that arrives with its terminator in one read is
2793 // the same block as one that arrives in pieces.
2794 const headerBytes = end < 0 ? pending.byteLength - (headerTerminator.byteLength - 1) : end;
2795 if (headerBytes > ENDPOINT_MULTIPART_HEADER_MAX_BYTES) throw multipartFailure(413, "MULTIPART_HEADERS_TOO_LARGE", `Multipart part headers exceed ${ENDPOINT_MULTIPART_HEADER_MAX_BYTES} bytes`);
2796 if (end < 0) {
2797 if (eof) throw multipartFailure(400, "MULTIPART_TRUNCATED", "Multipart body ended inside part headers");
2798 return;
2799 }
2800 await beginPart(pending.subarray(0, end));
2801 pending = pending.slice(end + headerTerminator.byteLength);
2802 phase = "body";
2803 continue;
2804 }
2805 let boundaryIndex = multipartIndexOf(pending, bodyBoundary);
2806 while (boundaryIndex >= 0 && !validBoundaryAt(boundaryIndex)) boundaryIndex = multipartIndexOf(pending, bodyBoundary, boundaryIndex + 1);
2807 if (boundaryIndex < 0) {
2808 const retain = bodyBoundary.byteLength + 2;
2809 const flush = Math.max(0, pending.byteLength - retain);
2810 if (flush > 0) { await writePart(pending.subarray(0, flush)); pending = pending.slice(flush); }
2811 if (eof) throw multipartFailure(400, "MULTIPART_TRUNCATED", "Multipart body ended before a closing boundary");
2812 return;
2813 }
2814 await writePart(pending.subarray(0, boundaryIndex));
2815 await finishPart();
2816 const suffix = boundaryIndex + bodyBoundary.byteLength;
2817 const closing = pending[suffix] === 45;
2818 pending = pending.slice(suffix + 2);
2819 phase = closing ? "done" : "headers";
2820 }
2821 };
2822
2823 const reader = request.body.getReader();
2824 const cancel = () => { void reader.cancel("endpoint timeout").catch(() => {}); };
2825 signal.addEventListener("abort", cancel, { once: true });
2826 try {
2827 while (true) {
2828 if (signal.aborted) throw endpointTimeoutError(schema);
2829 const { done, value } = await reader.read();
2830 if (done) break;
2831 if (!(value instanceof Uint8Array)) throw multipartFailure(400, "ENDPOINT_BODY_INVALID", "Endpoint request body stream did not yield bytes");
2832 for (let offset = 0; offset < value.byteLength; offset += 65_536) {
2833 pending = multipartConcat(pending, value.subarray(offset, Math.min(value.byteLength, offset + 65_536)));
2834 await processPending(false);
2835 }
2836 }
2837 await processPending(true);
2838 for (const field of schema.body) {
2839 if (!Object.hasOwn(values, field.name) && optionalWireType(field.type) === null) throw multipartFailure(400, "ENDPOINT_BODY_MISSING", `Missing body field ${field.name}`, { field: field.name });
2840 if (field.upload?.multiple) values[field.name] = Object.freeze(values[field.name]);
2841 }
2842 endpointUploadCleanups.set(request, async () => {
2843 for (const access of completedAccess) await access.dispose();
2844 });
2845 return validateEndpointSection(schema, "body", values);
2846 } catch (cause) {
2847 await reader.cancel(cause?.code ?? "multipart refused").catch(() => {});
2848 for (const sink of activeSinks) await sink.abort().catch(() => {});
2849 for (const access of completedAccess) await access.dispose().catch(() => {});
2850 throw cause;
2851 } finally {
2852 signal.removeEventListener("abort", cancel);
2853 try { reader.releaseLock(); } catch {}
2854 }
2855}
2856
2857async function releaseEndpointUploads(request) {
2858 const cleanup = endpointUploadCleanups.get(request);
2859 endpointUploadCleanups.delete(request);
2860 if (cleanup) await cleanup();
2861}
2862
2863async function decodeEndpointBody(request, schema, signal) {
2864 if (schema.body.length === 0 && (schema.method === "GET" || request.body === null)) return Object.freeze(Object.create(null));
2865 const contentTypeHeader = request.headers.get("content-type") ?? "";
2866 const contentType = contentTypeHeader.split(";", 1)[0].trim().toLowerCase();
2867 const uploadFields = schema.body.filter((field) => field.upload !== null);
2868 if (contentType === "multipart/form-data") {
2869 if (uploadFields.length === 0) throw Object.assign(new Error("multipart/form-data is accepted only by endpoints declaring a File body field"), { status: 415, code: "ENDPOINT_MULTIPART_UNDECLARED" });
2870 return decodeEndpointMultipartBody(request, schema, signal, contentTypeHeader);
2871 }
2872 if (uploadFields.length !== 0) throw Object.assign(new Error("Endpoint upload body requires multipart/form-data"), { status: 415, code: "ENDPOINT_CONTENT_TYPE" });
2873 if (contentType !== "application/json") throw Object.assign(new Error("Endpoint request body requires application/json"), { status: 415, code: "ENDPOINT_CONTENT_TYPE" });
2874 const declaredLength = Number(request.headers.get("content-length") ?? 0);
2875 if (Number.isFinite(declaredLength) && declaredLength > 1_048_576) throw Object.assign(new Error("Endpoint request body exceeds 1 MiB"), { status: 413, code: "ENDPOINT_BODY_TOO_LARGE" });
2876 const bytes = await readEndpointBodyBytes(request, schema, signal);
2877 let text;
2878 try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
2879 catch { throw Object.assign(new Error("Endpoint request body is not valid UTF-8"), { status: 400, code: "ENDPOINT_BODY_INVALID" }); }
2880 let body;
2881 try { body = JSON.parse(text); } catch { throw Object.assign(new Error("Endpoint request body is not valid JSON"), { status: 400, code: "ENDPOINT_BODY_INVALID" }); }
2882 if (!body || typeof body !== "object" || Array.isArray(body)) throw Object.assign(new Error("Endpoint request body must be a JSON object"), { status: 400, code: "ENDPOINT_BODY_INVALID" });
2883 const fields = new Map(schema.body.map((field) => [field.name, field]));
2884 for (const name of Object.keys(body)) if (!fields.has(name)) throw Object.assign(new Error(`Unknown body field ${name}`), { status: 400, code: "ENDPOINT_BODY_UNKNOWN", details: { field: name } });
2885 for (const field of schema.body) if (!Object.hasOwn(body, field.name) && optionalWireType(field.type) === null) throw Object.assign(new Error(`Missing body field ${field.name}`), { status: 400, code: "ENDPOINT_BODY_MISSING", details: { field: field.name } });
2886 return validateEndpointSection(schema, "body", body);
2887}
2888
2889function endpointResponseWithHeaders(response, pairs) {
2890 if (!pairs || pairs.length === 0) return response;
2891 const headers = new Headers(response.headers);
2892 for (const [name, value] of pairs) headers.append(name, value);
2893 return __noxidTraceCopyFailure(response, new Response(response.body, { status: response.status, headers }));
2894}
2895
2896function endpointCacheResponse(response, schema) {
2897 if (schema.cache === null || response.status !== 200) return response;
2898 const headers = new Headers(response.headers);
2899 const seconds = String(schema.cache.seconds);
2900 headers.set("cache-control", schema.cache.mode === "swr"
2901 ? `public, s-maxage=${seconds}, stale-while-revalidate=${seconds}`
2902 : `public, s-maxage=${seconds}, must-revalidate`);
2903 headers.set("x-noxid-cache-mode", schema.cache.mode);
2904 headers.set("x-noxid-cache-revalidate", seconds);
2905 headers.set("x-noxid-cache-stale", schema.cache.mode === "swr" ? seconds : "0");
2906 headers.set("x-noxid-cache-tags", schema.cache.tags.join(","));
2907 return new Response(response.body, { status: response.status, headers });
2908}
2909
2910function endpointRedirect(value, external) {
2911 if (external === true) {
2912 if (typeof value !== "string" || !/^https:\/\/[^\s]+$/.test(value) || /[\r\n]/.test(value)) throw new Error("external");
2913 return value;
2914 }
2915 if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || /[\r\n]/.test(value)) throw new Error("relative");
2916 if (applicationBasePath === "/" || value === applicationBasePath || value.startsWith(`${applicationBasePath}/`)) return value;
2917 return `${applicationBasePath.replace(/\/$/, "")}${value}`;
2918}
2919
2920async function applyEndpointMiddleware(request, schema, params, query, environment, executionContext, signal) {
2921 const headers = [];
2922 const context = Object.create(null);
2923 const chain = [
2924 ...globalMiddleware.map((name) => Object.freeze({ name, handle: globalMiddlewareHandlers[name] })),
2925 ...schema.middleware.map((name) => Object.freeze({ name, handle: middlewareHandlers[name] })),
2926 ];
2927 const route = Object.freeze({ id: schema.id, pattern: schema.path, method: schema.method, middleware: Object.freeze(chain.map((entry) => entry.name)) });
2928 __noxidTraceRoute(request, route.pattern);
2929 for (const { name, handle } of chain) {
2930 if (signal.aborted) return { response: endpointTimeoutFailure(schema), headers };
2931 if (typeof handle !== "function") return { response: failure(500, "ENDPOINT_MIDDLEWARE_MISSING", "Required endpoint middleware is not available", schema.id, { middleware: name }), headers };
2932 let result;
2933 try {
2934 result = await handle(__noxidDataContext({ middleware: name, semanticId: schema.id, traceId: __noxidTraceIdForRequest(request), boundary: `boundary:endpoint.${schema.name}.request`, target: "endpoint", route, request, url: new URL(request.url), params, query, host: hostModule, environment, executionContext, signal, context: Object.freeze({ ...context }), middlewareContext: Object.freeze({ ...context }) }, __noxidPrincipal(context, environment, __noxidAgentForRequest(request))));
2935 } catch {
2936 __noxidTraceBindPrincipal({ request }, __noxidPrincipal(context, environment, __noxidAgentForRequest(request)));
2937 __noxidTraceSemantic(request, "middleware", name.startsWith("middleware:") ? name : `middleware:${name}`);
2938 return { response: signal.aborted ? endpointTimeoutFailure(schema) : failure(500, "ENDPOINT_MIDDLEWARE_FAILED", "Endpoint middleware failed", schema.id, { middleware: name }), headers };
2939 }
2940 if (signal.aborted) return { response: endpointTimeoutFailure(schema), headers };
2941 const normalized = __noxidNormalizeMiddlewareResult(result);
2942 if (normalized.issue !== null) {
2943 __noxidTraceBindPrincipal({ request }, __noxidPrincipal(context, environment, __noxidAgentForRequest(request)));
2944 __noxidTraceSemantic(request, "middleware", name.startsWith("middleware:") ? name : `middleware:${name}`);
2945 return { response: failure(500, `ENDPOINT_MIDDLEWARE_${normalized.issue.toUpperCase()}`, "Endpoint middleware returned an invalid boundary result", schema.id, { middleware: name, validation: normalized.detail }), headers };
2946 }
2947 headers.push(...normalized.headers);
2948 if (normalized.context !== null) Object.assign(context, normalized.context);
2949 __noxidTraceBindPrincipal({ request }, __noxidPrincipal(context, environment, __noxidAgentForRequest(request)));
2950 __noxidTraceSemantic(request, "middleware", name.startsWith("middleware:") ? name : `middleware:${name}`);
2951 if (normalized.respond !== null) {
2952 const directHeaders = new Headers({ "content-type": `${normalized.respond.contentType}; charset=utf-8`, "cache-control": "no-store", "x-content-type-options": "nosniff" });
2953 for (const [header, value] of headers) directHeaders.append(header, value);
2954 return { response: new Response(normalized.respond.body, { status: normalized.respond.status, headers: directHeaders }), headers: [] };
2955 }
2956 if (normalized.redirect !== null) {
2957 let location;
2958 try { location = endpointRedirect(normalized.redirect, normalized.external); }
2959 catch { return { response: failure(500, "ENDPOINT_MIDDLEWARE_REDIRECT_INVALID", "Endpoint middleware returned an unsafe redirect", schema.id, { middleware: name }), headers }; }
2960 const redirect = new Response(null, { status: 307, headers: { location } });
2961 return { response: endpointResponseWithHeaders(redirect, headers), headers: [] };
2962 }
2963 if (!normalized.allow) return { response: failure(403, "ENDPOINT_MIDDLEWARE_DENIED", "Endpoint middleware denied the request", schema.id, { middleware: name }), headers };
2964 }
2965 return { response: null, headers, context: Object.freeze({ ...context }), route };
2966}
2967
2968async function authorizeEndpoint(request, schema, route, environment, executionContext, signal) {
2969 if (schema.capabilities.length === 0) return null;
2970 if (typeof authorize !== "function") return failure(500, "ENDPOINT_AUTHORIZER_MISSING", "Endpoint authorization is not configured", schema.id);
2971 for (const capability of schema.capabilities) {
2972 if (signal.aborted) return endpointTimeoutFailure(schema);
2973 let allowed = false;
2974 try { allowed = await authorize(Object.freeze({ capability, semanticId: schema.id, traceId: __noxidTraceIdForRequest(request), target: "endpoint", route, request, environment, executionContext, signal })) === true; } catch {}
2975 if (signal.aborted) return endpointTimeoutFailure(schema);
2976 if (!allowed) return failure(403, "ENDPOINT_CAPABILITY_DENIED", "Endpoint capability was denied", schema.id, { capability });
2977 }
2978 return null;
2979}
2980
2981function endpointRateIdentity(schema, request, environment, middlewareContext) {
2982 if (schema.limit === null) return { identity: null };
2983 if (schema.limit.scope === "session") {
2984 const identity = middlewareContext?.sessionId ?? middlewareContext?.session?.id ?? environment?.sessionId;
2985 return typeof identity === "string" && identity.length > 0 ? { identity: `session:${identity}` } : { error: failure(403, "ENDPOINT_RATE_IDENTITY_REQUIRED", "Session-scoped endpoint limit requires an authenticated session identity", schema.id) };
2986 }
2987 const forwarded = request.headers.get("cf-connecting-ip") ?? request.headers.get("x-real-ip");
2988 const identity = environment?.requestIdentity?.ip ?? environment?.ip ?? forwarded;
2989 return typeof identity === "string" && identity.length > 0 ? { identity: `ip:${identity}` } : { error: failure(403, "ENDPOINT_RATE_IDENTITY_REQUIRED", "IP-scoped endpoint limit requires a trusted client identity", schema.id) };
2990}
2991
2992async function withEndpointRateLock(key, operation) {
2993 const previous = endpointRateLocks.get(key) ?? Promise.resolve();
2994 let release;
2995 const current = new Promise((resolve) => { release = resolve; });
2996 endpointRateLocks.set(key, current);
2997 await previous;
2998 try { return await operation(); }
2999 finally {
3000 release();
3001 if (endpointRateLocks.get(key) === current) endpointRateLocks.delete(key);
3002 }
3003}
3004
3005function validEndpointRateBucket(bucket) {
3006 return bucket !== null && typeof bucket === "object"
3007 && typeof bucket.started === "number" && Number.isFinite(bucket.started) && bucket.started >= 0 && bucket.started <= Date.now()
3008 && typeof bucket.count === "number" && Number.isSafeInteger(bucket.count) && bucket.count >= 0
3009 && (bucket.windowMs === 60_000 || bucket.windowMs === 3_600_000);
3010}
3011
3012async function reserveEndpointRateCapacity(currentKey) {
3013 const keys = await endpointRateStorage.list();
3014 if (keys.includes(currentKey) || keys.length < ENDPOINT_RATE_BUCKET_MAX_ENTRIES) return;
3015 const loaded = await Promise.all(keys.map(async (key) => ({ key, bucket: await endpointRateStorage.get(key) })));
3016 for (const entry of loaded) if (!validEndpointRateBucket(entry.bucket)) await endpointRateStorage.delete(entry.key);
3017 const entries = loaded
3018 .filter((entry) => validEndpointRateBucket(entry.bucket))
3019 .sort((left, right) => left.bucket.started - right.bucket.started || left.key.localeCompare(right.key));
3020 while (entries.length >= ENDPOINT_RATE_BUCKET_MAX_ENTRIES) {
3021 const oldest = entries.shift();
3022 if (oldest) await endpointRateStorage.delete(oldest.key);
3023 }
3024}
3025
3026async function enforceEndpointRateLimit(schema, identity) {
3027 if (schema.limit === null) return null;
3028 const windowMs = schema.limit.window === "minute" ? 60_000 : 3_600_000;
3029 const key = `${schema.id}\n${identity}`;
3030 if (typeof __noxidSharedRateLimit === "function") {
3031 const retryAfter = await __noxidSharedRateLimit(key, schema.limit.requests, windowMs);
3032 return retryAfter === null
3033 ? null
3034 : failure(429, "ENDPOINT_RATE_LIMITED", "Endpoint rate limit exceeded", schema.id, { limit: schema.limit.requests, window: schema.limit.window }, { "retry-after": String(retryAfter) });
3035 }
3036 return withEndpointRateLock(key, async () => {
3037 const now = Date.now();
3038 let bucket = await endpointRateStorage.get(key);
3039 if (!validEndpointRateBucket(bucket) || bucket.count > schema.limit.requests || now - bucket.started >= windowMs || bucket.windowMs !== windowMs) {
3040 if (bucket !== null) await endpointRateStorage.delete(key);
3041 await reserveEndpointRateCapacity(key);
3042 bucket = { started: now, count: 0, windowMs };
3043 }
3044 if (bucket.count >= schema.limit.requests) {
3045 const retryAfter = Math.max(1, Math.ceil((bucket.started + windowMs - now) / 1000));
3046 return failure(429, "ENDPOINT_RATE_LIMITED", "Endpoint rate limit exceeded", schema.id, { limit: schema.limit.requests, window: schema.limit.window }, { "retry-after": String(retryAfter) });
3047 }
3048 const updated = { started: bucket.started, count: bucket.count + 1, windowMs };
3049 await endpointRateStorage.set(key, updated, { ttl: Math.max(0, (bucket.started + windowMs - now) / 1000) });
3050 return null;
3051 });
3052}
3053
3054async function endpointResponseSnapshot(response) {
3055 const failure = __noxidFailureSpans.get(response);
3056 const failureSnapshot = failure === undefined
3057 ? null
3058 : Object.freeze({ code: failure.code, semanticId: failure.semanticId });
3059 const copy = response.clone();
3060 let headers = [...copy.headers];
3061 if (typeof copy.headers.getSetCookie === "function") {
3062 headers = headers.filter(([name]) => name.toLowerCase() !== "set-cookie");
3063 headers.push(...copy.headers.getSetCookie().map((value) => ["set-cookie", value]));
3064 }
3065 const bytes = new Uint8Array(await copy.arrayBuffer());
3066 let binary = "";
3067 for (let offset = 0; offset < bytes.length; offset += 0x8000) binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
3068 return Object.freeze({ created: Date.now(), status: copy.status, headers: Object.freeze(headers.map((entry) => Object.freeze(entry))), body: btoa(binary), failure: failureSnapshot });
3069}
3070
3071function replayEndpointResponse(snapshot) {
3072 const binary = atob(snapshot.body);
3073 const body = new Uint8Array(binary.length);
3074 for (let index = 0; index < binary.length; index += 1) body[index] = binary.charCodeAt(index);
3075 const response = new Response(body, { status: snapshot.status, headers: snapshot.headers });
3076 return snapshot.failure === null || snapshot.failure === undefined
3077 ? response
3078 : __noxidTraceFailure(response, snapshot.failure.code, snapshot.failure.semanticId);
3079}
3080
3081function validEndpointFailureSnapshot(failure) {
3082 if (failure === null) return true;
3083 return typeof failure === "object"
3084 && Object.keys(failure).length === 2
3085 && typeof failure.code === "string" && NOXID_DIAGNOSTIC_CODE.test(failure.code)
3086 && (failure.semanticId === null || (typeof failure.semanticId === "string" && NOXID_SEMANTIC_ID.test(failure.semanticId)));
3087}
3088
3089function validEndpointSnapshotBase(snapshot) {
3090 return snapshot !== null && typeof snapshot === "object"
3091 && typeof snapshot.created === "number" && Number.isFinite(snapshot.created) && snapshot.created >= 0 && snapshot.created <= Date.now() && Date.now() - snapshot.created < ENDPOINT_IDEMPOTENCY_TTL_MS
3092 && typeof snapshot.status === "number" && Number.isSafeInteger(snapshot.status) && snapshot.status >= 100 && snapshot.status <= 599
3093 && Array.isArray(snapshot.headers) && snapshot.headers.every((entry) => Array.isArray(entry) && entry.length === 2 && entry.every((value) => typeof value === "string"))
3094 && typeof snapshot.body === "string";
3095}
3096
3097function validEndpointSnapshot(snapshot) {
3098 return validEndpointSnapshotBase(snapshot)
3099 && Object.hasOwn(snapshot, "failure") && validEndpointFailureSnapshot(snapshot.failure);
3100}
3101
3102function endpointSnapshotMatchesSchema(snapshot, schema) {
3103 if (!validEndpointSnapshot(snapshot)) return false;
3104 let body;
3105 try {
3106 const binary = atob(snapshot.body);
3107 const bytes = Uint8Array.from(binary, (value) => value.charCodeAt(0));
3108 body = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
3109 } catch { return false; }
3110 const code = body?.error?.code;
3111 const semanticId = body?.error?.semanticId;
3112 if (snapshot.failure === null) {
3113 return !new Set(["ENDPOINT_IMPLEMENTATION_MISSING", "ENDPOINT_TIMEOUT", "ENDPOINT_RESULT_TYPE", "ENDPOINT_RESULT_VALIDATOR_MISSING"]).has(code);
3114 }
3115 const expected = snapshot.failure.code === "ENDPOINT_RESULT_TYPE" || snapshot.failure.code === "ENDPOINT_RESULT_VALIDATOR_MISSING"
3116 ? Object.freeze({ semanticId: schema.result.id, status: 500 })
3117 : snapshot.failure.code === "ENDPOINT_TIMEOUT"
3118 ? Object.freeze({ semanticId: schema.id, status: 504 })
3119 : snapshot.failure.code === "ENDPOINT_IMPLEMENTATION_MISSING"
3120 ? Object.freeze({ semanticId: schema.id, status: 501 })
3121 : null;
3122 return expected !== null
3123 && snapshot.status === expected.status
3124 && snapshot.failure.semanticId === expected.semanticId
3125 && code === snapshot.failure.code
3126 && semanticId === snapshot.failure.semanticId;
3127}
3128
3129async function reserveEndpointIdempotencySlot(currentKey) {
3130 const keys = await endpointIdempotencyStorage.list();
3131 if (keys.includes(currentKey) || keys.length + endpointIdempotencyInFlight.size < ENDPOINT_IDEMPOTENCY_MAX_ENTRIES) return;
3132 const loaded = await Promise.all(keys.map(async (key) => ({ key, snapshot: await endpointIdempotencyStorage.get(key) })));
3133 for (const entry of loaded) if (!validEndpointSnapshot(entry.snapshot)) await endpointIdempotencyStorage.delete(entry.key);
3134 const entries = loaded
3135 .filter((entry) => validEndpointSnapshot(entry.snapshot))
3136 .sort((left, right) => left.snapshot.created - right.snapshot.created || left.key.localeCompare(right.key));
3137 while (entries.length + endpointIdempotencyInFlight.size >= ENDPOINT_IDEMPOTENCY_MAX_ENTRIES) {
3138 const oldestStored = entries.shift();
3139 if (oldestStored) await endpointIdempotencyStorage.delete(oldestStored.key);
3140 else endpointIdempotencyInFlight.delete(endpointIdempotencyInFlight.keys().next().value);
3141 }
3142}
3143
3144async function withEndpointIdempotencyLock(key, operation) {
3145 const previous = endpointIdempotencyLocks.get(key) ?? Promise.resolve();
3146 let release;
3147 const current = new Promise((resolve) => { release = resolve; });
3148 endpointIdempotencyLocks.set(key, current);
3149 await previous;
3150 try { return await operation(); }
3151 finally {
3152 release();
3153 if (endpointIdempotencyLocks.get(key) === current) endpointIdempotencyLocks.delete(key);
3154 }
3155}
3156
3157function endpointIdempotencyIdentity(request, environment, middlewareContext) {
3158 const session = middlewareContext?.sessionId ?? middlewareContext?.session?.id ?? environment?.sessionId;
3159 if (typeof session === "string" && session.length > 0) return `session:${session}`;
3160 const ip = environment?.requestIdentity?.ip ?? environment?.ip ?? request.headers.get("cf-connecting-ip") ?? request.headers.get("x-real-ip");
3161 return typeof ip === "string" && ip.length > 0 ? `ip:${ip}` : null;
3162}
3163
3164function endpointStorageFailure(schema, cause) {
3165 if (cause?.code === "SERVER_STORAGE_REDIS_UNAVAILABLE") {
3166 return failure(503, "ENDPOINT_STORAGE_UNAVAILABLE", "Endpoint operational storage is temporarily unavailable; retry the request", schema.id, null, { "retry-after": "1" });
3167 }
3168 return failure(500, "ENDPOINT_STORAGE_FAILED", "Endpoint operational storage is unavailable", schema.id);
3169}
3170
3171function sharedEndpointIdempotencyAvailable() {
3172 return typeof __noxidSharedIdempotencyPrepare === "function"
3173 && typeof __noxidSharedIdempotencyComplete === "function"
3174 && typeof __noxidSharedIdempotencyRelease === "function";
3175}
3176
3177async function prepareSharedEndpointIdempotency(schema, key, signal, deadlineAt) {
3178 if (typeof globalThis.crypto?.randomUUID !== "function") throw new Error("secure idempotency claims require crypto.randomUUID");
3179 const claim = globalThis.crypto.randomUUID();
3180 for (;;) {
3181 if (signal.aborted) return { response: endpointTimeoutFailure(schema) };
3182 const leaseMs = Math.max(1, Math.ceil(deadlineAt - Date.now() + 1000));
3183 const prepared = await __noxidSharedIdempotencyPrepare(key, claim, leaseMs);
3184 if (prepared?.state === "owner") return { claim };
3185 if (prepared?.state === "stored") {
3186 const existing = prepared.value;
3187 if (endpointSnapshotMatchesSchema(existing, schema)) return { snapshot: existing };
3188 const untraceableSnapshot = validEndpointSnapshotBase(existing);
3189 await endpointIdempotencyStorage.delete(key);
3190 if (untraceableSnapshot) return { response: endpointStorageFailure(schema) };
3191 continue;
3192 }
3193 if (prepared?.state !== "pending") throw new Error("invalid shared idempotency preparation result");
3194 await new Promise((resolve) => setTimeout(resolve, Math.min(10, Math.max(1, deadlineAt - Date.now()))));
3195 }
3196}
3197
3198function endpointStreamHeaders() {
3199 return {
3200 "content-type": "text/event-stream; charset=utf-8",
3201 "cache-control": "no-store",
3202 "connection": "keep-alive",
3203 "x-accel-buffering": "no",
3204 "x-content-type-options": "nosniff",
3205 };
3206}
3207
3208function endpointImmediateStreamFailure(schema, code, message, details = null) {
3209 return new Response(streamErrorFrame(schema, code, message, details), { status: 200, headers: endpointStreamHeaders() });
3210}
3211
3212function endpointClosedStreamResponse() {
3213 return new Response(null, { status: 200, headers: endpointStreamHeaders() });
3214}
3215
3216function resumeEndpointStream(schema, resume, requestBinding) {
3217 if (resume.invalid) return endpointImmediateStreamFailure(schema, "STREAM_RESUME_UNAVAILABLE", "Last-Event-ID is malformed or exceeds the supported length", { reason: "malformed" });
3218 const history = endpointStreamHistories.get(resume.token);
3219 if (!history || history.schemaId !== schema.id) return endpointImmediateStreamFailure(schema, "STREAM_RESUME_UNAVAILABLE", "Last-Event-ID does not name retained history for this stream endpoint", { reason: "unknown" });
3220 if (history.requestBinding !== requestBinding) return endpointImmediateStreamFailure(schema, "STREAM_RESUME_UNAVAILABLE", "Last-Event-ID belongs to a different stream request contract or request identity", { reason: "request-mismatch" });
3221 if (!history.completed) return endpointImmediateStreamFailure(schema, "STREAM_RESUME_UNAVAILABLE", "The named stream history is still active; reconnect after the prior connection closes", { reason: "active" });
3222 const firstRetained = history.events.length > 0 ? history.events[0].sequence : history.nextSequence;
3223 if (resume.sequence >= history.nextSequence) return endpointImmediateStreamFailure(schema, "STREAM_RESUME_UNAVAILABLE", "Last-Event-ID is ahead of the recorded stream", { reason: "future", nextSequence: history.nextSequence });
3224 if (resume.sequence < firstRetained - 1) return endpointImmediateStreamFailure(schema, "STREAM_RESUME_UNAVAILABLE", "Last-Event-ID is older than the bounded replay history", { reason: "evicted", firstRetained });
3225 const frames = history.events.filter((event) => event.sequence > resume.sequence).map((event) => event.frame);
3226 if (history.terminal !== null) frames.push(history.terminal);
3227 return new Response(frames.join(""), { status: 200, headers: endpointStreamHeaders() });
3228}
3229
3230async function invokeStreamEndpoint(request, schema, args, middleware, environment, executionContext, parentSignal, deadlineAt, implementation) {
3231 const traceId = __noxidTraceIdForRequest(request);
3232 if (request.signal.aborted) return endpointResponseWithHeaders(endpointClosedStreamResponse(), middleware.headers);
3233 const resume = parseStreamResumeId(request.headers.get("last-event-id"));
3234 if (resume !== null) {
3235 if (resume.invalid) return endpointResponseWithHeaders(resumeEndpointStream(schema, resume, null), middleware.headers);
3236 let requestBinding;
3237 try { requestBinding = await endpointStreamRequestBinding(resume.token, request, args, environment, middleware.context); }
3238 catch { return endpointResponseWithHeaders(endpointImmediateStreamFailure(schema, "STREAM_RESUME_BINDING_UNAVAILABLE", "The stream request contract cannot be bound safely for resume"), middleware.headers); }
3239 if (parentSignal.aborted) return endpointResponseWithHeaders(endpointTimeoutFailure(schema), middleware.headers);
3240 if (request.signal.aborted) return endpointResponseWithHeaders(endpointClosedStreamResponse(), middleware.headers);
3241 return endpointResponseWithHeaders(resumeEndpointStream(schema, resume, requestBinding), middleware.headers);
3242 }
3243 const token = newStreamToken();
3244 if (token === null) return endpointResponseWithHeaders(endpointImmediateStreamFailure(schema, "STREAM_RESUME_TOKEN_UNAVAILABLE", "A secure stream resume token cannot be created in this runtime"), middleware.headers);
3245 let requestBinding;
3246 try { requestBinding = await endpointStreamRequestBinding(token, request, args, environment, middleware.context); }
3247 catch { return endpointResponseWithHeaders(endpointImmediateStreamFailure(schema, "STREAM_RESUME_BINDING_UNAVAILABLE", "The stream request contract cannot be bound safely for resume"), middleware.headers); }
3248 if (parentSignal.aborted) return endpointResponseWithHeaders(endpointTimeoutFailure(schema), middleware.headers);
3249 if (request.signal.aborted) return endpointResponseWithHeaders(endpointClosedStreamResponse(), middleware.headers);
3250 const history = reserveStreamHistory(schema, token, requestBinding);
3251 if (history === null) return endpointResponseWithHeaders(endpointImmediateStreamFailure(schema, "STREAM_REPLAY_CAPACITY", "All bounded stream replay histories are active; retry after a connection closes", { maxHistories: ENDPOINT_STREAM_MAX_HISTORIES }), middleware.headers);
3252 const validator = endpointValidator(schema.result.validator);
3253 if (validator === null) {
3254 history.completed = true;
3255 history.terminal = streamErrorFrame(schema, "STREAM_EVENT_VALIDATOR_MISSING", "Stream event validator is unavailable", { validator: schema.result.validator });
3256 return endpointResponseWithHeaders(new Response(history.terminal, { status: 200, headers: endpointStreamHeaders() }), middleware.headers);
3257 }
3258
3259 const encoder = new TextEncoder();
3260 const controller = new AbortController();
3261 let abortKind = null;
3262 let iterator = null;
3263 let streamController = null;
3264 let open = true;
3265 let timer = null;
3266 const abort = (kind) => {
3267 if (controller.signal.aborted) return;
3268 abortKind = kind;
3269 controller.abort(kind);
3270 };
3271 const onParentAbort = () => abort("timeout");
3272 const onRequestAbort = () => abort("disconnect");
3273 parentSignal.addEventListener("abort", onParentAbort, { once: true });
3274 request.signal.addEventListener("abort", onRequestAbort, { once: true });
3275 if (parentSignal.aborted) abort("timeout");
3276 else if (request.signal.aborted) abort("disconnect");
3277 const remaining = Math.max(0, deadlineAt - Date.now());
3278 timer = setTimeout(() => abort("timeout"), remaining);
3279
3280 const enqueue = (text) => {
3281 if (!open) return false;
3282 try { streamController.enqueue(encoder.encode(text)); return true; }
3283 catch { open = false; abort("disconnect"); return false; }
3284 };
3285 const finish = async (terminal = null) => {
3286 if (history.completed) return;
3287 history.completed = true;
3288 history.terminal = terminal;
3289 if (terminal !== null) enqueue(terminal);
3290 open = false;
3291 try { streamController.close(); } catch {}
3292 if (iterator && typeof iterator.return === "function") {
3293 try { await iterator.return(); } catch {}
3294 }
3295 };
3296 const abortOutcome = controller.signal.aborted
3297 ? Promise.resolve(Object.freeze({ kind: "abort" }))
3298 : new Promise((resolve) => controller.signal.addEventListener("abort", () => resolve(Object.freeze({ kind: "abort" })), { once: true }));
3299 const waitWithHeartbeats = async (promise) => {
3300 const pending = Promise.resolve(promise).then(
3301 (value) => Object.freeze({ kind: "value", value }),
3302 (cause) => Object.freeze({ kind: "error", cause }),
3303 );
3304 while (true) {
3305 let heartbeatTimer;
3306 const heartbeat = new Promise((resolve) => { heartbeatTimer = setTimeout(() => resolve(Object.freeze({ kind: "heartbeat" })), ENDPOINT_STREAM_HEARTBEAT_MS); });
3307 const outcome = await Promise.race([pending, abortOutcome, heartbeat]);
3308 clearTimeout(heartbeatTimer);
3309 if (outcome.kind !== "heartbeat") return outcome;
3310 if (!enqueue(": noxid-heartbeat\n\n")) return Object.freeze({ kind: "abort" });
3311 }
3312 };
3313
3314 const body = new ReadableStream({
3315 async start(readableController) {
3316 streamController = readableController;
3317 try {
3318 if (controller.signal.aborted) { await finish(); return; }
3319 const middlewareContext = middleware.context ?? EMPTY_MIDDLEWARE_CONTEXT;
3320 const context = __noxidDataContext({ request, environment, executionContext, signal: controller.signal, semanticId: schema.id, traceId, target: "endpoint", route: middleware.route, capabilities: schema.capabilities, middlewareContext }, __noxidPrincipal(middlewareContext, environment, __noxidAgentForRequest(request)));
3321 const implementationOutcome = await waitWithHeartbeats(Promise.resolve().then(() => implementation(args, context)));
3322 if (implementationOutcome.kind === "abort") {
3323 if (abortKind === "timeout") await finish(streamErrorFrame(schema, "ENDPOINT_TIMEOUT", "Endpoint exceeded its declared timeout", { timeoutMs: schema.timeoutMs }));
3324 else await finish();
3325 return;
3326 }
3327 if (implementationOutcome.kind === "error") {
3328 const cause = implementationOutcome.cause;
3329 const code = typeof cause?.code === "string" ? cause.code : "STREAM_EXECUTION_FAILED";
3330 const message = cause?.expose === true && typeof cause?.message === "string" ? cause.message : "Stream endpoint execution failed";
3331 await finish(streamErrorFrame(schema, code, message));
3332 return;
3333 }
3334 const iterable = implementationOutcome.value;
3335 if (iterable === null || iterable === undefined || typeof iterable[Symbol.asyncIterator] !== "function") {
3336 await finish(streamErrorFrame(schema, "STREAM_ITERABLE_REQUIRED", "Stream endpoint implementation must return an AsyncIterable"));
3337 return;
3338 }
3339 iterator = iterable[Symbol.asyncIterator]();
3340 while (open) {
3341 const next = await waitWithHeartbeats(Promise.resolve().then(() => iterator.next()));
3342 if (next.kind === "abort") {
3343 if (abortKind === "timeout") await finish(streamErrorFrame(schema, "ENDPOINT_TIMEOUT", "Endpoint exceeded its declared timeout", { timeoutMs: schema.timeoutMs }));
3344 else await finish();
3345 return;
3346 }
3347 if (next.kind === "error") {
3348 const cause = next.cause;
3349 const code = typeof cause?.code === "string" ? cause.code : "STREAM_EXECUTION_FAILED";
3350 const message = cause?.expose === true && typeof cause?.message === "string" ? cause.message : "Stream endpoint execution failed";
3351 await finish(streamErrorFrame(schema, code, message));
3352 return;
3353 }
3354 if (!next.value || typeof next.value !== "object" || typeof next.value.done !== "boolean") {
3355 await finish(streamErrorFrame(schema, "STREAM_ITERATOR_RESULT_INVALID", "Stream endpoint iterator returned an invalid result"));
3356 return;
3357 }
3358 if (next.value.done) { await finish(); return; }
3359 let trusted;
3360 try { trusted = validator(next.value.value); }
3361 catch (cause) {
3362 await finish(streamErrorFrame(schema, "STREAM_EVENT_TYPE", "Stream endpoint yielded an event that violates its declared type", { validation: typeof cause?.toJSON === "function" ? cause.toJSON() : null }));
3363 return;
3364 }
3365 const eventValue = trusted === undefined ? null : trusted;
3366 const serialized = JSON.stringify(eventValue);
3367 const dataBytes = encoder.encode(serialized).byteLength;
3368 if (dataBytes > ENDPOINT_STREAM_MAX_EVENT_BYTES) {
3369 await finish(streamErrorFrame(schema, "STREAM_EVENT_TOO_LARGE", "Stream endpoint event exceeds the bounded replay size", { maxBytes: ENDPOINT_STREAM_MAX_EVENT_BYTES, actualBytes: dataBytes }));
3370 return;
3371 }
3372 const sequence = history.nextSequence;
3373 const frame = streamFrame("message", eventValue, `${history.token}:${sequence}`);
3374 recordStreamEvent(history, frame, encoder.encode(frame).byteLength);
3375 if (!enqueue(frame)) { await finish(); return; }
3376 }
3377 } catch {
3378 await finish(streamErrorFrame(schema, "STREAM_EXECUTION_FAILED", "Stream endpoint execution failed"));
3379 } finally {
3380 clearTimeout(timer);
3381 parentSignal.removeEventListener("abort", onParentAbort);
3382 request.signal.removeEventListener("abort", onRequestAbort);
3383 }
3384 },
3385 async cancel() {
3386 open = false;
3387 abort("disconnect");
3388 history.completed = true;
3389 if (iterator && typeof iterator.return === "function") {
3390 try { await iterator.return(); } catch {}
3391 }
3392 },
3393 });
3394 return endpointResponseWithHeaders(new Response(body, { status: 200, headers: endpointStreamHeaders() }), middleware.headers);
3395}
3396
3397function endpointResultResponse(schema, value) {
3398 const isResult = schema.result.errorValidator !== null;
3399 if (isResult) {
3400 if (!value || typeof value !== "object" || !matchesResultTag(value.tag)) return failure(500, "ENDPOINT_RESULT_TYPE", "Endpoint returned a value that violates its declared Result type", schema.result.id);
3401 const validatorId = value.tag === "Err" ? schema.result.errorValidator : schema.result.validator;
3402 const validator = endpointValidator(validatorId);
3403 if (validator === null) return failure(500, "ENDPOINT_RESULT_VALIDATOR_MISSING", "Endpoint result validator is unavailable", schema.result.id);
3404 let trusted;
3405 try { trusted = validator(value.value); }
3406 catch (cause) { return failure(500, "ENDPOINT_RESULT_TYPE", "Endpoint returned a value that violates its declared result type", schema.result.id, { validation: typeof cause?.toJSON === "function" ? cause.toJSON() : null }); }
3407 return value.tag === "Err"
3408 ? json(422, { ok: false, error: { code: "ENDPOINT_RESULT_ERR", message: "Endpoint returned its declared error result", semanticId: schema.result.id, value: trusted } })
3409 : json(200, { ok: true, value: trusted === undefined ? null : trusted });
3410 }
3411 const validator = endpointValidator(schema.result.validator);
3412 if (validator === null) return failure(500, "ENDPOINT_RESULT_VALIDATOR_MISSING", "Endpoint result validator is unavailable", schema.result.id);
3413 try {
3414 const trusted = validator(value);
3415 return json(200, { ok: true, value: trusted === undefined ? null : trusted });
3416 } catch (cause) { return failure(500, "ENDPOINT_RESULT_TYPE", "Endpoint returned a value that violates its declared result type", schema.result.id, { validation: typeof cause?.toJSON === "function" ? cause.toJSON() : null }); }
3417}
3418
3419function matchesResultTag(tag) { return tag === "Ok" || tag === "Err"; }
3420
3421function endpointAbortResponse(signal, schema) {
3422 if (signal.aborted) return Promise.resolve(endpointTimeoutFailure(schema));
3423 return new Promise((resolve) => signal.addEventListener("abort", () => resolve(endpointTimeoutFailure(schema)), { once: true }));
3424}
3425
3426async function invokeEndpoint(request, schema, args, middleware, environment, executionContext, signal, deadlineAt) {
3427 const middlewareContext = middleware.context ?? EMPTY_MIDDLEWARE_CONTEXT;
3428 const endpointPrincipal = __noxidPrincipal(middlewareContext, environment, __noxidAgentForRequest(request));
3429 __noxidTraceBindPrincipal({ request }, endpointPrincipal);
3430 const endpointSpan = __noxidTraceBeginSemantic(request);
3431 try {
3432 const implementation = compiledEndpoints[schema.id] ?? hostEndpoints[schema.id];
3433 if (typeof implementation !== "function") {
3434 return schema.kind === "stream"
3435 ? endpointResponseWithHeaders(endpointImmediateStreamFailure(schema, "STREAM_IMPLEMENTATION_MISSING", `No implementation is registered for ${schema.id}`), middleware.headers)
3436 : failure(501, "ENDPOINT_IMPLEMENTATION_MISSING", `No implementation is registered for ${schema.id}`, schema.id);
3437 }
3438 if (signal.aborted) return endpointResponseWithHeaders(endpointTimeoutFailure(schema), middleware.headers);
3439 if (schema.kind === "stream") return invokeStreamEndpoint(request, schema, args, middleware, environment, executionContext, signal, deadlineAt, implementation);
3440 const execution = (async () => {
3441 try {
3442 const context = __noxidDataContext({ request, environment, executionContext, signal, semanticId: schema.id, traceId: __noxidTraceIdForRequest(request), target: "endpoint", route: middleware.route, capabilities: schema.capabilities, middlewareContext }, endpointPrincipal);
3443 const value = await implementation(args, context);
3444 if (signal.aborted) return endpointTimeoutFailure(schema);
3445 const validatedResponse = endpointResultResponse(schema, value);
3446 if (validatedResponse.ok) await __noxidPublishLiveInvalidations(schema.invalidates, endpointPrincipal);
3447 return endpointCacheResponse(validatedResponse, schema);
3448 } catch (cause) {
3449 if (signal.aborted) return endpointTimeoutFailure(schema);
3450 const code = typeof cause?.code === "string" ? cause.code : "ENDPOINT_EXECUTION_FAILED";
3451 const message = cause?.expose === true && typeof cause?.message === "string" ? cause.message : "Endpoint execution failed";
3452 return failure(500, code, message, schema.id, null, {}, false);
3453 }
3454 })();
3455 const response = await Promise.race([execution, endpointAbortResponse(signal, schema)]);
3456 return endpointResponseWithHeaders(response, middleware.headers);
3457 } finally {
3458 __noxidTraceFinishSemantic(endpointSpan, "endpoint", schema.id, { route: middleware.route?.pattern });
3459 }
3460}
3461
3462async function withEndpointDeadline(schema, operation) {
3463 const controller = new AbortController();
3464 if (schema.timeoutMs === 0) {
3465 controller.abort();
3466 return endpointTimeoutFailure(schema);
3467 }
3468 let timer;
3469 const deadlineAt = Date.now() + schema.timeoutMs;
3470 const timeout = new Promise((resolve) => {
3471 timer = setTimeout(() => {
3472 controller.abort();
3473 resolve(endpointTimeoutFailure(schema));
3474 }, schema.timeoutMs);
3475 });
3476 try {
3477 return await Promise.race([Promise.resolve().then(() => operation(controller.signal, deadlineAt)), timeout]);
3478 } finally {
3479 clearTimeout(timer);
3480 }
3481}
3482
3483async function handleEndpointRequest(request, url, environment, executionContext) {
3484 const candidates = endpointMatches(url.pathname);
3485 const matches = candidates.matches;
3486 if (matches.length === 0 && candidates.malformed.length === 0) return null;
3487 const methodMatches = matches.filter((candidate) => candidate.schema.method === request.method.toUpperCase());
3488 const malformedMethodMatches = candidates.malformed.filter((schema) => schema.method === request.method.toUpperCase());
3489 if (methodMatches.length === 0 && malformedMethodMatches.length > 0) {
3490 const schema = malformedMethodMatches[0];
3491 return failure(400, "ENDPOINT_PATH_ENCODING_INVALID", "Endpoint path contains invalid percent encoding; percent-encode one valid UTF-8 path value", schema.id);
3492 }
3493 if (methodMatches.length === 0) {
3494 const allow = [...new Set([...matches.map((candidate) => candidate.schema.method), ...candidates.malformed.map((schema) => schema.method)])].sort().join(", ");
3495 return failure(405, "ENDPOINT_METHOD_NOT_ALLOWED", "Endpoint path does not accept this method", null, null, { allow });
3496 }
3497 const { schema, params: rawParams } = methodMatches[0];
3498 return withEndpointDeadline(schema, async (signal, deadlineAt) => {
3499 try {
3500 const queryTransport = endpointQueryTransport(url);
3501 const middleware = await applyEndpointMiddleware(request, schema, rawParams, queryTransport.values, environment, executionContext, signal);
3502 if (signal.aborted) return endpointTimeoutFailure(schema);
3503 if (middleware.response) return endpointResponseWithHeaders(middleware.response, middleware.headers);
3504 const authorizationFailure = await authorizeEndpoint(request, schema, middleware.route, environment, executionContext, signal);
3505 if (signal.aborted) return endpointResponseWithHeaders(endpointTimeoutFailure(schema), middleware.headers);
3506 if (authorizationFailure) return endpointResponseWithHeaders(authorizationFailure, middleware.headers);
3507 let params, query, body;
3508 try {
3509 params = decodeEndpointParams(schema, rawParams);
3510 query = decodeEndpointQuery(schema, queryTransport);
3511 body = await decodeEndpointBody(request, schema, signal);
3512 } catch (cause) {
3513 if (signal.aborted) return endpointResponseWithHeaders(endpointTimeoutFailure(schema), middleware.headers);
3514 const response = failure(cause?.status ?? (cause?.code === "ENDPOINT_INPUT_TYPE" || cause?.code?.endsWith("_TYPE") ? 422 : 400), cause?.code ?? "ENDPOINT_INPUT_INVALID", cause?.message ?? "Endpoint request is invalid", schema.id, cause?.details ?? null);
3515 return endpointResponseWithHeaders(response, middleware.headers);
3516 }
3517 if (signal.aborted) return endpointResponseWithHeaders(endpointTimeoutFailure(schema), middleware.headers);
3518 const argumentsRecord = Object.freeze(Object.assign(Object.create(null), params, query, body));
3519 if (schema.kind === "stream" && (schema.cache !== null || schema.idempotent)) return endpointResponseWithHeaders(failure(500, "STREAM_OPERATION_POLICY_INVALID", "Stream endpoints cannot use response cache or idempotent replay policies", schema.id), middleware.headers);
3520 const rateIdentity = endpointRateIdentity(schema, request, environment, middleware.context);
3521 if (rateIdentity.error) return endpointResponseWithHeaders(rateIdentity.error, middleware.headers);
3522 let idempotencyMapKey = null;
3523 if (schema.idempotent) {
3524 const idempotencyKey = request.headers.get("idempotency-key");
3525 if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0 || idempotencyKey.length > 256 || !/^[\x21-\x7e]+$/.test(idempotencyKey)) return endpointResponseWithHeaders(failure(400, "ENDPOINT_IDEMPOTENCY_KEY_REQUIRED", "Idempotent endpoint requests require a valid Idempotency-Key header", schema.id), middleware.headers);
3526 const replayIdentity = endpointIdempotencyIdentity(request, environment, middleware.context);
3527 if (replayIdentity === null) return endpointResponseWithHeaders(failure(403, "ENDPOINT_IDEMPOTENCY_IDENTITY_REQUIRED", "Idempotent endpoint replay requires a session or trusted client identity", schema.id), middleware.headers);
3528 idempotencyMapKey = `${schema.id}\n${replayIdentity}\n${idempotencyKey}`;
3529 }
3530 if (idempotencyMapKey !== null) {
3531 try {
3532 const prepared = await withEndpointIdempotencyLock(idempotencyMapKey, async () => {
3533 if (signal.aborted) return { response: endpointTimeoutFailure(schema) };
3534 const inFlight = endpointIdempotencyInFlight.get(idempotencyMapKey);
3535 if (inFlight) return { promise: inFlight };
3536 const shared = sharedEndpointIdempotencyAvailable();
3537 let sharedClaim = null;
3538 if (shared) {
3539 const distributed = await prepareSharedEndpointIdempotency(schema, idempotencyMapKey, signal, deadlineAt);
3540 if (distributed.response) return { response: endpointResponseWithHeaders(distributed.response, middleware.headers) };
3541 if (distributed.snapshot) return { snapshot: distributed.snapshot };
3542 sharedClaim = distributed.claim;
3543 } else {
3544 const existing = await endpointIdempotencyStorage.get(idempotencyMapKey);
3545 if (existing !== null) {
3546 if (endpointSnapshotMatchesSchema(existing, schema)) return { snapshot: existing };
3547 const untraceableSnapshot = validEndpointSnapshotBase(existing);
3548 await endpointIdempotencyStorage.delete(idempotencyMapKey);
3549 if (untraceableSnapshot) return { response: endpointResponseWithHeaders(endpointStorageFailure(schema), middleware.headers) };
3550 }
3551 }
3552 const rateFailure = await enforceEndpointRateLimit(schema, rateIdentity.identity);
3553 if (rateFailure) {
3554 if (sharedClaim !== null) await __noxidSharedIdempotencyRelease(idempotencyMapKey, sharedClaim);
3555 return { response: endpointResponseWithHeaders(rateFailure, middleware.headers) };
3556 }
3557 if (!shared) await reserveEndpointIdempotencySlot(idempotencyMapKey);
3558 let promise;
3559 promise = (async () => {
3560 try {
3561 const response = await invokeEndpoint(request, schema, argumentsRecord, middleware, environment, executionContext, signal, deadlineAt);
3562 const snapshot = await endpointResponseSnapshot(response);
3563 if (sharedClaim === null) {
3564 await endpointIdempotencyStorage.set(idempotencyMapKey, snapshot, { ttl: ENDPOINT_IDEMPOTENCY_TTL_MS / 1000 });
3565 } else {
3566 await __noxidSharedIdempotencyComplete(idempotencyMapKey, sharedClaim, snapshot, ENDPOINT_IDEMPOTENCY_TTL_MS / 1000);
3567 }
3568 return snapshot;
3569 } catch (cause) {
3570 if (sharedClaim !== null) await __noxidSharedIdempotencyRelease(idempotencyMapKey, sharedClaim);
3571 throw cause;
3572 } finally {
3573 if (endpointIdempotencyInFlight.get(idempotencyMapKey) === promise) endpointIdempotencyInFlight.delete(idempotencyMapKey);
3574 }
3575 })();
3576 endpointIdempotencyInFlight.set(idempotencyMapKey, promise);
3577 return { promise };
3578 });
3579 if (prepared.response) return prepared.response;
3580 if (prepared.snapshot) return replayEndpointResponse(prepared.snapshot);
3581 const snapshot = await Promise.race([prepared.promise, endpointAbortResponse(signal, schema).then(endpointResponseSnapshot)]);
3582 return replayEndpointResponse(snapshot);
3583 } catch (cause) { return endpointResponseWithHeaders(endpointStorageFailure(schema, cause), middleware.headers); }
3584 }
3585 let rateFailure;
3586 try { rateFailure = await enforceEndpointRateLimit(schema, rateIdentity.identity); }
3587 catch (cause) { return endpointResponseWithHeaders(endpointStorageFailure(schema, cause), middleware.headers); }
3588 if (rateFailure) return endpointResponseWithHeaders(rateFailure, middleware.headers);
3589 return await invokeEndpoint(request, schema, argumentsRecord, middleware, environment, executionContext, signal, deadlineAt);
3590 } finally {
3591 await releaseEndpointUploads(request);
3592 }
3593 });
3594}
3595
3596const MCP_PROTOCOL_VERSIONS = Object.freeze(["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"]);
3597const MCP_REQUEST_MAX_BYTES = 1_048_576;
3598const MCP_TOOL_RESULT_MAX_BYTES = 1_048_576;
3599const MCP_STREAM_MAX_EVENTS = 256;
3600
3601const mcpOpenApiDocument = openapiDocument === null ? null : JSON.parse(openapiDocument);
3602
3603function mcpOpenApiOperation(schema) {
3604 for (const pathItem of Object.values(mcpOpenApiDocument?.paths ?? {})) {
3605 const operation = pathItem?.[schema.method.toLowerCase()];
3606 if (operation?.["x-noxid-endpoint-id"] === schema.id && operation?.operationId === schema.name) return operation;
3607 }
3608 throw Object.assign(new Error(`OpenAPI is missing the exact routed operation for ${schema.id}`), { code: "MCP_OPENAPI_OPERATION_MISSING" });
3609}
3610
3611function mcpRewriteOpenApiSchema(value) {
3612 if (Array.isArray(value)) return value.map(mcpRewriteOpenApiSchema);
3613 if (value === null || typeof value !== "object") return value;
3614 const output = Object.create(null);
3615 for (const [key, nested] of Object.entries(value)) {
3616 output[key] = key === "$ref" && typeof nested === "string" && nested.startsWith("#/components/schemas/")
3617 ? `#/$defs/${nested.slice("#/components/schemas/".length)}`
3618 : mcpRewriteOpenApiSchema(nested);
3619 }
3620 return output;
3621}
3622
3623function mcpSchemaDefinitions() {
3624 const definitions = Object.create(null);
3625 for (const [name, schema] of Object.entries(mcpOpenApiDocument?.components?.schemas ?? {})) definitions[name] = mcpRewriteOpenApiSchema(schema);
3626 return definitions;
3627}
3628
3629function mcpAttachDefinitions(schema) {
3630 return Object.freeze({ ...schema, $defs: Object.freeze(mcpSchemaDefinitions()) });
3631}
3632
3633function mcpInputSchema(operation) {
3634 const properties = Object.create(null);
3635 const required = [];
3636 for (const parameter of operation.parameters ?? []) {
3637 const parameterSchema = parameter.schema ?? parameter.content?.["application/json"]?.schema;
3638 if (!parameterSchema || typeof parameter.name !== "string") throw Object.assign(new Error("OpenAPI endpoint parameter is missing its schema"), { code: "MCP_OPENAPI_SCHEMA_MISSING" });
3639 properties[parameter.name] = mcpRewriteOpenApiSchema(parameterSchema);
3640 if (parameter.required === true) required.push(parameter.name);
3641 }
3642 const bodySchema = operation.requestBody?.content?.["application/json"]?.schema;
3643 if (bodySchema) {
3644 const rewritten = mcpRewriteOpenApiSchema(bodySchema);
3645 for (const [name, schema] of Object.entries(rewritten.properties ?? {})) properties[name] = schema;
3646 for (const name of rewritten.required ?? []) if (!required.includes(name)) required.push(name);
3647 }
3648 return mcpAttachDefinitions({ type: "object", properties: Object.freeze(properties), required: Object.freeze(required), additionalProperties: false });
3649}
3650
3651function mcpOutputSchema(schema, operation) {
3652 const response = operation.responses?.["200"];
3653 let body;
3654 if (schema.kind === "stream") {
3655 const eventSchema = response?.content?.["text/event-stream"]?.["x-noxid-event-schema"];
3656 if (!eventSchema) throw Object.assign(new Error(`OpenAPI is missing the stream event schema for ${schema.id}`), { code: "MCP_OPENAPI_SCHEMA_MISSING" });
3657 const errorSchema = mcpOpenApiDocument?.components?.schemas?.NoxidErrorResponse?.properties?.error ?? {};
3658 body = { type: "object", properties: { ok: { type: "boolean" }, events: { type: "array", items: mcpRewriteOpenApiSchema(eventSchema) }, error: mcpRewriteOpenApiSchema(errorSchema) }, required: ["ok", "events"], additionalProperties: false };
3659 } else {
3660 const resultSchema = response?.content?.["application/json"]?.schema;
3661 if (!resultSchema) throw Object.assign(new Error(`OpenAPI is missing the result schema for ${schema.id}`), { code: "MCP_OPENAPI_SCHEMA_MISSING" });
3662 body = mcpRewriteOpenApiSchema(resultSchema);
3663 }
3664 return mcpAttachDefinitions({ type: "object", properties: { status: { type: "integer" }, body }, required: ["status", "body"], additionalProperties: false });
3665}
3666
3667const mcpEndpointSchemas = Object.freeze(endpointSchemas.filter((schema) => schema.path.length > 0 && schema.method.length > 0));
3668const mcpTools = Object.freeze((mcpEnabled ? mcpEndpointSchemas : []).map((schema) => {
3669 const operation = mcpOpenApiOperation(schema);
3670 const tool = { name: schema.name, inputSchema: mcpInputSchema(operation), outputSchema: mcpOutputSchema(schema, operation), "x-noxid-endpoint": Object.freeze({ semanticId: schema.id, version: schema.version, method: schema.method, path: schema.path, kind: schema.kind, signature: operation["x-noxid-signature"] }) };
3671 if (typeof operation.description === "string") tool.description = operation.description;
3672 return Object.freeze(tool);
3673}));
3674
3675function mcpRpcResult(id, result) {
3676 return Object.freeze({ jsonrpc: "2.0", id, result });
3677}
3678
3679function mcpRpcError(id, code, message, data = null) {
3680 const error = { code, message };
3681 if (data !== null) error.data = data;
3682 return Object.freeze({ jsonrpc: "2.0", id, error: Object.freeze(error) });
3683}
3684
3685function mcpJsonResponse(status, payload, headers = {}) {
3686 return new Response(payload === null ? null : JSON.stringify(payload), {
3687 status,
3688 headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff", ...headers },
3689 });
3690}
3691
3692async function mcpRequestText(request) {
3693 const declared = Number(request.headers.get("content-length") ?? 0);
3694 if (Number.isFinite(declared) && declared > MCP_REQUEST_MAX_BYTES) throw Object.assign(new Error("MCP request body exceeds 1 MiB"), { code: "MCP_REQUEST_TOO_LARGE" });
3695 if (request.body === null) return "";
3696 const reader = request.body.getReader();
3697 const chunks = [];
3698 let total = 0;
3699 try {
3700 while (true) {
3701 const chunk = await reader.read();
3702 if (chunk.done) break;
3703 if (!(chunk.value instanceof Uint8Array)) throw new Error("MCP request body did not yield bytes");
3704 total += chunk.value.byteLength;
3705 if (total > MCP_REQUEST_MAX_BYTES) {
3706 await reader.cancel("MCP request body too large").catch(() => {});
3707 throw Object.assign(new Error("MCP request body exceeds 1 MiB"), { code: "MCP_REQUEST_TOO_LARGE" });
3708 }
3709 chunks.push(chunk.value);
3710 }
3711 } finally {
3712 try { reader.releaseLock(); } catch {}
3713 }
3714 const bytes = new Uint8Array(total);
3715 let offset = 0;
3716 for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
3717 try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
3718 catch { throw Object.assign(new Error("MCP request body must be UTF-8"), { code: "MCP_REQUEST_UTF8" }); }
3719}
3720
3721function mcpWireValue(value, type) {
3722 if (arrayWireType(type) !== null) return JSON.stringify(value);
3723 if (typeof value === "string") return value;
3724 if (typeof value === "number" || typeof value === "boolean") return String(value);
3725 return JSON.stringify(value);
3726}
3727
3728function mcpEndpointRequest(outerRequest, schema, args) {
3729 const validArguments = args !== null && typeof args === "object" && !Array.isArray(args) && Object.getPrototypeOf(args) === Object.prototype;
3730 const values = validArguments ? args : Object.freeze({});
3731 const declared = new Set([...schema.params, ...schema.query, ...schema.body].map((field) => field.name));
3732 const unknown = Object.keys(values).filter((name) => !declared.has(name));
3733 const transportErrors = validArguments ? [] : ["__noxid_mcp_arguments_must_be_object__"];
3734 let path = endpointFullPath(schema);
3735 for (const field of schema.params) {
3736 const present = Object.hasOwn(values, field.name);
3737 if (!present) transportErrors.push(`__noxid_mcp_missing_${field.name}__`);
3738 path = path.replace(`[${field.name}]`, encodeURIComponent(present ? mcpWireValue(values[field.name], field.type) : "__noxid_mcp_missing__"));
3739 }
3740 const url = new URL(outerRequest.url);
3741 url.pathname = path;
3742 url.search = "";
3743 for (const field of schema.query) {
3744 if (!Object.hasOwn(values, field.name) || values[field.name] === null) continue;
3745 url.searchParams.set(field.name, mcpWireValue(values[field.name], field.type));
3746 }
3747 const body = Object.create(null);
3748 for (const field of schema.body) if (Object.hasOwn(values, field.name)) body[field.name] = values[field.name];
3749 if (schema.body.length > 0) for (const name of unknown) body[name] = values[name];
3750 else for (const name of unknown) url.searchParams.set(name, mcpWireValue(values[name], "String"));
3751 for (const name of transportErrors) url.searchParams.set(name, "invalid");
3752 const headers = new Headers(outerRequest.headers);
3753 for (const name of ["accept", "content-length", "content-type", "last-event-id", "mcp-protocol-version", "mcp-session-id"]) headers.delete(name);
3754 const hasBody = schema.body.length > 0;
3755 if (hasBody) headers.set("content-type", "application/json");
3756 return new Request(url, { method: schema.method, headers, body: hasBody ? JSON.stringify(body) : undefined, signal: outerRequest.signal });
3757}
3758
3759async function mcpBoundedResponseText(response) {
3760 if (response.body === null) return "";
3761 const reader = response.body.getReader();
3762 const chunks = [];
3763 let total = 0;
3764 try {
3765 while (true) {
3766 const chunk = await reader.read();
3767 if (chunk.done) break;
3768 total += chunk.value.byteLength;
3769 if (total > MCP_TOOL_RESULT_MAX_BYTES) {
3770 await reader.cancel("MCP tool result too large").catch(() => {});
3771 throw Object.assign(new Error("MCP tool result exceeds the 1 MiB agent-surface limit"), { code: "MCP_TOOL_RESULT_TOO_LARGE" });
3772 }
3773 chunks.push(chunk.value);
3774 }
3775 } finally {
3776 try { reader.releaseLock(); } catch {}
3777 }
3778 const bytes = new Uint8Array(total);
3779 let offset = 0;
3780 for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
3781 try { return new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
3782 catch { throw Object.assign(new Error("MCP tool response is not UTF-8"), { code: "MCP_TOOL_RESULT_UTF8" }); }
3783}
3784
3785function mcpStreamBody(text) {
3786 const events = [];
3787 let terminal = null;
3788 for (const block of text.replace(/\r\n/g, "\n").split("\n\n")) {
3789 if (block.length === 0 || block.split("\n").every((line) => line.length === 0 || line.startsWith(":"))) continue;
3790 let event = "message";
3791 const data = [];
3792 for (const line of block.split("\n")) {
3793 if (line.startsWith("event:")) event = line.slice(6).trimStart();
3794 if (line.startsWith("data:")) data.push(line.slice(5).replace(/^ /, ""));
3795 }
3796 if (data.length === 0) continue;
3797 let value;
3798 try { value = JSON.parse(data.join("\n")); }
3799 catch { throw Object.assign(new Error("Stream endpoint emitted invalid JSON through MCP"), { code: "MCP_STREAM_EVENT_INVALID" }); }
3800 if (event === "message") {
3801 events.push(value);
3802 if (events.length > MCP_STREAM_MAX_EVENTS) throw Object.assign(new Error("Stream endpoint exceeded the 256-event MCP result limit"), { code: "MCP_STREAM_EVENT_LIMIT" });
3803 } else if (event === "noxid-error") terminal = value?.error ?? Object.freeze({ code: "MCP_STREAM_FAILED", message: "Stream endpoint failed" });
3804 else throw Object.assign(new Error(`Stream endpoint emitted unsupported SSE event ${event}`), { code: "MCP_STREAM_EVENT_INVALID" });
3805 }
3806 return Object.freeze({ ok: terminal === null, events: Object.freeze(events), ...(terminal === null ? {} : { error: terminal }) });
3807}
3808
3809function mcpToolResult(status, body, isError) {
3810 const structuredContent = Object.freeze({ status, body });
3811 return Object.freeze({
3812 content: Object.freeze([Object.freeze({ type: "text", text: JSON.stringify(structuredContent) })]),
3813 structuredContent,
3814 isError,
3815 });
3816}
3817
3818async function callMcpEndpointTool(outerRequest, schema, args, environment, executionContext) {
3819 let endpointRequest;
3820 try { endpointRequest = mcpEndpointRequest(outerRequest, schema, args); }
3821 catch (cause) {
3822 const error = Object.freeze({ ok: false, error: Object.freeze({ code: cause?.code ?? "MCP_ARGUMENTS_INVALID", message: cause?.message ?? "MCP tool arguments are invalid", semanticId: schema.id, details: cause?.argument ? { argument: cause.argument } : null }) });
3823 return mcpToolResult(400, error, true);
3824 }
3825 inheritNoxidRequestTrace(outerRequest, endpointRequest);
3826 __noxidAgentRequests.set(endpointRequest, schema.id);
3827 const response = await withNoxidRequestTrace(endpointRequest, () => handleEndpointRequest(endpointRequest, new URL(endpointRequest.url), environment, executionContext));
3828 if (response === null) return mcpToolResult(500, Object.freeze({ ok: false, error: Object.freeze({ code: "MCP_ENDPOINT_DISPATCH_FAILED", message: "MCP tool did not resolve to its declared HTTP endpoint", semanticId: schema.id, details: null }) }), true);
3829 __noxidTraceResponseFailure(endpointRequest, response);
3830 try {
3831 const text = await mcpBoundedResponseText(response);
3832 if (schema.kind === "stream") return mcpToolResult(response.status, mcpStreamBody(text), text.includes("event: noxid-error"));
3833 let body;
3834 try { body = text.length === 0 ? null : JSON.parse(text); }
3835 catch { body = Object.freeze({ contentType: response.headers.get("content-type"), text }); }
3836 return mcpToolResult(response.status, body, !response.ok || body?.ok === false);
3837 } catch (cause) {
3838 return mcpToolResult(500, Object.freeze({ ok: false, error: Object.freeze({ code: cause?.code ?? "MCP_TOOL_RESULT_FAILED", message: cause?.message ?? "MCP tool result could not be represented safely", semanticId: schema.id, details: null }) }), true);
3839 }
3840}
3841
3842async function handleMcpRequest(request, environment, executionContext) {
3843 if (request.method !== "POST") return mcpJsonResponse(405, mcpRpcError(null, -32600, "The MCP endpoint accepts POST only"), { allow: "POST" });
3844 const contentType = request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
3845 if (contentType !== "application/json") return mcpJsonResponse(415, mcpRpcError(null, -32600, "Content-Type must be application/json"));
3846 const accept = request.headers.get("accept") ?? "";
3847 if (!(accept.includes("application/json") && accept.includes("text/event-stream"))) return mcpJsonResponse(406, mcpRpcError(null, -32600, "Accept must include application/json and text/event-stream"));
3848 let payload;
3849 try { payload = JSON.parse(await mcpRequestText(request)); }
3850 catch (cause) { return mcpJsonResponse(cause?.code === "MCP_REQUEST_TOO_LARGE" ? 413 : 400, mcpRpcError(null, -32700, cause?.message ?? "MCP request must be valid JSON")); }
3851 if (payload === null || typeof payload !== "object" || Array.isArray(payload) || payload.jsonrpc !== "2.0" || typeof payload.method !== "string") return mcpJsonResponse(400, mcpRpcError(payload?.id ?? null, -32600, "Invalid JSON-RPC request"));
3852 const notification = !Object.hasOwn(payload, "id");
3853 if (notification) {
3854 return payload.method === "notifications/initialized" || payload.method === "notifications/cancelled"
3855 ? mcpJsonResponse(202, null)
3856 : mcpJsonResponse(400, mcpRpcError(null, -32600, "Unsupported MCP notification"));
3857 }
3858 const id = payload.id;
3859 const headerVersion = request.headers.get("mcp-protocol-version");
3860 if (headerVersion !== null && !MCP_PROTOCOL_VERSIONS.includes(headerVersion)) return mcpJsonResponse(400, mcpRpcError(id, -32022, `Unsupported MCP protocol version ${headerVersion}`));
3861 if (payload.method === "initialize") {
3862 const version = payload.params?.protocolVersion;
3863 if (typeof version !== "string" || !MCP_PROTOCOL_VERSIONS.includes(version)) return mcpJsonResponse(400, mcpRpcError(id, -32602, "initialize requires a supported params.protocolVersion"));
3864 if (headerVersion !== null && headerVersion !== version) return mcpJsonResponse(400, mcpRpcError(id, -32020, "MCP-Protocol-Version must match initialize params.protocolVersion"));
3865 return mcpJsonResponse(200, mcpRpcResult(id, Object.freeze({ protocolVersion: version, serverInfo: Object.freeze({ name: "noxid-endpoints", version: "0.1.0" }), capabilities: Object.freeze({ tools: Object.freeze({ listChanged: false }) }) })));
3866 }
3867 if (payload.method === "ping") return mcpJsonResponse(200, mcpRpcResult(id, Object.freeze({})));
3868 if (payload.method === "tools/list") return mcpJsonResponse(200, mcpRpcResult(id, Object.freeze({ tools: mcpTools })));
3869 if (payload.method === "tools/call") {
3870 const name = payload.params?.name;
3871 if (typeof name !== "string") return mcpJsonResponse(400, mcpRpcError(id, -32602, "tools/call requires params.name"));
3872 const schema = mcpEndpointSchemas.find((candidate) => candidate.name === name);
3873 if (!schema) return mcpJsonResponse(404, mcpRpcError(id, -32601, `Unknown endpoint tool ${name}`));
3874 const result = await callMcpEndpointTool(request, schema, payload.params?.arguments ?? Object.freeze({}), environment, executionContext);
3875 return mcpJsonResponse(200, mcpRpcResult(id, result));
3876 }
3877 return mcpJsonResponse(404, mcpRpcError(id, -32601, `Unknown MCP method ${payload.method}`));
3878}
3879
3880async function handleAgentSurfaceRequest(request, url, environment, executionContext) {
3881 const prefix = applicationBasePath === "/" ? "" : applicationBasePath.replace(/\/$/, "");
3882 /* noxid-server:agent-run-request */
3883 if (url.pathname === `${prefix}/_noxid/openapi.json`) {
3884 if (!openapiEnabled || openapiDocument === null) return failure(404, "AGENT_SURFACE_DISABLED", "OpenAPI serving is disabled");
3885 if (request.method !== "GET") return failure(405, "OPENAPI_METHOD", "OpenAPI serving requires GET", null, null, { allow: "GET" });
3886 return new Response(openapiDocument, { status: 200, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" } });
3887 }
3888 if (url.pathname === `${prefix}/_noxid/mcp`) {
3889 if (!mcpEnabled) return failure(404, "AGENT_SURFACE_DISABLED", "MCP serving is disabled");
3890 return handleMcpRequest(request, environment, executionContext);
3891 }
3892 return null;
3893}
3894
3895"##;
3896
3897const QUEUE_RUNTIME: &str = r##"
3898let queueDatabasePromise;
3899let queueDatabase;
3900const QUEUE_POLL_INTERVAL_MS = 250;
3901
3902function queueSchemaByName(name) {
3903 return queueSchemas.find((schema) => schema.name === name) ?? null;
3904}
3905
3906function queueDatabaseUrl() {
3907 const node = globalThis.process?.env?.DATABASE_URL;
3908 if (typeof node === "string" && node.length > 0) return node;
3909 try {
3910 const deno = globalThis.Deno?.env?.get?.("DATABASE_URL");
3911 if (typeof deno === "string" && deno.length > 0) return deno;
3912 } catch {}
3913 return null;
3914}
3915
3916async function queueSql() {
3917 if (queueDatabasePromise !== undefined) return queueDatabasePromise;
3918 queueDatabasePromise = (async () => {
3919 const url = queueDatabaseUrl();
3920 if (url === null) throw Object.assign(new Error("DATABASE_URL is required for durable queues"), { code: "QUEUE_DATABASE_URL_REQUIRED" });
3921 let postgres;
3922 try { postgres = (await import("postgres")).default; }
3923 catch { throw Object.assign(new Error("the admitted postgres driver is unavailable"), { code: "QUEUE_POSTGRES_DRIVER_MISSING" }); }
3924 const sql = postgres(url, { max: databasePoolSize });
3925 await sql.unsafe(`CREATE TABLE IF NOT EXISTS _noxid_jobs (
3926 id text PRIMARY KEY,
3927 queue text NOT NULL,
3928 payload jsonb NOT NULL,
3929 principal text,
3930 state text NOT NULL CHECK (state IN ('pending','running','completed','dead-letter')),
3931 attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
3932 run_at timestamptz NOT NULL,
3933 locked_by text,
3934 locked_at timestamptz,
3935 last_error text,
3936 created_at timestamptz NOT NULL DEFAULT now(),
3937 updated_at timestamptz NOT NULL DEFAULT now()
3938 )`);
3939 await sql`ALTER TABLE _noxid_jobs ADD COLUMN IF NOT EXISTS principal text`;
3940 await sql.unsafe("CREATE INDEX IF NOT EXISTS _noxid_jobs_claim ON _noxid_jobs (queue, state, run_at, created_at)");
3941 queueDatabase = sql;
3942 return sql;
3943 })();
3944 try { return await queueDatabasePromise; }
3945 catch (error) { queueDatabasePromise = undefined; throw error; }
3946}
3947
3948function validateQueuePayload(schema, payload, phase) {
3949 let keys;
3950 try {
3951 if (payload === null || typeof payload !== "object" || Array.isArray(payload)) throw new Error("shape");
3952 const prototype = Object.getPrototypeOf(payload);
3953 if (prototype !== Object.prototype && prototype !== null) throw new Error("prototype");
3954 keys = Reflect.ownKeys(payload);
3955 } catch {
3956 throw Object.assign(new TypeError(`queue ${schema.name} ${phase} payload must be an ordinary object`), { code: phase === "claim" ? "QUEUE_PAYLOAD_DRIFT" : "QUEUE_PAYLOAD_TYPE", semanticId: schema.id });
3957 }
3958 if (keys.some((key) => typeof key !== "string") || keys.some((key) => !schema.payload.some((field) => field.name === key))) {
3959 throw Object.assign(new TypeError(`queue ${schema.name} ${phase} payload has undeclared fields`), { code: phase === "claim" ? "QUEUE_PAYLOAD_DRIFT" : "QUEUE_PAYLOAD_TYPE", semanticId: schema.id });
3960 }
3961 const trusted = Object.create(null);
3962 for (const field of schema.payload) {
3963 let descriptor;
3964 try { descriptor = Object.getOwnPropertyDescriptor(payload, field.name); } catch {}
3965 const optional = field.type.startsWith("Optional<");
3966 if (descriptor === undefined) {
3967 if (optional) { trusted[field.name] = null; continue; }
3968 throw Object.assign(new TypeError(`queue ${schema.name} ${phase} payload is missing ${field.name}`), { code: phase === "claim" ? "QUEUE_PAYLOAD_DRIFT" : "QUEUE_PAYLOAD_TYPE", semanticId: schema.id });
3969 }
3970 if (!("value" in descriptor) || !descriptor.enumerable) {
3971 throw Object.assign(new TypeError(`queue ${schema.name} ${phase} payload field ${field.name} is not ordinary data`), { code: phase === "claim" ? "QUEUE_PAYLOAD_DRIFT" : "QUEUE_PAYLOAD_TYPE", semanticId: schema.id });
3972 }
3973 const result = validateType(field.type, descriptor.value, `payload.${field.name}`, field.typeIds, true);
3974 if (result.issue) throw Object.assign(new TypeError(result.issue), { code: phase === "claim" ? "QUEUE_PAYLOAD_DRIFT" : "QUEUE_PAYLOAD_TYPE", semanticId: schema.id, details: result.details ?? null });
3975 trusted[field.name] = result.value;
3976 }
3977 return Object.freeze(trusted);
3978}
3979
3980function queueUtcInstant(value) {
3981 if (typeof value !== "string") return null;
3982 const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?Z$/.exec(value);
3983 if (match === null) return null;
3984 const instant = new Date(value);
3985 const milliseconds = Number((match[7] ?? "").padEnd(3, "0"));
3986 return !Number.isNaN(instant.getTime())
3987 && instant.getUTCFullYear() === Number(match[1])
3988 && instant.getUTCMonth() + 1 === Number(match[2])
3989 && instant.getUTCDate() === Number(match[3])
3990 && instant.getUTCHours() === Number(match[4])
3991 && instant.getUTCMinutes() === Number(match[5])
3992 && instant.getUTCSeconds() === Number(match[6])
3993 && instant.getUTCMilliseconds() === milliseconds
3994 ? instant
3995 : null;
3996}
3997
3998function queueRunAt(value) {
3999 if (value === undefined) return new Date();
4000 if (typeof value === "string") {
4001 const instant = queueUtcInstant(value);
4002 if (instant !== null) return instant;
4003 } else if (value !== null && typeof value === "object") {
4004 try {
4005 const milliseconds = Date.prototype.getTime.call(value);
4006 if (!Number.isNaN(milliseconds)) return new Date(milliseconds);
4007 } catch {}
4008 }
4009 throw Object.assign(new TypeError("queue runAt must be a valid Date or UTC ISO timestamp"), { code: "QUEUE_RUN_AT_INVALID" });
4010}
4011
4012function queueJobId() {
4013 if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
4014 throw Object.assign(new Error("durable queue enqueue requires crypto.randomUUID"), { code: "QUEUE_ID_UNAVAILABLE" });
4015}
4016
4017export async function enqueue(queue, payload, options = Object.create(null)) {
4018 if (typeof queue !== "string") throw Object.assign(new TypeError("queue name must be a string"), { code: "QUEUE_NAME_INVALID" });
4019 const schema = queueSchemaByName(queue);
4020 if (schema === null) throw Object.assign(new Error("Unknown queue " + queue), { code: "QUEUE_NOT_FOUND" });
4021 let optionKeys;
4022 let runAtDescriptor;
4023 let contextDescriptor;
4024 try {
4025 if (options === null || typeof options !== "object" || Array.isArray(options)) throw new Error("shape");
4026 const prototype = Object.getPrototypeOf(options);
4027 if (prototype !== Object.prototype && prototype !== null) throw new Error("prototype");
4028 optionKeys = Reflect.ownKeys(options);
4029 if (optionKeys.some((key) => key !== "runAt" && key !== "context")) throw new Error("field");
4030 runAtDescriptor = Object.getOwnPropertyDescriptor(options, "runAt");
4031 if (runAtDescriptor !== undefined && (!("value" in runAtDescriptor) || !runAtDescriptor.enumerable)) throw new Error("descriptor");
4032 contextDescriptor = Object.getOwnPropertyDescriptor(options, "context");
4033 if (contextDescriptor !== undefined && (!("value" in contextDescriptor) || !contextDescriptor.enumerable)) throw new Error("descriptor");
4034 }
4035 catch { throw Object.assign(new TypeError("queue options must be an ordinary object"), { code: "QUEUE_OPTIONS_INVALID" }); }
4036 const trusted = validateQueuePayload(schema, payload, "enqueue");
4037 const runAt = queueRunAt(runAtDescriptor?.value);
4038 const captured = contextDescriptor === undefined ? __NOXID_SYSTEM_PRINCIPAL : __noxidRuntimePrincipals.get(contextDescriptor.value);
4039 if (contextDescriptor !== undefined && captured === undefined) {
4040 throw Object.assign(new TypeError("queue context must be a runtime-created execution context"), { code: "QUEUE_PRINCIPAL_CONTEXT_INVALID" });
4041 }
4042 const persistedPrincipal = captured?.canonical === "system" ? null : captured?.canonical;
4043 if (persistedPrincipal !== null && typeof persistedPrincipal !== "string") throw Object.assign(new TypeError("queue context has no canonical principal"), { code: "QUEUE_PRINCIPAL_CONTEXT_INVALID" });
4044 const sql = await queueSql();
4045 const id = queueJobId();
4046 await sql`INSERT INTO _noxid_jobs (id, queue, payload, principal, state, attempts, run_at) VALUES (${id}, ${schema.name}, ${sql.json(trusted)}, ${persistedPrincipal}, 'pending', 0, ${runAt})`;
4047 return Object.freeze({ id, queue: schema.name, state: "pending", attempts: 0, runAt: runAt.toISOString() });
4048}
4049
4050async function claimQueueJob(worker, queue = null, now = new Date()) {
4051 const sql = await queueSql();
4052 return sql.begin(async (transaction) => {
4053 const rows = queue === null
4054 ? await transaction`SELECT id, queue, payload, principal, attempts, run_at FROM _noxid_jobs WHERE state = 'pending' AND run_at <= ${now} ORDER BY run_at, created_at, id FOR UPDATE SKIP LOCKED LIMIT 1`
4055 : await transaction`SELECT id, queue, payload, principal, attempts, run_at FROM _noxid_jobs WHERE state = 'pending' AND queue = ${queue} AND run_at <= ${now} ORDER BY run_at, created_at, id FOR UPDATE SKIP LOCKED LIMIT 1`;
4056 const row = rows[0];
4057 if (row === undefined) return null;
4058 const schema = queueSchemaByName(row.queue);
4059 if (schema === null) {
4060 await transaction`UPDATE _noxid_jobs SET state = 'dead-letter', last_error = 'QUEUE_DECLARATION_MISSING', locked_by = NULL, locked_at = NULL, updated_at = now() WHERE id = ${row.id}`;
4061 return Object.freeze({ drift: true, id: row.id, code: "QUEUE_DECLARATION_MISSING" });
4062 }
4063 let payload;
4064 try { payload = validateQueuePayload(schema, row.payload, "claim"); }
4065 catch (cause) {
4066 await transaction`UPDATE _noxid_jobs SET state = 'dead-letter', last_error = 'QUEUE_PAYLOAD_DRIFT', locked_by = NULL, locked_at = NULL, updated_at = now() WHERE id = ${row.id}`;
4067 return Object.freeze({ drift: true, id: row.id, queue: schema.name, code: cause?.code ?? "QUEUE_PAYLOAD_DRIFT" });
4068 }
4069 const attempts = Number(row.attempts) + 1;
4070 await transaction`UPDATE _noxid_jobs SET state = 'running', attempts = ${attempts}, locked_by = ${worker}, locked_at = ${now}, updated_at = now() WHERE id = ${row.id}`;
4071 let principal;
4072 try { principal = __noxidPrincipalFromCanonical(row.principal); }
4073 catch (cause) {
4074 await transaction`UPDATE _noxid_jobs SET state = 'dead-letter', last_error = 'QUEUE_PRINCIPAL_DRIFT', locked_by = NULL, locked_at = NULL, updated_at = now() WHERE id = ${row.id}`;
4075 return Object.freeze({ drift: true, id: row.id, queue: schema.name, code: cause?.code ?? "QUEUE_PRINCIPAL_DRIFT" });
4076 }
4077 return Object.freeze({ id: row.id, queue: schema.name, schema, payload, principal, attempts, runAt: new Date(row.run_at).toISOString() });
4078 });
4079}
4080
4081async function completeQueueJob(id) {
4082 const sql = await queueSql();
4083 await sql`UPDATE _noxid_jobs SET state = 'completed', locked_by = NULL, locked_at = NULL, last_error = NULL, updated_at = now() WHERE id = ${id}`;
4084}
4085
4086async function failQueueJob(job, cause, now) {
4087 const sql = await queueSql();
4088 const message = typeof cause?.message === "string" ? cause.message.slice(0, 4096) : "Queue handler failed";
4089 if (job.attempts <= job.schema.retry) {
4090 const next = new Date(now.getTime() + job.schema.backoffMs);
4091 await sql`UPDATE _noxid_jobs SET state = 'pending', run_at = ${next}, locked_by = NULL, locked_at = NULL, last_error = ${message}, updated_at = now() WHERE id = ${job.id}`;
4092 return Object.freeze({ id: job.id, queue: job.queue, state: "pending", attempts: job.attempts, runAt: next.toISOString() });
4093 }
4094 await sql`UPDATE _noxid_jobs SET state = 'dead-letter', locked_by = NULL, locked_at = NULL, last_error = ${message}, updated_at = now() WHERE id = ${job.id}`;
4095 return Object.freeze({ id: job.id, queue: job.queue, state: "dead-letter", attempts: job.attempts, runAt: job.runAt });
4096}
4097
4098function queueWorkerFailure(message) {
4099 throw Object.assign(new TypeError(message), { code: "QUEUE_CLOCK_INVALID" });
4100}
4101
4102function queueWorkerOptions(options, allowed) {
4103 let trustedOptions;
4104 try {
4105 if (options === null || typeof options !== "object" || Array.isArray(options)) throw new Error("shape");
4106 const prototype = Object.getPrototypeOf(options);
4107 if (prototype !== Object.prototype && prototype !== null) throw new Error("prototype");
4108 trustedOptions = Object.create(null);
4109 for (const key of Reflect.ownKeys(options)) {
4110 if (typeof key !== "string" || !allowed.has(key)) throw new Error("field");
4111 const descriptor = Object.getOwnPropertyDescriptor(options, key);
4112 if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) throw new Error("descriptor");
4113 trustedOptions[key] = descriptor.value;
4114 }
4115 Object.freeze(trustedOptions);
4116 } catch {
4117 throw Object.assign(new TypeError("worker clock options must be ordinary data"), { code: "QUEUE_CLOCK_INVALID" });
4118 }
4119 return trustedOptions;
4120}
4121
4122function queueWorkerIdentity(value, label) {
4123 if (value === undefined || value === null) return null;
4124 if (typeof value !== "string") queueWorkerFailure(`worker ${label} must be a string`);
4125 return value;
4126}
4127
4128function queueWorkerClock(value) {
4129 if (value === undefined) return new Date();
4130 if (typeof value === "string") {
4131 const instant = queueUtcInstant(value);
4132 if (instant !== null) return instant;
4133 } else if (value !== null && typeof value === "object") {
4134 try {
4135 const milliseconds = Date.prototype.getTime.call(value);
4136 if (!Number.isNaN(milliseconds)) return new Date(milliseconds);
4137 } catch {}
4138 }
4139 queueWorkerFailure("worker clock must be a valid Date or UTC ISO timestamp");
4140}
4141
4142async function workQueueOnceWithLifecycle(options, onRunning) {
4143 const trustedOptions = queueWorkerOptions(options, new Set(["queue", "worker", "now"]));
4144 const queue = queueWorkerIdentity(trustedOptions.queue, "queue");
4145 if (queue !== null && queueSchemaByName(queue) === null) throw Object.assign(new Error("Unknown queue " + queue), { code: "QUEUE_NOT_FOUND" });
4146 const now = queueWorkerClock(trustedOptions.now);
4147 const worker = queueWorkerIdentity(trustedOptions.worker, "identity") ?? `noxid-${globalThis.process?.pid ?? "worker"}-${queueJobId()}`;
4148 const job = await claimQueueJob(worker, queue, now);
4149 if (job === null) return null;
4150 if (job.drift) {
4151 const refusalTrace = tracingMode === "full" ? __noxidTraceContext() : null;
4152 __noxidTraceEmit(refusalTrace, "validation.refused", { code: job.code, jobId: job.id });
4153 throw Object.assign(new Error("Persisted queue payload refused at claim"), { code: job.code, jobId: job.id });
4154 }
4155 const queueSpan = __noxidTraceBeginSemantic(null);
4156 const traceId = queueSpan?.trace?.id ?? null;
4157 const implementation = compiledQueues[job.schema.id] ?? hostQueues[job.schema.id];
4158 if (typeof implementation !== "function") {
4159 __noxidTraceFinishSemantic(queueSpan, "queue", job.schema.id, { jobId: job.id, attempts: job.attempts, spanName: "queue.run" });
4160 return failQueueJob(job, Object.assign(new Error("Queue implementation is missing"), { code: "QUEUE_IMPLEMENTATION_MISSING" }), now);
4161 }
4162 if (onRunning !== null) onRunning(job);
4163 let completed = false;
4164 try {
4165 const value = await implementation(job.payload, __noxidDataContext({ semanticId: job.schema.id, traceId, queue: job.queue, jobId: job.id, attempts: job.attempts, runAt: job.runAt }, job.principal));
4166 await completeQueueJob(job.id);
4167 completed = true;
4168 await __noxidPublishLiveInvalidations(job.schema.invalidates, job.principal);
4169 return Object.freeze({ id: job.id, queue: job.queue, state: "completed", attempts: job.attempts, runAt: job.runAt, value: value === undefined ? null : value });
4170 } catch (cause) {
4171 if (completed) throw cause;
4172 return failQueueJob(job, cause, now);
4173 } finally {
4174 __noxidTraceFinishSemantic(queueSpan, "queue", job.schema.id, { jobId: job.id, attempts: job.attempts, spanName: "queue.run" });
4175 }
4176}
4177
4178export async function workQueueOnce(options = Object.create(null)) {
4179 return workQueueOnceWithLifecycle(options, null);
4180}
4181
4182const QUEUE_DRAIN_SEMANTIC_ID = "queue-drain:on-demand";
4183const QUEUE_DRAIN_DEFAULT_BUDGET_MS = 25_000;
4184const QUEUE_DRAIN_MAX_BUDGET_MS = 300_000;
4185
4186function queueDrainBudget(executionContext) {
4187 let configured;
4188 try { configured = executionContext?.queueDrainBudgetMs; }
4189 catch { return null; }
4190 if (configured === undefined) return QUEUE_DRAIN_DEFAULT_BUDGET_MS;
4191 return Number.isSafeInteger(configured) && configured > 0 && configured <= QUEUE_DRAIN_MAX_BUDGET_MS
4192 ? configured
4193 : null;
4194}
4195
4196async function handleQueueDrainRequest(request, url, environment, executionContext) {
4197 if (url.pathname !== queueDrainPath) return null;
4198 let enabled = false;
4199 try { enabled = executionContext?.noxidQueueDrain === true; } catch {}
4200 if (!enabled) return failure(404, "QUEUE_DRAIN_DISABLED", "Queue draining is not enabled for this deployment", QUEUE_DRAIN_SEMANTIC_ID);
4201 if (request.method !== "POST") return failure(405, "QUEUE_DRAIN_METHOD", "Queue draining requires POST", QUEUE_DRAIN_SEMANTIC_ID, null, { allow: "POST" });
4202 const budgetMs = queueDrainBudget(executionContext);
4203 if (budgetMs === null) return failure(500, "QUEUE_DRAIN_BUDGET_INVALID", "The deployment supplied an invalid queue drain budget; configure a positive duration no greater than 300000ms", QUEUE_DRAIN_SEMANTIC_ID);
4204 if (queueSchemas.length === 0) return failure(501, "QUEUE_DRAIN_UNAVAILABLE", "Queue draining requires at least one declared queue", QUEUE_DRAIN_SEMANTIC_ID);
4205 if (typeof authorize !== "function") return failure(500, "QUEUE_DRAIN_AUTHORIZER_MISSING", "Queue draining requires a host authorizer for the queue.drain capability", QUEUE_DRAIN_SEMANTIC_ID, { capability: "queue.drain" });
4206 let allowed = false;
4207 try {
4208 allowed = await authorize(Object.freeze({
4209 capability: "queue.drain",
4210 semanticId: QUEUE_DRAIN_SEMANTIC_ID,
4211 traceId: __noxidTraceIdForRequest(request),
4212 target: "server",
4213 route: null,
4214 request,
4215 environment,
4216 executionContext,
4217 })) === true;
4218 } catch {}
4219 if (!allowed) return failure(403, "QUEUE_DRAIN_CAPABILITY_DENIED", "Queue drain capability was denied", QUEUE_DRAIN_SEMANTIC_ID, { capability: "queue.drain" });
4220
4221 const startedAt = Date.now();
4222 const counts = { claimed: 0, completed: 0, retried: 0, deadLettered: 0 };
4223 try {
4224 while (Date.now() - startedAt < budgetMs) {
4225 const result = await workQueueOnce();
4226 if (result === null) break;
4227 counts.claimed += 1;
4228 if (result.state === "completed") counts.completed += 1;
4229 else if (result.state === "pending") counts.retried += 1;
4230 else if (result.state === "dead-letter") counts.deadLettered += 1;
4231 }
4232 } catch (cause) {
4233 const causeCode = typeof cause?.code === "string" ? cause.code : "QUEUE_DRAIN_EXECUTION_FAILED";
4234 return failure(500, "QUEUE_DRAIN_FAILED", "Queue draining failed; inspect the durable queue database and handler configuration", QUEUE_DRAIN_SEMANTIC_ID, { causeCode });
4235 }
4236 return json(200, {
4237 ok: true,
4238 budgetMs,
4239 elapsedMs: Math.max(0, Date.now() - startedAt),
4240 counts: Object.freeze(counts),
4241 });
4242}
4243
4244const QUEUE_WORKER_STATE_NAMES = Object.freeze(["Idle", "Scheduled", "Claiming", "Running", "Stopping", "Stopped", "Failed"]);
4245const QUEUE_WORKER_EVENT_NAMES = Object.freeze(["Start", "Arm", "Deliver", "Claimed", "Settle", "Stop", "ArmFailed", "Notify"]);
4246const QUEUE_WORKER_TRANSITIONS = Object.freeze({
4247 Idle: Object.freeze({ Start: "Claiming", Arm: "Scheduled", Deliver: "Idle", Claimed: "Idle", Settle: "Idle", Stop: "Stopped", ArmFailed: "Idle", Notify: "Idle" }),
4248 Scheduled: Object.freeze({ Start: "Scheduled", Arm: "Scheduled", Deliver: "Claiming", Claimed: "Scheduled", Settle: "Scheduled", Stop: "Stopped", ArmFailed: "Failed", Notify: "Scheduled" }),
4249 Claiming: Object.freeze({ Start: "Claiming", Arm: "Claiming", Deliver: "Claiming", Claimed: "Running", Settle: "Idle", Stop: "Stopping", ArmFailed: "Failed", Notify: "Claiming" }),
4250 Running: Object.freeze({ Start: "Running", Arm: "Running", Deliver: "Running", Claimed: "Running", Settle: "Idle", Stop: "Stopping", ArmFailed: "Running", Notify: "Running" }),
4251 Stopping: Object.freeze({ Start: "Stopping", Arm: "Stopping", Deliver: "Stopping", Claimed: "Stopping", Settle: "Stopped", Stop: "Stopping", ArmFailed: "Stopping", Notify: "Stopping" }),
4252 Stopped: Object.freeze({ Start: "Stopped", Arm: "Stopped", Deliver: "Stopped", Claimed: "Stopped", Settle: "Stopped", Stop: "Stopped", ArmFailed: "Stopped", Notify: "Stopped" }),
4253 Failed: Object.freeze({ Start: "Failed", Arm: "Failed", Deliver: "Failed", Claimed: "Failed", Settle: "Failed", Stop: "Failed", ArmFailed: "Failed", Notify: "Failed" }),
4254});
4255
4256function queueWorkerState(name, detail = null) {
4257 if (!QUEUE_WORKER_STATE_NAMES.includes(name)) throw new Error(`Unknown queue worker state ${name}`);
4258 return Object.freeze({ name, detail });
4259}
4260
4261function queueWorkerEvent(name, detail = null) {
4262 if (!QUEUE_WORKER_EVENT_NAMES.includes(name)) throw new Error(`Unknown queue worker event ${name}`);
4263 return Object.freeze({ name, detail });
4264}
4265
4266function queueWorkerTransition(state, event) {
4267 const target = QUEUE_WORKER_TRANSITIONS[state.name]?.[event.name];
4268 if (target === undefined) throw new Error(`Unknown queue worker transition ${state.name} x ${event.name}`);
4269 if (state.name === "Scheduled" && (event.name === "Deliver" || event.name === "ArmFailed") && state.detail !== event.detail?.token) return state;
4270 if (target === state.name) return state;
4271 if (target === "Scheduled") return queueWorkerState(target, event.detail?.token ?? null);
4272 if (target === "Running") return queueWorkerState(target, event.detail?.job ?? null);
4273 if (target === "Failed") return queueWorkerState(target, event.detail?.error ?? null);
4274 return queueWorkerState(target);
4275}
4276
4277export function startQueueWorker(options = Object.create(null)) {
4278 const trustedOptions = queueWorkerOptions(options, new Set(["queue", "worker", "now", "setTimeout", "clearTimeout", "pollIntervalMs", "onError"]));
4279 const queue = queueWorkerIdentity(trustedOptions.queue, "queue");
4280 if (queue !== null && queueSchemaByName(queue) === null) throw Object.assign(new Error("Unknown queue " + queue), { code: "QUEUE_NOT_FOUND" });
4281 const worker = queueWorkerIdentity(trustedOptions.worker, "identity");
4282 const claimOptions = Object.create(null);
4283 if (queue !== null) claimOptions.queue = queue;
4284 if (worker !== null) claimOptions.worker = worker;
4285 if (trustedOptions.now !== undefined) claimOptions.now = queueWorkerClock(trustedOptions.now);
4286 Object.freeze(claimOptions);
4287 const setTimer = trustedOptions.setTimeout ?? globalThis.setTimeout;
4288 const clearTimer = trustedOptions.clearTimeout ?? globalThis.clearTimeout;
4289 const interval = trustedOptions.pollIntervalMs ?? QUEUE_POLL_INTERVAL_MS;
4290 const onError = trustedOptions.onError ?? console.error;
4291 if (typeof setTimer !== "function") queueWorkerFailure("worker setTimeout must be callable");
4292 if (typeof clearTimer !== "function") queueWorkerFailure("worker clearTimeout must be callable");
4293 if (typeof onError !== "function") queueWorkerFailure("worker onError must be callable");
4294 if (!Number.isSafeInteger(interval) || interval <= 0) queueWorkerFailure("worker pollIntervalMs must be a positive integer");
4295 let state = queueWorkerState("Idle");
4296 let queueSemanticId = queue === null ? null : queueSchemaByName(queue).id;
4297 const workerTrace = tracingMode === "full" ? __noxidTraceContext() : null;
4298 const transition = (event) => {
4299 const previous = state;
4300 const next = queueWorkerTransition(state, event);
4301 if (next === previous) return next;
4302 if (event.name === "Claimed" && typeof event.detail?.job?.schema?.id === "string") queueSemanticId = event.detail.job.schema.id;
4303 __noxidTraceEmit(workerTrace, "queue.state", {
4304 semanticId: queueSemanticId,
4305 state: next.name,
4306 transition: event.name,
4307 jobId: event.detail?.job?.id,
4308 attempts: event.detail?.job?.attempts,
4309 });
4310 return next;
4311 };
4312 let nextTimerToken = 0;
4313 let activeAttempt = null;
4314 let stopJoin = null;
4315 const settledJoin = Promise.resolve();
4316 const hookFailure = (cause) => { try { console.error(cause); } catch {} };
4317 const observeHookResult = (result) => {
4318 Promise.resolve(result).catch(hookFailure);
4319 };
4320 const clearToken = (token) => {
4321 if (token.delivered || token.clearInvoked) return;
4322 token.cancelled = true;
4323 token.consumed = true;
4324 if (!token.handleReady) return;
4325 token.clearInvoked = true;
4326 try { observeHookResult(clearTimer(token.handle)); }
4327 catch (cause) { hookFailure(cause); }
4328 };
4329 const notify = (error) => {
4330 try { observeHookResult(onError(error)); }
4331 catch (cause) { hookFailure(cause); }
4332 };
4333 const settleAttempt = (attempt, error) => {
4334 if (activeAttempt !== attempt || attempt.settled) return;
4335 attempt.settled = true;
4336 state = transition(queueWorkerEvent("Settle"));
4337 activeAttempt = null;
4338 if (error !== null) notify(error);
4339 if (state.name === "Idle") arm();
4340 attempt.resolveJoin();
4341 };
4342 const reserveAttempt = () => {
4343 let resolveJoin;
4344 const attempt = { settled: false, join: new Promise((resolve) => { resolveJoin = resolve; }), resolveJoin: null };
4345 attempt.resolveJoin = resolveJoin;
4346 activeAttempt = attempt;
4347 return attempt;
4348 };
4349 const launchAttempt = (attempt) => {
4350 if (attempt.settled) return;
4351 let result;
4352 try {
4353 result = workQueueOnceWithLifecycle(claimOptions, (job) => {
4354 if (activeAttempt === attempt && !attempt.settled) state = transition(queueWorkerEvent("Claimed", { job }));
4355 });
4356 } catch (cause) {
4357 settleAttempt(attempt, cause);
4358 return;
4359 }
4360 Promise.resolve(result).then(
4361 () => settleAttempt(attempt, null),
4362 (cause) => settleAttempt(attempt, cause),
4363 );
4364 };
4365 const beginAttempt = (event, deferLaunch = false) => {
4366 const previous = state;
4367 state = transition(event);
4368 if (previous === state || state.name !== "Claiming") return null;
4369 const attempt = reserveAttempt();
4370 if (!deferLaunch) launchAttempt(attempt);
4371 return attempt;
4372 };
4373 function arm() {
4374 if (state.name !== "Idle") return;
4375 const token = { id: ++nextTimerToken, consumed: false, delivered: false, cancelled: false, handleReady: false, handle: null, clearInvoked: false };
4376 state = transition(queueWorkerEvent("Arm", { token }));
4377 let synchronousAttempt = null;
4378 let arming = true;
4379 const fire = () => {
4380 if (token.consumed || state.name !== "Scheduled" || state.detail !== token) return;
4381 token.consumed = true;
4382 token.delivered = true;
4383 const attempt = beginAttempt(queueWorkerEvent("Deliver", { token }), arming);
4384 if (arming) synchronousAttempt = attempt;
4385 };
4386 let handle;
4387 try {
4388 handle = setTimer(fire, interval);
4389 observeHookResult(handle);
4390 } catch (cause) {
4391 arming = false;
4392 token.consumed = true;
4393 state = transition(queueWorkerEvent("ArmFailed", { token, error: cause }));
4394 if (synchronousAttempt !== null && state.name === "Failed") {
4395 synchronousAttempt.settled = true;
4396 activeAttempt = null;
4397 synchronousAttempt.resolveJoin();
4398 synchronousAttempt = null;
4399 }
4400 hookFailure(cause);
4401 if (synchronousAttempt !== null) launchAttempt(synchronousAttempt);
4402 return;
4403 }
4404 arming = false;
4405 token.handle = handle;
4406 token.handleReady = true;
4407 if (token.cancelled) clearToken(token);
4408 if (synchronousAttempt !== null) launchAttempt(synchronousAttempt);
4409 }
4410 /* noxid-server:agent-run-reconcile */
4411 beginAttempt(queueWorkerEvent("Start"));
4412 return Object.freeze({
4413 stop() {
4414 if (state.name === "Stopping" || state.name === "Stopped" || state.name === "Failed") return stopJoin ?? settledJoin;
4415 if (state.name === "Scheduled") {
4416 const token = state.detail;
4417 state = transition(queueWorkerEvent("Stop"));
4418 clearToken(token);
4419 stopJoin = settledJoin;
4420 return stopJoin;
4421 }
4422 if (state.name === "Idle") {
4423 state = transition(queueWorkerEvent("Stop"));
4424 stopJoin = settledJoin;
4425 return stopJoin;
4426 }
4427 if (state.name === "Claiming" || state.name === "Running") {
4428 state = transition(queueWorkerEvent("Stop"));
4429 stopJoin = activeAttempt?.join ?? settledJoin;
4430 return stopJoin;
4431 }
4432 return settledJoin;
4433 },
4434 });
4435}
4436
4437export async function queueStatus(queue = null) {
4438 if (queue !== null && queueSchemaByName(queue) === null) throw Object.assign(new Error("Unknown queue " + queue), { code: "QUEUE_NOT_FOUND" });
4439 const sql = await queueSql();
4440 const rows = queue === null
4441 ? await sql`SELECT queue, state, count(*)::int AS count FROM _noxid_jobs GROUP BY queue, state ORDER BY queue, state`
4442 : await sql`SELECT queue, state, count(*)::int AS count FROM _noxid_jobs WHERE queue = ${queue} GROUP BY queue, state ORDER BY queue, state`;
4443 return Object.freeze(rows.map((row) => Object.freeze({ queue: row.queue, state: row.state, count: Number(row.count) })));
4444}
4445
4446export async function closeQueueDatabase() {
4447 if (queueDatabase !== undefined) await queueDatabase.end({ timeout: 1 });
4448 queueDatabase = undefined;
4449 queueDatabasePromise = undefined;
4450}
4451
4452globalThis.__NOXID_QUEUE_ENQUEUE__ = enqueue;
4453"##;
4454
4455const MODEL_RUNTIME: &str = r##"
4467const MODEL_ANTHROPIC_VERSION = "2023-06-01";
4468const MODEL_DEFAULT_MAX_TOKENS = 1024;
4469const MODEL_PROVIDER_CODE = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
4470const MODEL_STRUCTURED_TOOL = "noxid_structured_result";
4471
4472function modelError(code, message, detail = null) {
4473 const error = new Error(message);
4474 error.code = code;
4475 if (detail !== null) error.detail = detail;
4476 return error;
4477}
4478
4479function modelDefinitionFor(handle) {
4480 const name = typeof handle === "string" ? handle : handle?.model ?? null;
4481 if (typeof name === "string" && Object.hasOwn(modelDeclarations, name)) return modelDeclarations[name];
4482 throw modelError(
4483 "MODEL_NOT_DECLARED",
4484 `no model \`${typeof name === "string" ? name : String(handle)}\` is declared; import { models } from "noxid:server" and pass models.<Name> for a model declared under server/models/<name>.nox`,
4485 );
4486}
4487
4488// One declared name at a time, never the whole allowlist: an unrelated missing
4489// secret must not fail a request that never asked for it.
4490function modelSecret(definition, name) {
4491 const value = globalThis.process?.env?.[name];
4492 if (typeof value !== "string" || value.length === 0) {
4493 throw modelError(
4494 "MODEL_SECRET_MISSING",
4495 `model \`${definition.name}\` requires the declared secret ${name}, which is absent from the environment; only requests that call this model fail`,
4496 );
4497 }
4498 return value;
4499}
4500
4501function modelBaseUrl(definition) {
4502 const declared = definition.baseUrl.kind === "secret"
4503 ? modelSecret(definition, definition.baseUrl.value)
4504 : definition.baseUrl.value;
4505 let parsed;
4506 try { parsed = new URL(declared); } catch {
4507 throw modelError("MODEL_PROVIDER_ERROR", `model \`${definition.name}\` has a base URL that is not a valid absolute URL`);
4508 }
4509 const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
4510 if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback)) {
4511 throw modelError("MODEL_PROVIDER_ERROR", `model \`${definition.name}\` resolved a non-https base URL; a model credential may only travel over https, or over http to a loopback host`);
4512 }
4513 return `${parsed.origin}${parsed.pathname.replace(/\/$/, "")}`;
4514}
4515
4516function modelTypeSchemaFor(definition, handle) {
4517 const name = typeof handle === "string" ? handle : handle?.type ?? null;
4518 const key = typeof name === "string" ? `type:${name}` : null;
4519 const entry = key !== null && Object.hasOwn(modelTypeSchemas, key) ? modelTypeSchemas[key] : null;
4520 if (entry === null) {
4521 throw modelError(
4522 "MODEL_OUTPUT_SCHEMA_UNSUPPORTED",
4523 `model \`${definition.name}\` cannot generate \`${typeof name === "string" ? name : String(handle)}\`: it is not a declared type with a strict structured-output schema; import { types } from "noxid:server" and pass types.<TypeName> for a type this build declares`,
4524 );
4525 }
4526 const validator = typeValidators[`type:${name}`] ?? typeValidators[`validator:${name}`] ?? null;
4527 if (validator === null) {
4528 throw modelError(
4529 "MODEL_OUTPUT_SCHEMA_UNSUPPORTED",
4530 `model \`${definition.name}\` cannot generate \`${name}\`: this build emits no boundary validator for it, and model output is never accepted unvalidated`,
4531 );
4532 }
4533 return Object.freeze({ name, schema: entry.schema, validator });
4534}
4535
4536function modelUsage(input, output) {
4537 return Object.freeze({
4538 inputTokens: Number.isSafeInteger(input) && input >= 0 ? input : 0,
4539 outputTokens: Number.isSafeInteger(output) && output >= 0 ? output : 0,
4540 });
4541}
4542
4543function modelTraceFields(definition, usage, retries, durationMs, code) {
4544 const fields = {
4545 semanticId: definition.id,
4546 model: definition.name,
4547 modelProvider: definition.provider,
4548 modelId: definition.modelId,
4549 tokensInput: usage.inputTokens,
4550 tokensOutput: usage.outputTokens,
4551 modelRetries: retries,
4552 durationMs,
4553 };
4554 if (code !== null) fields.code = code;
4555 return fields;
4556}
4557
4558// Prompts, options, and completions are never presented to the serializer:
4559// only the allowlisted identity, token, retry, and latency fields above.
4560function modelTrace(request, definition, usage, retries, durationMs, code = null) {
4561 let trace = __noxidTraceForRequest(request ?? null);
4562 if (trace === null && tracingMode === "full") trace = __noxidTraceContext();
4563 __noxidTraceEmit(trace, "model.generate", modelTraceFields(definition, usage, retries, durationMs, code));
4564}
4565
4566function modelOptions(options) {
4567 if (options === undefined || options === null) return Object.freeze({});
4568 if (typeof options !== "object") throw modelError("MODEL_OPTIONS_INVALID", "model options must be an object with optional temperature, maxTokens, system, signal, and request");
4569 const unknown = Object.keys(options).filter((key) => !["temperature", "maxTokens", "system", "signal", "request"].includes(key));
4570 if (unknown.length !== 0) throw modelError("MODEL_OPTIONS_INVALID", `model option \`${unknown[0]}\` is unknown; use temperature, maxTokens, system, signal, or request`);
4571 if (options.temperature !== undefined && (typeof options.temperature !== "number" || !Number.isFinite(options.temperature) || options.temperature < 0 || options.temperature > 2)) {
4572 throw modelError("MODEL_OPTIONS_INVALID", "model option `temperature` must be a number between 0 and 2");
4573 }
4574 if (options.maxTokens !== undefined && (!Number.isSafeInteger(options.maxTokens) || options.maxTokens <= 0)) {
4575 throw modelError("MODEL_OPTIONS_INVALID", "model option `maxTokens` must be a positive whole number");
4576 }
4577 if (options.system !== undefined && typeof options.system !== "string") {
4578 throw modelError("MODEL_OPTIONS_INVALID", "model option `system` must be a string");
4579 }
4580 return options;
4581}
4582
4583function modelPrompt(definition, prompt) {
4584 if (typeof prompt !== "string" || prompt.length === 0) {
4585 throw modelError("MODEL_PROMPT_INVALID", `model \`${definition.name}\` requires a non-empty string prompt; message-array prompts are not part of this boundary`);
4586 }
4587 return prompt;
4588}
4589
4590function modelTemperature(definition, options) {
4591 if (options.temperature !== undefined) return options.temperature;
4592 return definition.temperature === null ? undefined : definition.temperature;
4593}
4594
4595function modelMaxTokens(definition, options) {
4596 if (options.maxTokens !== undefined) return options.maxTokens;
4597 return definition.maxTokens === null ? MODEL_DEFAULT_MAX_TOKENS : definition.maxTokens;
4598}
4599
4600// Each provider names its own failures in a different field: Anthropic in
4601// `error.type`, OpenAI-compatible endpoints in `error.code`. Only a value that
4602// looks like a code survives — an error *message* may quote the prompt back.
4603function modelProviderCode(definition, payload) {
4604 const code = definition.provider === "anthropic"
4605 ? payload?.error?.type ?? null
4606 : payload?.error?.code ?? payload?.error?.type ?? null;
4607 return typeof code === "string" && MODEL_PROVIDER_CODE.test(code) ? code : null;
4608}
4609
4610// The provider's response body may quote the prompt back at us, so it never
4611// reaches the error: status plus the provider's own error code, nothing else.
4612async function modelRefuseResponse(definition, response) {
4613 let code = null;
4614 try { code = modelProviderCode(definition, await response.json()); } catch {}
4615 throw modelError(
4616 "MODEL_PROVIDER_ERROR",
4617 `model \`${definition.name}\` was refused by its ${definition.provider} endpoint with status ${response.status}${code === null ? "" : ` (${code})`}`,
4618 );
4619}
4620
4621async function modelSend(definition, path, headers, body, signal, stream = false) {
4622 const url = `${modelBaseUrl(definition)}${path}`;
4623 let response;
4624 try {
4625 // `globalThis.fetch` explicitly: this module exports its own `fetch` as
4626 // the request handler, and a bare call would reach that instead.
4627 response = await globalThis.fetch(url, {
4628 method: "POST",
4629 headers: { "content-type": "application/json", ...headers },
4630 body: JSON.stringify(body),
4631 signal: signal ?? undefined,
4632 });
4633 } catch (cause) {
4634 if (cause?.name === "AbortError" || cause?.name === "TimeoutError") {
4635 throw modelError("MODEL_TIMEOUT", `model \`${definition.name}\` was cancelled before its ${definition.provider} call completed`);
4636 }
4637 throw modelError("MODEL_PROVIDER_ERROR", `model \`${definition.name}\` could not reach its ${definition.provider} endpoint`);
4638 }
4639 if (!response.ok) await modelRefuseResponse(definition, response);
4640 if (stream) return response;
4641 try { return await response.json(); } catch {
4642 throw modelError("MODEL_PROVIDER_ERROR", `model \`${definition.name}\` returned a body its ${definition.provider} transport could not decode as JSON`);
4643 }
4644}
4645
4646function anthropicHeaders(definition) {
4647 return { "x-api-key": modelSecret(definition, definition.secret), "anthropic-version": MODEL_ANTHROPIC_VERSION };
4648}
4649
4650function openaiHeaders(definition) {
4651 return { authorization: `Bearer ${modelSecret(definition, definition.secret)}` };
4652}
4653
4654function anthropicBody(definition, prompt, options, stream) {
4655 const body = {
4656 model: definition.modelId,
4657 max_tokens: modelMaxTokens(definition, options),
4658 messages: [{ role: "user", content: prompt }],
4659 };
4660 const temperature = modelTemperature(definition, options);
4661 if (temperature !== undefined) body.temperature = temperature;
4662 if (typeof options.system === "string") body.system = options.system;
4663 if (stream) body.stream = true;
4664 return body;
4665}
4666
4667function openaiBody(definition, prompt, options, stream) {
4668 const messages = [];
4669 if (typeof options.system === "string") messages.push({ role: "system", content: options.system });
4670 messages.push({ role: "user", content: prompt });
4671 const body = { model: definition.modelId, messages, max_tokens: modelMaxTokens(definition, options) };
4672 const temperature = modelTemperature(definition, options);
4673 if (temperature !== undefined) body.temperature = temperature;
4674 if (stream) {
4675 body.stream = true;
4676 body.stream_options = { include_usage: true };
4677 }
4678 return body;
4679}
4680
4681function anthropicText(payload) {
4682 const blocks = Array.isArray(payload?.content) ? payload.content : [];
4683 return blocks.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
4684}
4685
4686function anthropicToolInput(payload) {
4687 const blocks = Array.isArray(payload?.content) ? payload.content : [];
4688 const block = blocks.find((candidate) => candidate?.type === "tool_use" && candidate?.name === MODEL_STRUCTURED_TOOL);
4689 return block === undefined ? undefined : block.input;
4690}
4691
4692function anthropicUsage(payload) {
4693 return modelUsage(payload?.usage?.input_tokens, payload?.usage?.output_tokens);
4694}
4695
4696function openaiUsage(payload) {
4697 return modelUsage(payload?.usage?.prompt_tokens, payload?.usage?.completion_tokens);
4698}
4699
4700async function modelProviderText(definition, prompt, options) {
4701 if (definition.provider === "anthropic") {
4702 const payload = await modelSend(definition, "/v1/messages", anthropicHeaders(definition), anthropicBody(definition, prompt, options, false), options.signal);
4703 return Object.freeze({ text: anthropicText(payload), usage: anthropicUsage(payload) });
4704 }
4705 const payload = await modelSend(definition, "/v1/chat/completions", openaiHeaders(definition), openaiBody(definition, prompt, options, false), options.signal);
4706 const content = payload?.choices?.[0]?.message?.content;
4707 return Object.freeze({ text: typeof content === "string" ? content : "", usage: openaiUsage(payload) });
4708}
4709
4710// Structured output is a forced tool on Anthropic and a strict `json_schema`
4711// response format on OpenAI-compatible endpoints. Both are requests, not
4712// guarantees: the answer still goes through the boundary validator.
4713async function modelProviderObject(definition, prompt, schema, options) {
4714 if (definition.provider === "anthropic") {
4715 const body = anthropicBody(definition, prompt, options, false);
4716 body.tools = [{ name: MODEL_STRUCTURED_TOOL, description: `Return the ${schema.name} result.`, input_schema: schema.schema }];
4717 body.tool_choice = { type: "tool", name: MODEL_STRUCTURED_TOOL };
4718 const payload = await modelSend(definition, "/v1/messages", anthropicHeaders(definition), body, options.signal);
4719 return Object.freeze({ value: anthropicToolInput(payload), usage: anthropicUsage(payload) });
4720 }
4721 const body = openaiBody(definition, prompt, options, false);
4722 body.response_format = { type: "json_schema", json_schema: { name: schema.name, strict: true, schema: schema.schema } };
4723 const payload = await modelSend(definition, "/v1/chat/completions", openaiHeaders(definition), body, options.signal);
4724 const content = payload?.choices?.[0]?.message?.content;
4725 let value;
4726 try { value = typeof content === "string" ? JSON.parse(content) : undefined; } catch { value = undefined; }
4727 return Object.freeze({ value, usage: openaiUsage(payload) });
4728}
4729
4730async function* modelSseEvents(definition, response, signal) {
4731 const reader = response.body?.getReader();
4732 if (reader === undefined) throw modelError("MODEL_PROVIDER_ERROR", `model \`${definition.name}\` returned a stream with no readable body`);
4733 const decoder = new TextDecoder();
4734 let buffer = "";
4735 try {
4736 for (;;) {
4737 if (signal?.aborted) throw modelError("MODEL_TIMEOUT", `model \`${definition.name}\` stream was cancelled`);
4738 const chunk = await reader.read();
4739 if (chunk.done) break;
4740 buffer += decoder.decode(chunk.value, { stream: true });
4741 let boundary = buffer.indexOf("\n\n");
4742 while (boundary !== -1) {
4743 const frame = buffer.slice(0, boundary);
4744 buffer = buffer.slice(boundary + 2);
4745 const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("");
4746 if (data.length !== 0 && data !== "[DONE]") {
4747 try { yield JSON.parse(data); } catch {}
4748 }
4749 boundary = buffer.indexOf("\n\n");
4750 }
4751 }
4752 } finally {
4753 try { await reader.cancel(); } catch {}
4754 }
4755}
4756
4757async function* modelProviderStream(definition, prompt, options) {
4758 let input = 0;
4759 let output = 0;
4760 if (definition.provider === "anthropic") {
4761 const response = await modelSend(definition, "/v1/messages", anthropicHeaders(definition), anthropicBody(definition, prompt, options, true), options.signal, true);
4762 for await (const event of modelSseEvents(definition, response, options.signal)) {
4763 if (event?.type === "message_start") input = event?.message?.usage?.input_tokens ?? input;
4764 if (event?.type === "content_block_delta" && typeof event?.delta?.text === "string") yield Object.freeze({ delta: event.delta.text });
4765 if (event?.type === "message_delta") output = event?.usage?.output_tokens ?? output;
4766 }
4767 return modelUsage(input, output);
4768 }
4769 const response = await modelSend(definition, "/v1/chat/completions", openaiHeaders(definition), openaiBody(definition, prompt, options, true), options.signal, true);
4770 for await (const event of modelSseEvents(definition, response, options.signal)) {
4771 const delta = event?.choices?.[0]?.delta?.content;
4772 if (typeof delta === "string" && delta.length !== 0) yield Object.freeze({ delta });
4773 if (event?.usage) {
4774 input = event.usage.prompt_tokens ?? input;
4775 output = event.usage.completion_tokens ?? output;
4776 }
4777 }
4778 return modelUsage(input, output);
4779}
4780
4781async function modelGenerateText(handle, prompt, options) {
4782 const definition = modelDefinitionFor(handle);
4783 const resolved = modelOptions(options);
4784 const text = modelPrompt(definition, prompt);
4785 const started = Date.now();
4786 const controller = modelScenarioController();
4787 try {
4788 const result = controller === null
4789 ? await modelProviderText(definition, text, resolved)
4790 : controller.text(definition);
4791 modelTrace(resolved.request, definition, result.usage, 0, Date.now() - started);
4792 return Object.freeze({ text: result.text, usage: result.usage });
4793 } catch (cause) {
4794 modelTrace(resolved.request, definition, modelUsage(0, 0), 0, Date.now() - started, cause?.code ?? "MODEL_PROVIDER_ERROR");
4795 throw cause;
4796 }
4797}
4798
4799async function modelGenerateObject(handle, prompt, type, options) {
4800 const definition = modelDefinitionFor(handle);
4801 const resolved = modelOptions(options);
4802 const text = modelPrompt(definition, prompt);
4803 const schema = modelTypeSchemaFor(definition, type);
4804 const started = Date.now();
4805 const controller = modelScenarioController();
4806 let input = 0;
4807 let output = 0;
4808 let retries = 0;
4809 let detail = null;
4810 try {
4811 for (;;) {
4812 const result = controller === null
4813 ? await modelProviderObject(definition, text, schema, resolved)
4814 : controller.object(definition);
4815 input += result.usage.inputTokens;
4816 output += result.usage.outputTokens;
4817 try {
4818 const value = schema.validator(result.value, true);
4819 const usage = modelUsage(input, output);
4820 modelTrace(resolved.request, definition, usage, retries, Date.now() - started);
4821 return Object.freeze({ value, usage });
4822 } catch (cause) {
4823 detail = cause?.message ?? String(cause);
4824 // The retry cap is declared, not adaptive: past it the shape is wrong,
4825 // not unlucky, and looping would only spend tokens. `retries: N` counts
4826 // re-attempts *after* the first attempt, so a call makes at most N + 1
4827 // provider attempts, and the refusal states both numbers rather than
4828 // leaving the reader to guess which one `retries` meant.
4829 if (retries >= definition.retries) {
4830 const attempts = retries + 1;
4831 throw modelError(
4832 "MODEL_OUTPUT_INVALID",
4833 `model \`${definition.name}\` returned output that failed \`${schema.name}\` validation after ${retries} ${retries === 1 ? "retry" : "retries"} (${attempts} ${attempts === 1 ? "attempt" : "attempts"}); the boundary validator refused it`,
4834 detail,
4835 );
4836 }
4837 retries += 1;
4838 }
4839 }
4840 } catch (cause) {
4841 modelTrace(resolved.request, definition, modelUsage(input, output), retries, Date.now() - started, cause?.code ?? "MODEL_PROVIDER_ERROR");
4842 throw cause;
4843 }
4844}
4845
4846function modelStreamText(handle, prompt, options) {
4847 const definition = modelDefinitionFor(handle);
4848 const resolved = modelOptions(options);
4849 const text = modelPrompt(definition, prompt);
4850 const controller = modelScenarioController();
4851 return {
4852 async *[Symbol.asyncIterator]() {
4853 const started = Date.now();
4854 try {
4855 const source = controller === null
4856 ? modelProviderStream(definition, text, resolved)
4857 : controller.tokens(definition);
4858 let usage = modelUsage(0, 0);
4859 for (;;) {
4860 const step = await source.next();
4861 if (step.done) {
4862 usage = step.value ?? usage;
4863 break;
4864 }
4865 yield step.value;
4866 }
4867 modelTrace(resolved.request, definition, usage, 0, Date.now() - started);
4868 yield Object.freeze({ usage });
4869 } catch (cause) {
4870 modelTrace(resolved.request, definition, modelUsage(0, 0), 0, Date.now() - started, cause?.code ?? "MODEL_PROVIDER_ERROR");
4871 throw cause;
4872 }
4873 },
4874 };
4875}
4876
4877globalThis.__NOXID_MODEL_RUNTIME__ = Object.freeze({
4878 generateText: modelGenerateText,
4879 generateObject: modelGenerateObject,
4880 streamText: modelStreamText,
4881 declarations: modelDeclarations,
4882});
4883"##;
4884
4885const MODEL_SCENARIO_RUNTIME: &str = r##"
4890function modelScenarioController() {
4891 const controller = globalThis.__NOXID_MODEL_SCENARIO__;
4892 if (controller === undefined || controller === null) return null;
4893 // The refusal names the call site, not just the model: one endpoint may call
4894 // several models, and the same model several times, so "which call" is the
4895 // part a reader cannot reconstruct from the message alone.
4896 const site = typeof controller.callSite === "string" && controller.callSite.length !== 0
4897 ? ` at call site \`${controller.callSite}\``
4898 : "";
4899 const take = (definition, kind) => {
4900 const stub = typeof controller.take === "function" ? controller.take(definition.name, kind) : null;
4901 if (stub === null || stub === undefined) {
4902 throw modelError(
4903 "MODEL_STUB_REQUIRED",
4904 `scenario called model \`${definition.name}\`${site} with no stub left; add \`given model ${definition.name} = text "..."\`, \`= object <Type>(field = value)\`, \`= tokens ["a", "b"]\`, or \`= fails MODEL_PROVIDER_ERROR\` to the scenario`,
4905 );
4906 }
4907 if (stub.kind === "fails") throw modelError(stub.code, `scenario stub for model \`${definition.name}\`${site} fails with ${stub.code}`);
4908 if (stub.kind !== kind) {
4909 throw modelError(
4910 "MODEL_STUB_REQUIRED",
4911 `scenario stubbed model \`${definition.name}\`${site} with a ${stub.kind} stub, but the call needs a ${kind} stub; declare the stub shape the call actually consumes`,
4912 );
4913 }
4914 return stub;
4915 };
4916 return Object.freeze({
4917 text(definition) {
4918 const stub = take(definition, "text");
4919 return Object.freeze({ text: stub.text, usage: modelUsage(stub.inputTokens, stub.outputTokens) });
4920 },
4921 object(definition) {
4922 const stub = take(definition, "object");
4923 return Object.freeze({ value: stub.value, usage: modelUsage(stub.inputTokens, stub.outputTokens) });
4924 },
4925 async *tokens(definition) {
4926 const stub = take(definition, "tokens");
4927 for (const token of stub.tokens) yield Object.freeze({ delta: token });
4928 return modelUsage(stub.inputTokens, stub.outputTokens);
4929 },
4930 });
4931}
4932"##;
4933
4934const LIVE_RESOURCE_RUNTIME: &str = r##"
4935const LIVE_RESOURCE_SCHEMA = Object.freeze({
4936 id: "live-resource:connection",
4937 name: "LiveResourceConnection",
4938 path: liveResourcePath,
4939 method: "GET",
4940 middleware: Object.freeze([]),
4941 timeoutMs: 30_000,
4942});
4943const LIVE_RESOURCE_MAX_HISTORIES = 128;
4944const LIVE_RESOURCE_MAX_EVENTS = 256;
4945const LIVE_RESOURCE_MAX_HISTORY_BYTES = 1_048_576;
4946const LIVE_RESOURCE_TOKEN = /^[A-Za-z0-9_-]{16,128}$/;
4947const liveResourceSchemaById = new Map(liveResourceSchemas.map((schema) => [schema.id, schema]));
4948const presenceSchemaById = new Map(presenceSchemas.map((schema) => [schema.id, schema]));
4949const liveResourceHistories = new Map();
4950const liveResourceEncoder = new TextEncoder();
4951
4952function liveResourceResetResponse(headers = []) {
4953 const responseHeaders = new Headers({
4954 "content-type": "text/event-stream; charset=utf-8",
4955 "cache-control": "no-store",
4956 "x-content-type-options": "nosniff",
4957 });
4958 for (const [name, value] of headers) responseHeaders.append(name, value);
4959 return new Response("event: noxid-live-reset\ndata: null\n\n", { status: 200, headers: responseHeaders });
4960}
4961
4962function liveResourceCursor(value) {
4963 if (typeof value !== "string" || value.length === 0 || value.length > 160) return null;
4964 const separator = value.lastIndexOf(":");
4965 if (separator < 0) return null;
4966 const token = value.slice(0, separator);
4967 const sequenceText = value.slice(separator + 1);
4968 if (!LIVE_RESOURCE_TOKEN.test(token) || !/^[1-9]\d{0,15}$/.test(sequenceText)) return null;
4969 const sequence = Number(sequenceText);
4970 return Number.isSafeInteger(sequence) ? Object.freeze({ token, sequence }) : null;
4971}
4972
4973function liveResourceToken() {
4974 let token;
4975 try { token = globalThis.crypto?.randomUUID?.().replaceAll("-", "_"); } catch {}
4976 if (typeof token !== "string" || !LIVE_RESOURCE_TOKEN.test(token)) {
4977 token = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}_${Math.random().toString(36).slice(2)}`;
4978 }
4979 return token.padEnd(16, "0").slice(0, 128);
4980}
4981
4982const PRESENCE_CREDENTIAL = /^[A-Za-z0-9_-]{16,128}$/;
4983const PRESENCE_STORAGE_SCHEMA = "noxid.presence.member.v1";
4984const PRESENCE_INDEX_SCHEMA = "noxid.presence.expiry.v1";
4985const PRESENCE_MAX_MEMBERS = 256;
4986const PRESENCE_JOINS_PER_MINUTE = 120;
4987const PRESENCE_MEMBER_WRITES_PER_MINUTE = 600;
4988const PRESENCE_ABUSE_ATTEMPTS_PER_MINUTE = 1200;
4989const PRESENCE_MAX_DELTA_BYTES = 6_000;
4990const PRESENCE_MAX_SNAPSHOT_BYTES = 65_536;
4991const PRESENCE_SWEEP_BATCH = 128;
4992const presenceMemberStorage = presenceSchemas.length === 0 ? null : __noxidStorage(`noxid:presence:${applicationNamespace}:members`);
4993const presenceExpiryStorage = presenceSchemas.length === 0 ? null : __noxidStorage(`noxid:presence:${applicationNamespace}:expiry`);
4994const presenceLocalLocks = new Map();
4995const presenceLocalRates = new Map();
4996
4997function presenceSseFrame(schema, event) {
4998 return `event: noxid-presence-event\ndata: ${JSON.stringify({ presence: schema.id, event })}\n\n`;
4999}
5000
5001function presenceSseFrameBytes(schema, event) {
5002 return liveResourceEncoder.encode(presenceSseFrame(schema, event)).byteLength;
5003}
5004
5005async function presenceDigest(...parts) {
5006 if (typeof globalThis.crypto?.subtle?.digest !== "function") {
5007 throw Object.assign(new Error("Presence credentials require Web Crypto SHA-256"), { code: "PRESENCE_CRYPTO_UNAVAILABLE" });
5008 }
5009 const bytes = await globalThis.crypto.subtle.digest("SHA-256", liveResourceEncoder.encode([applicationNamespace, ...parts].join("\n")));
5010 return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
5011}
5012
5013function presenceDigestEqual(left, right) {
5014 if (typeof left !== "string" || typeof right !== "string" || left.length !== right.length) return false;
5015 let difference = 0;
5016 for (let index = 0; index < left.length; index += 1) difference |= left.charCodeAt(index) ^ right.charCodeAt(index);
5017 return difference === 0;
5018}
5019
5020async function presenceLocalLock(key, operation, signal = null) {
5021 const previous = presenceLocalLocks.get(key) ?? Promise.resolve();
5022 let release;
5023 const current = new Promise((resolve) => { release = resolve; });
5024 presenceLocalLocks.set(key, current);
5025 const releaseAfterPrevious = () => previous.finally(() => {
5026 release();
5027 if (presenceLocalLocks.get(key) === current) presenceLocalLocks.delete(key);
5028 });
5029 if (signal?.aborted) {
5030 void releaseAfterPrevious();
5031 throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence operation was cancelled while waiting for its partition");
5032 }
5033 if (signal !== null) {
5034 let onAbort;
5035 const aborted = new Promise((resolve) => {
5036 onAbort = () => resolve(false);
5037 signal.addEventListener("abort", onAbort, { once: true });
5038 });
5039 const acquired = await Promise.race([previous.then(() => true), aborted]);
5040 signal.removeEventListener("abort", onAbort);
5041 if (!acquired) {
5042 void releaseAfterPrevious();
5043 throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence operation was cancelled while waiting for its partition");
5044 }
5045 } else {
5046 await previous;
5047 }
5048 try { return await operation(); }
5049 finally {
5050 release();
5051 if (presenceLocalLocks.get(key) === current) presenceLocalLocks.delete(key);
5052 }
5053}
5054
5055async function presencePartitionLock(schema, canonical, routeId, routePath, operation, signal = null) {
5056 const digest = await presenceDigest(schema.id, canonical, routeId, routePath);
5057 const key = `presence-lock:${digest}`;
5058 return presenceLocalLock(key, async () => {
5059 if (signal?.aborted) throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence operation exceeded its bounded lifetime");
5060 if (typeof __noxidSharedIdempotencyPrepare !== "function" || typeof __noxidSharedIdempotencyRelease !== "function") {
5061 const value = await operation();
5062 if (signal?.aborted) throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence operation exceeded its bounded lifetime");
5063 return value;
5064 }
5065 if (typeof globalThis.crypto?.randomUUID !== "function") throw presenceFailure("PRESENCE_CRYPTO_UNAVAILABLE", "Presence partition locks require secure random claims");
5066 const claim = globalThis.crypto.randomUUID();
5067 const leaseMilliseconds = LIVE_RESOURCE_SCHEMA.timeoutMs + 5_000;
5068 for (;;) {
5069 if (signal?.aborted) throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence operation exceeded its bounded lifetime");
5070 const prepared = await __noxidSharedIdempotencyPrepare(key, claim, leaseMilliseconds);
5071 if (prepared?.state === "owner") break;
5072 if (prepared?.state !== "pending") throw presenceFailure("PRESENCE_LOCK_INVALID", "Presence partition lock returned an invalid shared state");
5073 await new Promise((resolve) => setTimeout(resolve, 5));
5074 }
5075 let lost = false;
5076 let renewing = false;
5077 let renewalPromise = Promise.resolve();
5078 const renewal = setInterval(() => {
5079 if (renewing || lost) return;
5080 renewing = true;
5081 renewalPromise = __noxidSharedIdempotencyPrepare(key, claim, leaseMilliseconds)
5082 .then((value) => { if (value?.state !== "owner") lost = true; }, () => { lost = true; })
5083 .finally(() => { renewing = false; });
5084 }, Math.floor(leaseMilliseconds / 3));
5085 renewal?.unref?.();
5086 try {
5087 const value = await operation();
5088 if (lost || signal?.aborted) throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence partition lock ownership was not retained through commit");
5089 return value;
5090 } finally {
5091 clearInterval(renewal);
5092 await renewalPromise;
5093 await __noxidSharedIdempotencyRelease(key, claim);
5094 }
5095 }, signal);
5096}
5097
5098async function presencePartition(schema, canonical, routeId, routePath) {
5099 return `partition:${await presenceDigest(schema.id, canonical, routeId, routePath)}:`;
5100}
5101
5102async function presenceMemberKey(schema, canonical, routeId, routePath, memberId) {
5103 return `member:${await presencePartition(schema, canonical, routeId, routePath)}${memberId}`;
5104}
5105
5106function presenceExpiryKey(memberKey) {
5107 return `expiry:${memberKey}`;
5108}
5109
5110function presenceFailure(code, message, cause = undefined) {
5111 return Object.assign(new Error(message, cause === undefined ? undefined : { cause }), { code });
5112}
5113
5114async function presenceAdmitRate(identity, kind, budget, parts = []) {
5115 const key = `presence-${kind}-rate:${await presenceDigest(identity, ...parts)}`;
5116 if (typeof __noxidSharedRateLimit === "function") {
5117 let retryAfter;
5118 try { retryAfter = await __noxidSharedRateLimit(key, budget, 60_000); }
5119 catch (cause) { throw presenceFailure("PRESENCE_STORAGE_UNAVAILABLE", "Presence operational rate storage is temporarily unavailable", cause); }
5120 if (retryAfter !== null && retryAfter !== undefined) throw Object.assign(presenceFailure("PRESENCE_RATE_LIMITED", "Presence write rate exceeded its compiler-owned per-principal budget"), { retryAfter });
5121 return;
5122 }
5123 const now = Date.now();
5124 let bucket = presenceLocalRates.get(key);
5125 if (bucket === undefined || now - bucket.started >= 60_000) bucket = { started: now, count: 0 };
5126 if (bucket.count >= budget) throw Object.assign(presenceFailure("PRESENCE_RATE_LIMITED", "Presence write rate exceeded its compiler-owned membership budget"), { retryAfter: Math.max(1, Math.ceil((bucket.started + 60_000 - now) / 1000)) });
5127 bucket.count += 1;
5128 presenceLocalRates.delete(key);
5129 presenceLocalRates.set(key, bucket);
5130 while (presenceLocalRates.size > 1024) presenceLocalRates.delete(presenceLocalRates.keys().next().value);
5131}
5132
5133function presenceOperationalIdentity(environment, middlewareContext, principal) {
5134 const session = middlewareContext?.sessionId ?? middlewareContext?.session?.id ?? environment?.sessionId;
5135 if (typeof session === "string" && session.length > 0 && session.length <= 256 && !/[\u0000-\u001f\u007f]/.test(session)) return `session:${session}`;
5136 const trusted = environment?.requestIdentity;
5137 const requestIdentity = trusted?.id ?? trusted?.ip;
5138 if (typeof requestIdentity === "string" && requestIdentity.length > 0 && requestIdentity.length <= 256 && !/[\u0000-\u001f\u007f]/.test(requestIdentity)) return `request:${requestIdentity}`;
5139 return principal.canonical === "system" ? null : `principal:${principal.canonical}`;
5140}
5141
5142function presenceValidateType(typeId, value, code) {
5143 const validator = typeValidators[typeId];
5144 if (typeof validator !== "function") throw presenceFailure("PRESENCE_VALIDATOR_MISSING", `Compiled presence validator ${typeId} is unavailable`);
5145 try { return validator(value, true); }
5146 catch (cause) { throw presenceFailure(code, "Presence data did not match its compiler-generated closed type", cause); }
5147}
5148
5149function presenceValidateStored(schema, value, key) {
5150 if (value === null || typeof value !== "object" || Array.isArray(value)
5151 || value.schema !== PRESENCE_STORAGE_SCHEMA || value.presence !== schema.id
5152 || typeof value.principal !== "string" || typeof value.routeId !== "string" || typeof value.routePath !== "string"
5153 || typeof value.memberId !== "string" || !PRESENCE_CREDENTIAL.test(value.memberId)
5154 || typeof value.nonce !== "string" || !PRESENCE_CREDENTIAL.test(value.nonce)
5155 || typeof value.tokenDigest !== "string" || !/^[a-f0-9]{64}$/.test(value.tokenDigest)
5156 || !Number.isSafeInteger(value.version) || value.version < 1
5157 || !Number.isSafeInteger(value.expiresAt) || value.expiresAt < 1
5158 || Object.keys(value).sort().join("\n") !== "expiresAt\nmemberId\nnonce\npresence\nprincipal\nrecord\nrouteId\nroutePath\nschema\ntokenDigest\nversion") {
5159 throw presenceFailure("PRESENCE_STORAGE_INVALID", `Presence storage record ${key} failed its closed compiler-owned schema`);
5160 }
5161 const record = presenceValidateType(schema.recordType, value.record, "PRESENCE_STORAGE_INVALID");
5162 presenceValidateType(schema.memberType, Object.freeze({ id: value.memberId, ...record }), "PRESENCE_STORAGE_INVALID");
5163 return Object.freeze({ ...value, record });
5164}
5165
5166async function presenceWriteStored(schema, stored, memberKey) {
5167 const graceMilliseconds = Math.max(schema.ttlMilliseconds, 30_000);
5168 const ttl = Math.max(1, Math.ceil((schema.ttlMilliseconds + graceMilliseconds) / 1000));
5169 await presenceExpiryStorage.set(presenceExpiryKey(memberKey), Object.freeze({
5170 schema: PRESENCE_INDEX_SCHEMA,
5171 key: memberKey,
5172 presence: schema.id,
5173 principal: stored.principal,
5174 routeId: stored.routeId,
5175 routePath: stored.routePath,
5176 memberId: stored.memberId,
5177 version: stored.version,
5178 expiresAt: stored.expiresAt,
5179 state: "active",
5180 }), { ttl });
5181 await presenceMemberStorage.set(memberKey, stored, { ttl });
5182}
5183
5184function presenceValidateIndex(value, indexKey) {
5185 if (value === null || typeof value !== "object" || Array.isArray(value)
5186 || value.schema !== PRESENCE_INDEX_SCHEMA || typeof value.key !== "string" || presenceExpiryKey(value.key) !== indexKey
5187 || typeof value.presence !== "string" || typeof value.principal !== "string"
5188 || typeof value.memberId !== "string" || !PRESENCE_CREDENTIAL.test(value.memberId)
5189 || !Number.isSafeInteger(value.version) || value.version < 1
5190 || !Number.isSafeInteger(value.expiresAt) || value.expiresAt < 1
5191 || typeof value.routeId !== "string" || typeof value.routePath !== "string" || !["active", "left", "left-published"].includes(value.state)
5192 || Object.keys(value).sort().join("\n") !== "expiresAt\nkey\nmemberId\npresence\nprincipal\nrouteId\nroutePath\nschema\nstate\nversion") {
5193 throw presenceFailure("PRESENCE_INDEX_INVALID", "Presence expiry index failed its metadata-only schema");
5194 }
5195 return Object.freeze(value);
5196}
5197
5198async function presenceTopicId(schema, routeId, routePath) {
5199 return `presence-topic:${await presenceDigest(schema.id, routeId, routePath)}`;
5200}
5201
5202async function presencePublish(schema, principal, body) {
5203 const startedAt = __noxidPubSubNow();
5204 let state = "delivered";
5205 let code = null;
5206 try { await __noxidPubSubPublish(__noxidPubSubEvent("presence", await presenceTopicId(schema, body.routeId, body.routePath), principal, body)); }
5207 catch (cause) { state = "failed"; code = cause?.code ?? "PRESENCE_PUBLISH_FAILED"; throw cause; }
5208 finally { __noxidTraceEmit(tracingMode === "full" ? __noxidTraceContext() : null, "presence.publish", { semanticId: schema.id, state, event: body.tag, code, durationMs: Math.max(0, __noxidPubSubNow() - startedAt) }); }
5209}
5210
5211async function presencePublishCanonical(schema, canonical, body) {
5212 const startedAt = __noxidPubSubNow();
5213 let state = "delivered";
5214 let code = null;
5215 try { await __noxidPubSubPublish(__noxidPubSubEventFromCanonical("presence", await presenceTopicId(schema, body.routeId, body.routePath), canonical, body)); }
5216 catch (cause) { state = "failed"; code = cause?.code ?? "PRESENCE_PUBLISH_FAILED"; throw cause; }
5217 finally { __noxidTraceEmit(tracingMode === "full" ? __noxidTraceContext() : null, "presence.publish", { semanticId: schema.id, state, event: body.tag, code, durationMs: Math.max(0, __noxidPubSubNow() - startedAt) }); }
5218}
5219
5220async function presenceClaimExpired(indexKey, expected = null) {
5221 const rawIndex = expected ?? await presenceExpiryStorage.get(indexKey);
5222 if (rawIndex === null) return false;
5223 const index = presenceValidateIndex(rawIndex, indexKey);
5224 const schema = presenceSchemaById.get(index.presence);
5225 if (schema === undefined) throw presenceFailure("PRESENCE_INDEX_INVALID", "Presence expiry index named an unknown compiler contract");
5226 let tombstone = index;
5227 if (index.state === "left-published") return false;
5228 if (index.state === "active") {
5229 if (index.expiresAt > Date.now()) return false;
5230 tombstone = await presencePartitionLock(schema, index.principal, index.routeId, index.routePath, async () => {
5231 const currentIndex = await presenceExpiryStorage.get(indexKey);
5232 if (currentIndex === null || currentIndex.state !== "active" || currentIndex.version !== index.version || currentIndex.expiresAt > Date.now()) return null;
5233 const raw = await presenceMemberStorage.get(index.key);
5234 if (raw === null) {
5235 await presenceExpiryStorage.delete(indexKey);
5236 return null;
5237 }
5238 const stored = presenceValidateStored(schema, raw, index.key);
5239 if (stored.version !== currentIndex.version || stored.expiresAt > Date.now()) return null;
5240 const left = Object.freeze({ ...currentIndex, version: stored.version + 1, state: "left" });
5241 await presenceExpiryStorage.set(indexKey, left);
5242 await presenceMemberStorage.delete(index.key);
5243 return left;
5244 });
5245 if (tombstone === null) return false;
5246 } else if (index.state === "left") {
5247 tombstone = await presencePartitionLock(schema, index.principal, index.routeId, index.routePath, async () => {
5248 const rawCurrent = await presenceExpiryStorage.get(indexKey);
5249 if (rawCurrent === null) return null;
5250 const current = presenceValidateIndex(rawCurrent, indexKey);
5251 if (current.state !== "left" || current.version !== index.version) return null;
5252 const raw = await presenceMemberStorage.get(current.key);
5253 if (raw !== null) {
5254 const stored = presenceValidateStored(schema, raw, current.key);
5255 if (stored.principal !== current.principal || stored.routeId !== current.routeId || stored.routePath !== current.routePath || stored.memberId !== current.memberId || stored.version >= current.version) throw presenceFailure("PRESENCE_STORAGE_INVALID", "Pending Left conflicts with its authored member record");
5256 await presenceMemberStorage.delete(current.key);
5257 }
5258 return current;
5259 });
5260 if (tombstone === null) return false;
5261 }
5262 await presencePublishCanonical(schema, tombstone.principal, Object.freeze({ tag: "Left", value: tombstone.memberId, memberId: tombstone.memberId, version: tombstone.version, routeId: tombstone.routeId, routePath: tombstone.routePath }));
5263 await presencePartitionLock(schema, tombstone.principal, tombstone.routeId, tombstone.routePath, async () => {
5264 const current = await presenceExpiryStorage.get(indexKey);
5265 if (current?.state === "left" && current.version === tombstone.version) {
5266 await presenceExpiryStorage.set(indexKey, Object.freeze({ ...current, state: "left-published" }), { ttl: Math.max(60, Math.ceil(schema.ttlMilliseconds * 2 / 1000)) });
5267 }
5268 });
5269 return true;
5270}
5271
5272async function presenceSweep() {
5273 if (presenceExpiryStorage === null) return;
5274 const startedAt = __noxidPubSubNow();
5275 let state = "delivered";
5276 let code = null;
5277 let claim = null;
5278 let ownsClaim = false;
5279 try {
5280 if (typeof __noxidSharedIdempotencyPrepare === "function") {
5281 claim = globalThis.crypto?.randomUUID?.();
5282 if (typeof claim !== "string") throw presenceFailure("PRESENCE_CRYPTO_UNAVAILABLE", "Presence sweep ownership requires secure random claims");
5283 const prepared = await __noxidSharedIdempotencyPrepare(`presence-sweep:${applicationNamespace}`, claim, LIVE_RESOURCE_SCHEMA.timeoutMs + 5_000);
5284 if (prepared?.state === "pending") { state = "retry"; return; }
5285 if (prepared?.state !== "owner") throw presenceFailure("PRESENCE_SWEEP_LOCK_INVALID", "Presence sweep lock returned an invalid shared state");
5286 ownsClaim = true;
5287 }
5288 const keys = await presenceExpiryStorage.list("expiry:");
5289 const cursor = await presenceExpiryStorage.get("sweep:cursor");
5290 const start = typeof cursor?.key === "string" ? Math.max(0, keys.findIndex((key) => key > cursor.key)) : 0;
5291 const batch = [...keys.slice(start), ...keys.slice(0, start)].slice(0, PRESENCE_SWEEP_BATCH);
5292 for (const key of batch) {
5293 try {
5294 const raw = await presenceExpiryStorage.get(key);
5295 if (raw === null) continue;
5296 const index = presenceValidateIndex(raw, key);
5297 if (index.state === "left" || index.state === "active" && index.expiresAt <= Date.now()) await presenceClaimExpired(key, index);
5298 } catch (cause) {
5299 __noxidTraceEmit(null, "presence.sweep.item", { semanticId: null, state: "failed", code: cause?.code ?? "PRESENCE_SWEEP_ITEM_FAILED" });
5300 }
5301 }
5302 if (batch.length > 0) await presenceExpiryStorage.set("sweep:cursor", Object.freeze({ key: batch.at(-1) }));
5303 } catch (cause) {
5304 state = "failed";
5305 code = cause?.code ?? "PRESENCE_SWEEP_FAILED";
5306 } finally {
5307 if (ownsClaim && claim !== null && typeof __noxidSharedIdempotencyRelease === "function") await __noxidSharedIdempotencyRelease(`presence-sweep:${applicationNamespace}`, claim).catch((cause) => { state = "failed"; code = cause?.code ?? "PRESENCE_SWEEP_RELEASE_FAILED"; });
5308 __noxidTraceEmit(null, "presence.sweep", { semanticId: null, state, code, durationMs: Math.max(0, __noxidPubSubNow() - startedAt) });
5309 }
5310}
5311
5312if (presenceSchemas.length > 0) {
5313 const interval = Math.max(1000, Math.min(...presenceSchemas.map((schema) => schema.heartbeatMilliseconds)));
5314 const schedule = () => {
5315 const timer = setTimeout(async () => { await presenceSweep(); schedule(); }, interval + Math.floor(Math.random() * Math.max(1, Math.floor(interval / 3))));
5316 timer?.unref?.();
5317 };
5318 schedule();
5319}
5320
5321async function presenceSnapshot(schema, principal, routeId, routePath, signal) {
5322 return presencePartitionLock(schema, principal.canonical, routeId, routePath, async () => {
5323 const members = [];
5324 const versions = new Map();
5325 const prefix = `member:${await presencePartition(schema, principal.canonical, routeId, routePath)}`;
5326 for (const key of await presenceMemberStorage.list(prefix)) {
5327 const raw = await presenceMemberStorage.get(key);
5328 if (raw === null) continue;
5329 const stored = presenceValidateStored(schema, raw, key);
5330 if (stored.principal !== principal.canonical || stored.routeId !== routeId || stored.routePath !== routePath) throw presenceFailure("PRESENCE_STORAGE_INVALID", "Presence storage crossed its canonical principal or route partition");
5331 const indexKey = presenceExpiryKey(key);
5332 const rawIndex = await presenceExpiryStorage.get(indexKey);
5333 const index = rawIndex === null ? null : presenceValidateIndex(rawIndex, indexKey);
5334 if (index?.state === "left" || index?.state === "left-published" || stored.expiresAt <= Date.now()) continue;
5335 if (index === null || index.state !== "active" || index.version !== stored.version || index.expiresAt !== stored.expiresAt || index.principal !== stored.principal || index.routeId !== stored.routeId || index.routePath !== stored.routePath) {
5336 await presenceWriteStored(schema, stored, key);
5337 }
5338 const member = presenceValidateType(schema.memberType, Object.freeze({ id: stored.memberId, ...stored.record }), "PRESENCE_STORAGE_INVALID");
5339 members.push(member);
5340 versions.set(stored.memberId, stored.version);
5341 if (members.length > PRESENCE_MAX_MEMBERS) throw presenceFailure("PRESENCE_CAPACITY_EXCEEDED", "Presence snapshot exceeded its bounded compiler-owned member capacity");
5342 }
5343 members.sort((left, right) => left.id.localeCompare(right.id));
5344 const value = presenceValidateType(schema.snapshotType, Object.freeze({ members: Object.freeze(members) }), "PRESENCE_STORAGE_INVALID");
5345 if (presenceSseFrameBytes(schema, Object.freeze({ tag: "Snapshot", value })) > PRESENCE_MAX_SNAPSHOT_BYTES) throw presenceFailure("PRESENCE_SNAPSHOT_TOO_LARGE", "Presence snapshot exceeded its bounded SSE transport budget");
5346 return Object.freeze({ value, versions });
5347 }, signal);
5348}
5349
5350function liveRouteSegments(path) {
5351 const normalized = path.length > 1 ? path.replace(/\/+$/, "") : path;
5352 if (normalized === "/") return [];
5353 const values = [];
5354 for (const encoded of normalized.slice(1).split("/")) {
5355 try { values.push(decodeURIComponent(encoded)); }
5356 catch { return null; }
5357 }
5358 return values;
5359}
5360
5361function liveRoutePatternSegments(pattern) {
5362 if (pattern === "/") return [];
5363 return pattern.slice(1).split("/");
5364}
5365
5366function liveRouteParameter(value, type) {
5367 if (type === "String") return value;
5368 if (type === "Int" && /^-?\d+$/.test(value)) {
5369 const parsed = Number(value);
5370 return Number.isSafeInteger(parsed) ? parsed : null;
5371 }
5372 if (type === "Boolean" && (value === "true" || value === "false")) return value === "true";
5373 return null;
5374}
5375
5376function liveRouteInstance(scope, supplied) {
5377 if (typeof supplied !== "string" || !supplied.startsWith("/") || supplied.length > 2048
5378 || liveResourceEncoder.encode(supplied).byteLength > 2048 || supplied.includes("?") || supplied.includes(String.fromCharCode(35))
5379 || /[\u0000-\u001f\u007f]/.test(supplied)) return null;
5380 const actual = liveRouteSegments(supplied);
5381 const expected = liveRoutePatternSegments(scope.pattern);
5382 if (actual === null || expected === null) return null;
5383 if (actual.some((segment) => segment === "." || segment === ".." || /[\u0000-\u001f\u007f]/.test(segment))) return null;
5384 const catchAllIndex = expected.findIndex((segment) => segment.startsWith("{*") && segment.endsWith("}"));
5385 if (catchAllIndex === -1 && actual.length !== expected.length) return null;
5386 if (catchAllIndex !== -1 && (catchAllIndex !== expected.length - 1 || actual.length < expected.length)) return null;
5387 const parameters = new Map(scope.parameters.map((parameter) => [parameter.name, parameter]));
5388 const params = Object.create(null);
5389 const canonicalSegments = [];
5390 for (let index = 0; index < expected.length; index += 1) {
5391 const segment = expected[index];
5392 if (segment.startsWith("{*") && segment.endsWith("}")) {
5393 const name = segment.slice(2, -1);
5394 const parameter = parameters.get(name);
5395 if (!parameter?.catchAll || parameter.type !== "Array<String>") return null;
5396 params[name] = Object.freeze(actual.slice(index));
5397 canonicalSegments.push(...actual.slice(index));
5398 break;
5399 }
5400 if (!segment.startsWith("{") || !segment.endsWith("}")) {
5401 if (segment !== actual[index]) return null;
5402 canonicalSegments.push(actual[index]);
5403 continue;
5404 }
5405 const name = segment.slice(1, -1);
5406 const parameter = parameters.get(name);
5407 if (!parameter || parameter.catchAll) return null;
5408 const converted = liveRouteParameter(actual[index], parameter.type);
5409 if (converted === null) return null;
5410 params[name] = converted;
5411 canonicalSegments.push(parameter.type === "String" ? converted : String(converted));
5412 }
5413 const path = canonicalSegments.length === 0 ? "/" : `/${canonicalSegments.map((segment) => encodeURIComponent(segment)).join("/")}`;
5414 if (liveResourceEncoder.encode(path).byteLength > 2048) return null;
5415 return Object.freeze({ path, params: Object.freeze(params) });
5416}
5417
5418function liveResourceSubscription(request, url) {
5419 if (request.method !== "GET") return { response: failure(405, "LIVE_RESOURCE_METHOD", "Live resource connections require GET", LIVE_RESOURCE_SCHEMA.id, null, { allow: "GET" }) };
5420 const accept = request.headers.get("accept") ?? "";
5421 if (!accept.toLowerCase().split(",").some((value) => value.trim().startsWith("text/event-stream"))) {
5422 return { response: failure(406, "LIVE_RESOURCE_ACCEPT_REQUIRED", "Live resource connections require Accept: text/event-stream", LIVE_RESOURCE_SCHEMA.id) };
5423 }
5424 for (const key of url.searchParams.keys()) {
5425 if (key !== "resource" && key !== "presence") return { response: failure(400, "LIVE_RESOURCE_QUERY_INVALID", "Live connections accept only repeated compiler-owned resource and presence query fields", LIVE_RESOURCE_SCHEMA.id) };
5426 }
5427 const requested = url.searchParams.getAll("resource");
5428 const requestedPresence = url.searchParams.getAll("presence");
5429 if (requested.length > liveResourceSchemas.length || new Set(requested).size !== requested.length
5430 || requestedPresence.length > presenceSchemas.length || new Set(requestedPresence).size !== requestedPresence.length
5431 || requested.length + requestedPresence.length === 0) {
5432 return { response: failure(400, "LIVE_RESOURCE_SET_INVALID", "Live connections require one bounded duplicate-free compiler-owned resource or presence set", LIVE_RESOURCE_SCHEMA.id) };
5433 }
5434 const schemas = [];
5435 const routeId = request.headers.get("x-noxid-route-id");
5436 if (typeof routeId !== "string" || routeId.length === 0 || liveResourceEncoder.encode(routeId).byteLength > 512 || /[\u0000-\u001f\u007f]/.test(routeId)) {
5437 return { response: failure(400, "LIVE_RESOURCE_ROUTE_REQUIRED", "Live resource connections require the compiler-selected route identity", LIVE_RESOURCE_SCHEMA.id) };
5438 }
5439 let routeScope = null;
5440 for (const semanticId of requested) {
5441 const schema = liveResourceSchemaById.get(semanticId);
5442 if (schema === undefined) return { response: failure(403, "LIVE_RESOURCE_NOT_DECLARED", "Live resource subscription is not present in the compiled manifest", semanticId) };
5443 const candidate = schema.routeScopes.find((scope) => scope.id === routeId);
5444 if (candidate === undefined) return { response: failure(403, "LIVE_RESOURCE_ROUTE_DENIED", "Live resource is not exposed by the compiler-selected route", semanticId, { route: routeId }) };
5445 if (routeScope === null) routeScope = candidate;
5446 else if (routeScope.pattern !== candidate.pattern || JSON.stringify(routeScope.parameters) !== JSON.stringify(candidate.parameters) || routeScope.middleware.join("\n") !== candidate.middleware.join("\n")) {
5447 return { response: failure(500, "LIVE_RESOURCE_ROUTE_DRIFT", "Compiled live resources disagree on the selected route middleware contract", semanticId) };
5448 }
5449 schemas.push(schema);
5450 }
5451 const selectedPresence = [];
5452 for (const semanticId of requestedPresence) {
5453 const schema = presenceSchemaById.get(semanticId);
5454 if (schema === undefined) return { response: failure(403, "PRESENCE_NOT_DECLARED", "Presence subscription is not present in the compiled manifest", semanticId) };
5455 const candidate = schema.routeScopes.find((scope) => scope.id === routeId);
5456 if (candidate === undefined) return { response: failure(403, "PRESENCE_ROUTE_DENIED", "Presence is not exposed by the compiler-selected route", semanticId, { route: routeId }) };
5457 if (routeScope === null) routeScope = candidate;
5458 else if (routeScope.pattern !== candidate.pattern || JSON.stringify(routeScope.parameters) !== JSON.stringify(candidate.parameters) || routeScope.middleware.join("\n") !== candidate.middleware.join("\n")) {
5459 return { response: failure(500, "LIVE_RESOURCE_ROUTE_DRIFT", "Compiled live subscriptions disagree on the selected route middleware contract", semanticId) };
5460 }
5461 schemas.push(schema);
5462 selectedPresence.push(schema);
5463 }
5464 schemas.sort((left, right) => left.id.localeCompare(right.id));
5465 selectedPresence.sort((left, right) => left.id.localeCompare(right.id));
5466 const resourceSchemas = Object.freeze(requested.map((id) => liveResourceSchemaById.get(id)).sort((left, right) => left.id.localeCompare(right.id)));
5467 const ids = Object.freeze(resourceSchemas.map((schema) => schema.id));
5468 const presenceIds = Object.freeze(selectedPresence.map((schema) => schema.id));
5469 const route = liveRouteInstance(routeScope, request.headers.get("x-noxid-route-path"));
5470 if (route === null) return { response: failure(400, "LIVE_RESOURCE_ROUTE_PATH_INVALID", "Live connection route path must exactly match its compiled route pattern and typed parameters", LIVE_RESOURCE_SCHEMA.id, { route: routeId }) };
5471 return { schemas: Object.freeze(schemas), resourceSchemas, presenceSchemas: Object.freeze(selectedPresence), ids, presenceIds, routeScope, routePath: route.path, params: route.params, key: `${routeId}\n${route.path}\nresources:${ids.join("\n")}\npresences:${presenceIds.join("\n")}` };
5472}
5473
5474function liveResourcePruneHistories() {
5475 if (liveResourceHistories.size < LIVE_RESOURCE_MAX_HISTORIES) return true;
5476 for (const [token, history] of liveResourceHistories) {
5477 if (!history.completed) continue;
5478 liveResourceHistories.delete(token);
5479 if (liveResourceHistories.size < LIVE_RESOURCE_MAX_HISTORIES) return true;
5480 }
5481 return false;
5482}
5483
5484function liveResourceFreshHistory(principal, subscriptionKey) {
5485 if (!liveResourcePruneHistories()) return null;
5486 let token = liveResourceToken();
5487 while (liveResourceHistories.has(token)) token = liveResourceToken();
5488 const history = { token, principal, subscriptionKey, events: [], bytes: 0, nextSequence: 1, completed: false };
5489 liveResourceHistories.set(token, history);
5490 return history;
5491}
5492
5493function liveResourceResumeHistory(lastEventId, principal, subscriptionKey) {
5494 const cursor = liveResourceCursor(lastEventId);
5495 if (cursor === null) return null;
5496 const history = liveResourceHistories.get(cursor.token);
5497 if (history === undefined || !history.completed || history.principal !== principal || history.subscriptionKey !== subscriptionKey) return null;
5498 const earliest = history.events[0]?.sequence ?? history.nextSequence;
5499 if (cursor.sequence < earliest - 1 || cursor.sequence >= history.nextSequence) return null;
5500 history.completed = false;
5501 liveResourceHistories.delete(history.token);
5502 liveResourceHistories.set(history.token, history);
5503 return Object.freeze({ history, replay: history.events.filter((event) => event.sequence > cursor.sequence) });
5504}
5505
5506function liveResourceRecord(history, semanticId) {
5507 const sequence = history.nextSequence++;
5508 const frame = `id: ${history.token}:${sequence}\nevent: noxid-live-invalidation\ndata: ${JSON.stringify(semanticId)}\n\n`;
5509 const bytes = liveResourceEncoder.encode(frame).byteLength;
5510 const event = Object.freeze({ sequence, semanticId, frame, bytes });
5511 history.events.push(event);
5512 history.bytes += bytes;
5513 while (history.events.length > LIVE_RESOURCE_MAX_EVENTS || history.bytes > LIVE_RESOURCE_MAX_HISTORY_BYTES) {
5514 history.bytes -= history.events.shift().bytes;
5515 }
5516 return event;
5517}
5518
5519async function liveResourceAuthorize(request, schemas, route, environment, executionContext, signal) {
5520 for (const schema of schemas) {
5521 if (signal.aborted) return endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA);
5522 if (schema.capabilities.length > 0 && typeof authorize !== "function") {
5523 return failure(500, "LIVE_RESOURCE_AUTHORIZER_MISSING", "Live resource authorization is not configured", schema.id);
5524 }
5525 for (const capability of schema.capabilities) {
5526 let allowed = false;
5527 try {
5528 allowed = await authorize(Object.freeze({ capability, semanticId: schema.id, traceId: __noxidTraceIdForRequest(request), target: "live-resource", route, request, environment, executionContext, signal })) === true;
5529 } catch {}
5530 if (signal.aborted) return endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA);
5531 if (!allowed) return failure(403, "LIVE_RESOURCE_CAPABILITY_DENIED", "Live resource capability was denied", schema.id, { capability });
5532 }
5533 }
5534 return null;
5535}
5536
5537function presenceValidateEvent(schema, envelope, routeId, routePath) {
5538 const body = envelope?.body;
5539 if (body === null || typeof body !== "object" || Array.isArray(body)
5540 || !["Joined", "Updated", "Left"].includes(body.tag)
5541 || typeof body.memberId !== "string" || !PRESENCE_CREDENTIAL.test(body.memberId)
5542 || !Number.isSafeInteger(body.version) || body.version < 1
5543 || body.routeId !== routeId || body.routePath !== routePath
5544 || Object.keys(body).sort().join("\n") !== "memberId\nrouteId\nroutePath\ntag\nvalue\nversion") {
5545 throw presenceFailure("PRESENCE_EVENT_INVALID", "Presence pub/sub delivered an invalid compiler-owned event");
5546 }
5547 if (body.tag === "Left") {
5548 if (typeof body.value !== "string" || !PRESENCE_CREDENTIAL.test(body.value)) throw presenceFailure("PRESENCE_EVENT_INVALID", "Presence Left requires one opaque member identity");
5549 if (body.memberId !== body.value) throw presenceFailure("PRESENCE_EVENT_INVALID", "Presence Left identity metadata drifted");
5550 return Object.freeze({ event: Object.freeze({ tag: "Left", value: body.value }), memberId: body.memberId, version: body.version });
5551 }
5552 const value = presenceValidateType(schema.memberType, body.value, "PRESENCE_EVENT_INVALID");
5553 if (value.id !== body.memberId) throw presenceFailure("PRESENCE_EVENT_INVALID", "Presence event identity metadata drifted");
5554 return Object.freeze({ event: Object.freeze({ tag: body.tag, value }), memberId: body.memberId, version: body.version });
5555}
5556
5557function liveResourceStream(request, resourceSchemas, presenceStreamSchemas, principal, routeId, routePath, history, replay, middlewareHeaders, deadline) {
5558 let closed = false;
5559 let controller = null;
5560 let heartbeatTimer = null;
5561 const pending = new Set();
5562 const pendingPresence = [];
5563 const replayQueue = [...replay];
5564 let syncPending = false;
5565 const unsubscribe = [];
5566 const trace = tracingMode === "full" ? __noxidTraceContext() : null;
5567
5568 const cleanup = async (closeStream) => {
5569 if (closed) return;
5570 closed = true;
5571 history.completed = true;
5572 if (heartbeatTimer !== null) clearInterval(heartbeatTimer);
5573 deadline.signal.removeEventListener("abort", onAbort);
5574 deadline.release();
5575 await Promise.allSettled(unsubscribe.splice(0).map((stop) => stop()));
5576 if (closeStream) { try { controller?.close(); } catch {} }
5577 };
5578 const send = (event) => {
5579 const startedAt = __noxidPubSubNow();
5580 controller.enqueue(liveResourceEncoder.encode(event.frame));
5581 __noxidTraceEmit(trace, "live.deliver", { semanticId: event.semanticId, state: "delivered", durationMs: Math.max(0, __noxidPubSubNow() - startedAt) });
5582 };
5583 const sendPresence = (entry) => {
5584 const startedAt = __noxidPubSubNow();
5585 let state = "delivered";
5586 let code = null;
5587 try {
5588 const frame = presenceSseFrame(entry.schema, entry.event);
5589 if (liveResourceEncoder.encode(frame).byteLength > PRESENCE_MAX_SNAPSHOT_BYTES) throw presenceFailure("PRESENCE_SNAPSHOT_TOO_LARGE", "Presence event exceeded its bounded SSE transport budget");
5590 controller.enqueue(liveResourceEncoder.encode(frame));
5591 } catch (cause) {
5592 state = "failed";
5593 code = cause?.code ?? "PRESENCE_DELIVERY_FAILED";
5594 throw cause;
5595 } finally {
5596 __noxidTraceEmit(trace, "presence.deliver", { semanticId: entry.schema.id, state, event: entry.event.tag, code, durationMs: Math.max(0, __noxidPubSubNow() - startedAt) });
5597 }
5598 };
5599 const queuePresence = (schema, event) => {
5600 if (event.tag === "Updated") {
5601 let same = -1;
5602 for (let index = pendingPresence.length - 1; index >= 0; index -= 1) {
5603 const entry = pendingPresence[index];
5604 if (entry.schema.id !== schema.id) continue;
5605 const memberId = entry.event.tag === "Left" ? entry.event.value : entry.event.value?.id;
5606 if ((entry.event.tag === "Joined" || entry.event.tag === "Left") && memberId === event.value.id) break;
5607 if (entry.event.tag === "Updated" && memberId === event.value.id) { same = index; break; }
5608 }
5609 if (same >= 0) { pendingPresence[same] = Object.freeze({ schema, event }); flush(); return; }
5610 }
5611 if (pendingPresence.length >= PRESENCE_MAX_MEMBERS) throw presenceFailure("PRESENCE_DELIVERY_CAPACITY", "Presence delivery exceeded its bounded queue; reconnect for a fresh Snapshot");
5612 pendingPresence.push(Object.freeze({ schema, event }));
5613 flush();
5614 };
5615 const flush = () => {
5616 if (closed || controller === null) return;
5617 try {
5618 while ((controller.desiredSize ?? 1) > 0 && replayQueue.length > 0) send(replayQueue.shift());
5619 while ((controller.desiredSize ?? 1) > 0 && pendingPresence.length > 0) sendPresence(pendingPresence.shift());
5620 if ((controller.desiredSize ?? 1) > 0 && syncPending) {
5621 syncPending = false;
5622 controller.enqueue(liveResourceEncoder.encode("event: noxid-live-sync\ndata: null\n\n"));
5623 }
5624 while ((controller.desiredSize ?? 1) > 0 && pending.size > 0) {
5625 const semanticId = pending.values().next().value;
5626 pending.delete(semanticId);
5627 send(liveResourceRecord(history, semanticId));
5628 }
5629 } catch { void cleanup(false); }
5630 };
5631 const onAbort = () => { void cleanup(true); };
5632
5633 const stream = new ReadableStream({
5634 async start(streamController) {
5635 controller = streamController;
5636 deadline.signal.addEventListener("abort", onAbort, { once: true });
5637 heartbeatTimer = setInterval(() => {
5638 if (closed || (controller.desiredSize ?? 1) <= 0) return;
5639 try { controller.enqueue(liveResourceEncoder.encode(": heartbeat\n\n")); } catch { void cleanup(false); }
5640 }, ENDPOINT_STREAM_HEARTBEAT_MS);
5641 heartbeatTimer?.unref?.();
5642 flush();
5643 try {
5644 for (const schema of resourceSchemas) {
5645 const stop = await __noxidPubSubSubscribe("invalidation", schema.id, principal, () => {
5646 pending.add(schema.id);
5647 flush();
5648 });
5649 if (closed) await stop();
5650 else unsubscribe.push(stop);
5651 }
5652 for (const schema of presenceStreamSchemas) {
5653 const buffered = [];
5654 const versions = new Map();
5655 const present = new Set();
5656 let ready = false;
5657 const reconcileDelta = (delta) => {
5658 const known = versions.get(delta.memberId);
5659 if (known !== undefined && delta.version <= known) return null;
5660 versions.set(delta.memberId, delta.version);
5661 if (delta.event.tag === "Left") {
5662 return present.delete(delta.memberId) ? delta.event : null;
5663 }
5664 if (delta.event.tag === "Joined" && present.has(delta.memberId)) {
5665 throw presenceFailure("PRESENCE_RESYNC_REQUIRED", "Presence structural ordering was incomplete; reconnect for a fresh Snapshot");
5666 }
5667 if (delta.event.tag === "Updated" && !present.has(delta.memberId)) {
5668 present.add(delta.memberId);
5669 return Object.freeze({ tag: "Joined", value: delta.event.value });
5670 }
5671 present.add(delta.memberId);
5672 return delta.event;
5673 };
5674 const topicId = await presenceTopicId(schema, routeId, routePath);
5675 const stop = await __noxidPubSubSubscribe("presence", topicId, principal, (envelope) => {
5676 try {
5677 if (envelope?.body?.routeId !== routeId || envelope?.body?.routePath !== routePath) return;
5678 const delta = presenceValidateEvent(schema, envelope, routeId, routePath);
5679 if (!ready) {
5680 if (delta.event.tag === "Updated") {
5681 let same = -1;
5682 for (let index = buffered.length - 1; index >= 0; index -= 1) {
5683 const item = buffered[index];
5684 if ((item.event.tag === "Joined" || item.event.tag === "Left") && item.memberId === delta.memberId) break;
5685 if (item.event.tag === "Updated" && item.memberId === delta.memberId) { same = index; break; }
5686 }
5687 if (same >= 0 && buffered[same].version <= delta.version) { buffered[same] = delta; return; }
5688 }
5689 if (buffered.length >= PRESENCE_MAX_MEMBERS) throw presenceFailure("PRESENCE_SNAPSHOT_RACE_CAPACITY", "Presence changed too quickly while constructing Snapshot; reconnect to retry");
5690 buffered.push(delta);
5691 } else {
5692 const event = reconcileDelta(delta);
5693 if (event !== null) queuePresence(schema, event);
5694 }
5695 } catch (cause) {
5696 void cleanup(false);
5697 try { controller.error(cause); } catch {}
5698 }
5699 });
5700 if (closed) { await stop(); continue; }
5701 unsubscribe.push(stop);
5702 const snapshot = await presenceSnapshot(schema, principal, routeId, routePath, deadline.signal);
5703 for (const [memberId, version] of snapshot.versions) { versions.set(memberId, version); present.add(memberId); }
5704 queuePresence(schema, Object.freeze({ tag: "Snapshot", value: snapshot.value }));
5705 ready = true;
5706 for (const delta of buffered) {
5707 const event = reconcileDelta(delta);
5708 if (event !== null) queuePresence(schema, event);
5709 }
5710 }
5711 // This fence closes both initial-fetch -> first-subscribe and
5712 // disconnect -> resumed-subscribe gaps. Replay remains strictly after
5713 // the supplied cursor; the fence carries no cursor and asks the client
5714 // to validate current state once after every exact set is installed.
5715 syncPending = resourceSchemas.length > 0;
5716 flush();
5717 } catch (cause) {
5718 await cleanup(false);
5719 try { controller.error(cause); } catch {}
5720 }
5721 },
5722 pull() { flush(); },
5723 cancel() { return cleanup(false); },
5724 });
5725 const headers = new Headers({
5726 "content-type": "text/event-stream; charset=utf-8",
5727 "cache-control": "no-store",
5728 "connection": "keep-alive",
5729 "x-accel-buffering": "no",
5730 "x-content-type-options": "nosniff",
5731 });
5732 for (const [name, value] of middlewareHeaders) headers.append(name, value);
5733 return new Response(stream, { status: 200, headers });
5734}
5735
5736function presenceRequestShape(value, operation) {
5737 if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
5738 const allowed = operation === "join"
5739 ? "nonce\noperation\npresence\nrecord\ntoken"
5740 : operation === "update"
5741 ? "memberId\noperation\npresence\nrecord\ntoken"
5742 : "memberId\noperation\npresence\ntoken";
5743 return Object.keys(value).sort().join("\n") === allowed ? value : null;
5744}
5745
5746async function presenceReadRequest(request, signal) {
5747 if (request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
5748 throw presenceFailure("PRESENCE_CONTENT_TYPE_REQUIRED", "Presence writes require Content-Type: application/json");
5749 }
5750 const declared = request.headers.get("content-length");
5751 if (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > 12_288)) throw presenceFailure("PRESENCE_BODY_TOO_LARGE", "Presence writes are bounded to 12288 bytes");
5752 if (request.body === null) throw presenceFailure("PRESENCE_BODY_INVALID", "Presence write body must be valid JSON");
5753 const reader = request.body.getReader();
5754 const chunks = [];
5755 let total = 0;
5756 const cancel = () => { void reader.cancel("presence request cancelled").catch(() => {}); };
5757 signal.addEventListener("abort", cancel, { once: true });
5758 try {
5759 while (true) {
5760 if (signal.aborted) throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence request exceeded its bounded lifetime");
5761 const chunk = await reader.read();
5762 if (signal.aborted) throw presenceFailure("PRESENCE_OPERATION_ABORTED", "Presence request exceeded its bounded lifetime");
5763 if (chunk.done) break;
5764 if (!(chunk.value instanceof Uint8Array)) throw presenceFailure("PRESENCE_BODY_INVALID", "Presence request body stream did not yield bytes");
5765 total += chunk.value.byteLength;
5766 if (total > 12_288) {
5767 await reader.cancel("presence request body too large").catch(() => {});
5768 throw presenceFailure("PRESENCE_BODY_TOO_LARGE", "Presence writes are bounded to 12288 bytes");
5769 }
5770 chunks.push(chunk.value);
5771 }
5772 } finally {
5773 signal.removeEventListener("abort", cancel);
5774 try { reader.releaseLock(); } catch {}
5775 }
5776 const bytes = new Uint8Array(total);
5777 let offset = 0;
5778 for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
5779 let text;
5780 try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
5781 catch (cause) { throw presenceFailure("PRESENCE_BODY_INVALID", "Presence write body must be UTF-8", cause); }
5782 let value;
5783 try { value = JSON.parse(text); }
5784 catch (cause) { throw presenceFailure("PRESENCE_BODY_INVALID", "Presence write body must be valid JSON", cause); }
5785 const operation = value?.operation;
5786 if (!["join", "update", "heartbeat", "leave"].includes(operation) || presenceRequestShape(value, operation) === null) {
5787 throw presenceFailure("PRESENCE_BODY_INVALID", "Presence writes use only the compiler-owned join, update, heartbeat, and leave shapes");
5788 }
5789 return value;
5790}
5791
5792async function presenceEnsureAggregate(schema, canonical, routeId, routePath, candidateId, candidateRecord) {
5793 const members = [];
5794 const prefix = `member:${await presencePartition(schema, canonical, routeId, routePath)}`;
5795 for (const key of await presenceMemberStorage.list(prefix)) {
5796 const raw = await presenceMemberStorage.get(key);
5797 if (raw === null) continue;
5798 const stored = presenceValidateStored(schema, raw, key);
5799 if (stored.principal !== canonical || stored.routeId !== routeId || stored.routePath !== routePath) throw presenceFailure("PRESENCE_STORAGE_INVALID", "Presence aggregate crossed its canonical principal or route partition");
5800 if (stored.expiresAt <= Date.now() || stored.memberId === candidateId) continue;
5801 members.push(presenceValidateType(schema.memberType, Object.freeze({ id: stored.memberId, ...stored.record }), "PRESENCE_STORAGE_INVALID"));
5802 }
5803 members.push(presenceValidateType(schema.memberType, Object.freeze({ id: candidateId, ...candidateRecord }), "PRESENCE_RECORD_INVALID"));
5804 if (members.length > PRESENCE_MAX_MEMBERS) throw presenceFailure("PRESENCE_CAPACITY_EXCEEDED", "Presence partition reached its bounded member capacity");
5805 const snapshot = presenceValidateType(schema.snapshotType, Object.freeze({ members: Object.freeze(members) }), "PRESENCE_RECORD_INVALID");
5806 if (presenceSseFrameBytes(schema, Object.freeze({ tag: "Snapshot", value: snapshot })) > PRESENCE_MAX_SNAPSHOT_BYTES) {
5807 throw presenceFailure("PRESENCE_SNAPSHOT_TOO_LARGE", "Presence write would exceed the bounded aggregate Snapshot transport budget");
5808 }
5809}
5810
5811async function presenceDelta(schema, tag, stored) {
5812 const value = tag === "Left"
5813 ? stored.memberId
5814 : presenceValidateType(schema.memberType, Object.freeze({ id: stored.memberId, ...stored.record }), "PRESENCE_STORAGE_INVALID");
5815 const body = Object.freeze({ tag, value, memberId: stored.memberId, version: stored.version, routeId: stored.routeId, routePath: stored.routePath });
5816 if (liveResourceEncoder.encode(JSON.stringify(body)).byteLength > PRESENCE_MAX_DELTA_BYTES) {
5817 throw presenceFailure("PRESENCE_RECORD_TOO_LARGE", "Presence record exceeds the bounded cross-driver delta budget");
5818 }
5819 try { __noxidPubSubEventFromCanonical("presence", await presenceTopicId(schema, stored.routeId, stored.routePath), stored.principal, body, "presence_size_probe"); }
5820 catch (cause) {
5821 if (cause?.code === "PUBSUB_EVENT_TOO_LARGE") throw presenceFailure("PRESENCE_RECORD_TOO_LARGE", "Presence record exceeds the bounded cross-driver event budget", cause);
5822 throw cause;
5823 }
5824 return body;
5825}
5826
5827async function presencePublishAndClearTombstone(schema, tombstone, signal = null) {
5828 await presencePublishCanonical(schema, tombstone.principal, Object.freeze({ tag: "Left", value: tombstone.memberId, memberId: tombstone.memberId, version: tombstone.version, routeId: tombstone.routeId, routePath: tombstone.routePath }));
5829 await presencePartitionLock(schema, tombstone.principal, tombstone.routeId, tombstone.routePath, async () => {
5830 const key = presenceExpiryKey(tombstone.key);
5831 const current = await presenceExpiryStorage.get(key);
5832 if (current?.state === "left" && current.version === tombstone.version) {
5833 await presenceExpiryStorage.set(key, Object.freeze({ ...current, state: "left-published" }), { ttl: Math.max(60, Math.ceil(schema.ttlMilliseconds * 2 / 1000)) });
5834 }
5835 }, signal);
5836}
5837
5838async function presenceMutate(schema, principal, operationalIdentity, routeId, routePath, body, signal) {
5839 const canonical = principal.canonical;
5840 if (body.operation === "join") {
5841 if (!PRESENCE_CREDENTIAL.test(body.nonce) || !PRESENCE_CREDENTIAL.test(body.token)) throw presenceFailure("PRESENCE_CREDENTIAL_INVALID", "Presence join requires bounded cryptographic nonce and credential fields");
5842 const record = presenceValidateType(schema.recordType, body.record, "PRESENCE_RECORD_INVALID");
5843 const memberId = `member_${(await presenceDigest(schema.id, canonical, routePath, body.nonce)).slice(0, 48)}`;
5844 const tokenDigest = await presenceDigest(schema.id, canonical, routePath, memberId, body.token);
5845 for (;;) {
5846 const outcome = await presencePartitionLock(schema, canonical, routeId, routePath, async () => {
5847 const memberKey = await presenceMemberKey(schema, canonical, routeId, routePath, memberId);
5848 const indexKey = presenceExpiryKey(memberKey);
5849 const marker = await presenceExpiryStorage.get(indexKey);
5850 const trustedMarker = marker === null ? null : presenceValidateIndex(marker, indexKey);
5851 if (trustedMarker !== null && (trustedMarker.principal !== canonical || trustedMarker.routeId !== routeId || trustedMarker.routePath !== routePath || trustedMarker.presence !== schema.id || trustedMarker.memberId !== memberId)) throw presenceFailure("PRESENCE_INDEX_INVALID", "Presence join marker crossed its compiler-owned partition");
5852 if (trustedMarker?.state === "left") return Object.freeze({ tombstone: trustedMarker });
5853 const version = trustedMarker?.state === "left-published" ? trustedMarker.version + 1 : trustedMarker?.state === "active" ? trustedMarker.version : 1;
5854 const raw = await presenceMemberStorage.get(memberKey);
5855 if (raw !== null) {
5856 const stored = presenceValidateStored(schema, raw, memberKey);
5857 if (stored.principal !== canonical || stored.routeId !== routeId || stored.routePath !== routePath || stored.nonce !== body.nonce || !presenceDigestEqual(stored.tokenDigest, tokenDigest)) throw presenceFailure("PRESENCE_CREDENTIAL_DENIED", "Presence join credentials do not match the compiler-owned membership");
5858 if (stored.expiresAt <= Date.now()) {
5859 const tombstone = Object.freeze({ schema: PRESENCE_INDEX_SCHEMA, key: memberKey, presence: schema.id, principal: canonical, routeId, routePath, memberId, version: stored.version + 1, expiresAt: stored.expiresAt, state: "left" });
5860 await presenceExpiryStorage.set(presenceExpiryKey(memberKey), tombstone);
5861 await presenceMemberStorage.delete(memberKey);
5862 return Object.freeze({ tombstone, nextVersion: tombstone.version + 1 });
5863 }
5864 await presenceEnsureAggregate(schema, canonical, routeId, routePath, memberId, record);
5865 const changed = JSON.stringify(record) !== JSON.stringify(stored.record);
5866 const resumed = trustedMarker?.state === "left-published";
5867 const interrupted = trustedMarker?.state === "active" && trustedMarker.version > stored.version;
5868 const nextVersion = Math.max(interrupted ? trustedMarker.version : 0, resumed ? version : changed ? stored.version + 1 : stored.version);
5869 const publishTag = resumed || interrupted ? "Joined" : changed ? "Updated" : "Joined";
5870 const next = Object.freeze({ ...stored, record, version: nextVersion, expiresAt: Date.now() + schema.ttlMilliseconds });
5871 await presenceDelta(schema, publishTag, next);
5872 await presenceWriteStored(schema, next, memberKey);
5873 return Object.freeze({ stored: next, publishTag });
5874 }
5875 await presenceEnsureAggregate(schema, canonical, routeId, routePath, memberId, record);
5876 const stored = Object.freeze({ schema: PRESENCE_STORAGE_SCHEMA, presence: schema.id, principal: canonical, routeId, routePath, memberId, nonce: body.nonce, tokenDigest, record, version, expiresAt: Date.now() + schema.ttlMilliseconds });
5877 await presenceDelta(schema, "Joined", stored);
5878 await presenceWriteStored(schema, stored, memberKey);
5879 return Object.freeze({ stored, publishTag: "Joined" });
5880 }, signal);
5881 if (outcome.tombstone) {
5882 await presencePublishAndClearTombstone(schema, outcome.tombstone, signal);
5883 continue;
5884 }
5885 await presencePublish(schema, principal, await presenceDelta(schema, outcome.publishTag, outcome.stored));
5886 return Object.freeze({ memberId });
5887 }
5888 }
5889
5890 if (!PRESENCE_CREDENTIAL.test(body.memberId) || !PRESENCE_CREDENTIAL.test(body.token)) throw presenceFailure("PRESENCE_CREDENTIAL_INVALID", "Presence mutation requires bounded opaque member credentials");
5891 const memberKey = await presenceMemberKey(schema, canonical, routeId, routePath, body.memberId);
5892 const tokenDigest = await presenceDigest(schema.id, canonical, routePath, body.memberId, body.token);
5893 const outcome = await presencePartitionLock(schema, canonical, routeId, routePath, async () => {
5894 const indexKey = presenceExpiryKey(memberKey);
5895 const rawIndex = await presenceExpiryStorage.get(indexKey);
5896 const index = rawIndex === null ? null : presenceValidateIndex(rawIndex, indexKey);
5897 if (index !== null && (index.principal !== canonical || index.routeId !== routeId || index.routePath !== routePath || index.presence !== schema.id || index.memberId !== body.memberId)) throw presenceFailure("PRESENCE_INDEX_INVALID", "Presence mutation marker crossed its compiler-owned partition");
5898 const raw = await presenceMemberStorage.get(memberKey);
5899 if (index?.state === "left" || index?.state === "left-published") {
5900 if (raw !== null) {
5901 const stale = presenceValidateStored(schema, raw, memberKey);
5902 if (!presenceDigestEqual(stale.tokenDigest, tokenDigest)) throw presenceFailure("PRESENCE_CREDENTIAL_DENIED", "Presence credentials do not own this route-bound membership");
5903 await presenceMemberStorage.delete(memberKey);
5904 }
5905 return Object.freeze({ expired: true, tombstone: index.state === "left" ? index : null });
5906 }
5907 if (raw === null) return Object.freeze({ expired: true });
5908 const stored = presenceValidateStored(schema, raw, memberKey);
5909 if (stored.principal !== canonical || stored.routeId !== routeId || stored.routePath !== routePath || !presenceDigestEqual(stored.tokenDigest, tokenDigest)) throw presenceFailure("PRESENCE_CREDENTIAL_DENIED", "Presence credentials do not own this route-bound membership");
5910 await presenceAdmitRate(operationalIdentity, "member", PRESENCE_MEMBER_WRITES_PER_MINUTE, [schema.id, routeId, routePath, stored.memberId]);
5911 if (stored.expiresAt <= Date.now()) {
5912 const tombstone = Object.freeze({ schema: PRESENCE_INDEX_SCHEMA, key: memberKey, presence: schema.id, principal: canonical, routeId, routePath, memberId: stored.memberId, version: stored.version + 1, expiresAt: stored.expiresAt, state: "left" });
5913 await presenceExpiryStorage.set(presenceExpiryKey(memberKey), tombstone);
5914 await presenceMemberStorage.delete(memberKey);
5915 return Object.freeze({ expired: true, tombstone });
5916 }
5917 if (body.operation === "leave") {
5918 const tombstone = Object.freeze({ schema: PRESENCE_INDEX_SCHEMA, key: memberKey, presence: schema.id, principal: canonical, routeId, routePath, memberId: stored.memberId, version: stored.version + 1, expiresAt: stored.expiresAt, state: "left" });
5919 await presenceExpiryStorage.set(presenceExpiryKey(memberKey), tombstone);
5920 await presenceMemberStorage.delete(memberKey);
5921 return Object.freeze({ tombstone });
5922 }
5923 const nextRecord = body.operation === "update"
5924 ? presenceValidateType(schema.recordType, body.record, "PRESENCE_RECORD_INVALID")
5925 : stored.record;
5926 if (body.operation === "update") await presenceEnsureAggregate(schema, canonical, routeId, routePath, stored.memberId, nextRecord);
5927 const unchanged = body.operation === "update" && JSON.stringify(nextRecord) === JSON.stringify(stored.record);
5928 const next = Object.freeze({ ...stored, record: nextRecord, version: unchanged ? stored.version : stored.version + 1, expiresAt: Date.now() + schema.ttlMilliseconds });
5929 if (body.operation === "update") await presenceDelta(schema, "Updated", next);
5930 await presenceWriteStored(schema, next, memberKey);
5931 return Object.freeze({ stored: next, publish: body.operation === "update" });
5932 }, signal);
5933 if (outcome.tombstone) await presencePublishAndClearTombstone(schema, outcome.tombstone, signal);
5934 if (outcome.expired) throw presenceFailure("PRESENCE_MEMBERSHIP_EXPIRED", "Presence membership expired; reconnect for a fresh Snapshot and join again");
5935 if (outcome.publish) await presencePublish(schema, principal, await presenceDelta(schema, "Updated", outcome.stored));
5936 return Object.freeze({});
5937}
5938
5939async function handlePresenceWriteRequest(request, url, environment, executionContext) {
5940 if (request.method !== "POST") return failure(405, "PRESENCE_METHOD", "Presence writes require POST", LIVE_RESOURCE_SCHEMA.id, null, { allow: "POST" });
5941 if (url.search !== "") return failure(400, "PRESENCE_QUERY_INVALID", "Presence writes do not accept query fields", LIVE_RESOURCE_SCHEMA.id);
5942 const controller = new AbortController();
5943 const abortPresenceRequest = () => controller.abort("Presence request was cancelled");
5944 request.signal.addEventListener("abort", abortPresenceRequest, { once: true });
5945 if (request.signal.aborted) abortPresenceRequest();
5946 const timer = setTimeout(() => controller.abort("Presence write timed out"), LIVE_RESOURCE_SCHEMA.timeoutMs);
5947 timer?.unref?.();
5948 let schema = null;
5949 try {
5950 const body = await presenceReadRequest(request, controller.signal);
5951 schema = presenceSchemaById.get(body.presence) ?? null;
5952 if (schema === null) return failure(403, "PRESENCE_NOT_DECLARED", "Presence write is not present in the compiled manifest", body.presence ?? null);
5953 const routeId = request.headers.get("x-noxid-route-id");
5954 if (typeof routeId !== "string" || routeId.length === 0 || liveResourceEncoder.encode(routeId).byteLength > 512 || /[\u0000-\u001f\u007f]/.test(routeId)) return failure(400, "LIVE_RESOURCE_ROUTE_REQUIRED", "Presence writes require the compiler-selected route identity", schema.id);
5955 const routeScope = schema.routeScopes.find((scope) => scope.id === routeId);
5956 if (routeScope === undefined) return failure(403, "PRESENCE_ROUTE_DENIED", "Presence is not exposed by the compiler-selected route", schema.id, { route: routeId });
5957 const route = liveRouteInstance(routeScope, request.headers.get("x-noxid-route-path"));
5958 if (route === null) return failure(400, "LIVE_RESOURCE_ROUTE_PATH_INVALID", "Presence route path must exactly match its compiled route pattern and typed parameters", schema.id, { route: routeId });
5959 const routeSchema = Object.freeze({ ...LIVE_RESOURCE_SCHEMA, id: routeScope.id, path: routeScope.pattern, method: "POST", middleware: routeScope.middleware });
5960 const middleware = await applyEndpointMiddleware(request, routeSchema, route.params, Object.freeze({ presence: schema.id, operation: body.operation }), environment, executionContext, controller.signal);
5961 if (controller.signal.aborted) return endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA);
5962 if (middleware.response) return endpointResponseWithHeaders(middleware.response, middleware.headers);
5963 const denied = await liveResourceAuthorize(request, [schema], middleware.route, environment, executionContext, controller.signal);
5964 if (denied) return endpointResponseWithHeaders(denied, middleware.headers);
5965 const principal = __noxidLiveConnectionPrincipal(middleware.context, environment);
5966 const operationalIdentity = presenceOperationalIdentity(environment, middleware.context, principal);
5967 if (operationalIdentity === null) return failure(403, "PRESENCE_RATE_IDENTITY_REQUIRED", "Public presence writes require a trusted request identity for operational abuse controls", schema.id);
5968 await presenceAdmitRate(operationalIdentity, "abuse", PRESENCE_ABUSE_ATTEMPTS_PER_MINUTE);
5969 if (body.operation === "join") await presenceAdmitRate(operationalIdentity, "join", PRESENCE_JOINS_PER_MINUTE);
5970 const result = await presenceMutate(schema, principal, operationalIdentity, routeId, route.path, body, controller.signal);
5971 if (controller.signal.aborted) return endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA);
5972 return endpointResponseWithHeaders(json(200, Object.freeze({ ok: true, ...result })), middleware.headers);
5973 } catch (cause) {
5974 if (controller.signal.aborted || cause?.code === "PRESENCE_OPERATION_ABORTED") return endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA);
5975 const clientCodes = new Set(["PRESENCE_CREDENTIAL_INVALID", "PRESENCE_RECORD_INVALID", "PRESENCE_RECORD_TOO_LARGE", "PRESENCE_SNAPSHOT_TOO_LARGE", "PRESENCE_CAPACITY_EXCEEDED"]);
5976 const status = cause?.code === "PRESENCE_BODY_TOO_LARGE" ? 413
5977 : cause?.code === "PRESENCE_BODY_INVALID" || cause?.code === "PRESENCE_CONTENT_TYPE_REQUIRED" ? 400
5978 : cause?.code === "PRESENCE_CREDENTIAL_DENIED" ? 403
5979 : cause?.code === "PRESENCE_MEMBERSHIP_EXPIRED" ? 409
5980 : cause?.code === "PRESENCE_RATE_LIMITED" ? 429
5981 : cause?.code === "PRESENCE_STORAGE_UNAVAILABLE" ? 503
5982 : clientCodes.has(cause?.code) ? 422 : 500;
5983 return failure(status, cause?.code ?? "PRESENCE_WRITE_FAILED", cause?.message ?? "Presence write failed", schema?.id ?? LIVE_RESOURCE_SCHEMA.id, null, cause?.retryAfter === undefined ? {} : { "retry-after": String(cause.retryAfter) });
5984 } finally {
5985 clearTimeout(timer);
5986 request.signal.removeEventListener("abort", abortPresenceRequest);
5987 }
5988}
5989
5990async function handleLiveResourceRequest(request, url, environment, executionContext) {
5991 if (url.pathname === presenceWritePath) return handlePresenceWriteRequest(request, url, environment, executionContext);
5992 if (url.pathname !== liveResourcePath) return null;
5993 const subscription = liveResourceSubscription(request, url);
5994 if (subscription.response) return subscription.response;
5995 const deadlineController = new AbortController();
5996 let deadlineReleased = false;
5997 let streamOwnsDeadline = false;
5998 let resolveDeadline;
5999 const deadlineResponse = new Promise((resolve) => { resolveDeadline = resolve; });
6000 const expire = () => {
6001 if (deadlineReleased) return;
6002 deadlineController.abort("Live resource connection lifetime ended");
6003 resolveDeadline(endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA));
6004 };
6005 const deadlineTimer = setTimeout(expire, LIVE_RESOURCE_SCHEMA.timeoutMs);
6006 deadlineTimer?.unref?.();
6007 request.signal.addEventListener("abort", expire, { once: true });
6008 const deadline = Object.freeze({
6009 signal: deadlineController.signal,
6010 release() {
6011 if (deadlineReleased) return;
6012 deadlineReleased = true;
6013 clearTimeout(deadlineTimer);
6014 request.signal.removeEventListener("abort", expire);
6015 },
6016 });
6017 const setup = async () => {
6018 const routeSchema = Object.freeze({ ...LIVE_RESOURCE_SCHEMA, id: subscription.routeScope.id, path: subscription.routeScope.pattern, middleware: subscription.routeScope.middleware });
6019 const middleware = await applyEndpointMiddleware(request, routeSchema, subscription.params, Object.freeze({ resource: subscription.ids, presence: subscription.presenceIds }), environment, executionContext, deadline.signal);
6020 if (deadline.signal.aborted) return endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA);
6021 if (middleware.response) return endpointResponseWithHeaders(middleware.response, middleware.headers);
6022 const authorizationFailure = await liveResourceAuthorize(request, subscription.schemas, middleware.route, environment, executionContext, deadline.signal);
6023 if (deadline.signal.aborted) return endpointTimeoutFailure(LIVE_RESOURCE_SCHEMA);
6024 if (authorizationFailure) return endpointResponseWithHeaders(authorizationFailure, middleware.headers);
6025 const principal = __noxidLiveConnectionPrincipal(middleware.context, environment);
6026 const suppliedCursor = request.headers.get("last-event-id");
6027 if (suppliedCursor !== null) {
6028 const resumed = liveResourceResumeHistory(suppliedCursor, principal.canonical, subscription.key);
6029 if (resumed === null) return liveResourceResetResponse(middleware.headers);
6030 const response = liveResourceStream(request, subscription.resourceSchemas, subscription.presenceSchemas, principal, subscription.routeScope.id, subscription.routePath, resumed.history, resumed.replay, middleware.headers, deadline);
6031 streamOwnsDeadline = true;
6032 return response;
6033 }
6034 const history = liveResourceFreshHistory(principal.canonical, subscription.key);
6035 if (history === null) return failure(503, "LIVE_RESOURCE_CAPACITY", "Live resource replay capacity is temporarily exhausted", LIVE_RESOURCE_SCHEMA.id);
6036 const response = liveResourceStream(request, subscription.resourceSchemas, subscription.presenceSchemas, principal, subscription.routeScope.id, subscription.routePath, history, [], middleware.headers, deadline);
6037 streamOwnsDeadline = true;
6038 return response;
6039 };
6040 try { return await Promise.race([setup(), deadlineResponse]); }
6041 finally { if (!streamOwnsDeadline) deadline.release(); }
6042}
6043"##;
6044
6045const HANDLER_RUNTIME: &str = r#"/* noxid-server:live-invalidation */
6046
6047function json(status, body, headers = {}) {
6048 return new Response(JSON.stringify(body), {
6049 status,
6050 headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff", ...headers },
6051 });
6052}
6053
6054function failure(status, code, message, semanticId = null, details = null, headers = {}, traceable = true) {
6055 const response = json(status, { ok: false, error: { code, message, semanticId, details } }, headers);
6056 return traceable ? __noxidTraceFailure(response, code, semanticId, details?.capability) : response;
6057}
6058
6059const SSR_MIDDLEWARE_STATE = Symbol.for("noxid.ssr.middleware.state");
6060const EMPTY_MIDDLEWARE_CONTEXT = Object.freeze({});
6061function inheritedMiddlewareState(scope, executionContext) {
6062 const state = executionContext?.[SSR_MIDDLEWARE_STATE];
6063 if (!state || state.routeId !== scope.id || !Array.isArray(state.applied)) return null;
6064 const expected = [...globalMiddleware, ...scope.middleware];
6065 if (state.applied.length !== expected.length || !expected.every((name, index) => state.applied[index] === name)) return null;
6066 return state;
6067}
6068
6069function splitGeneric(type, prefix) {
6070 if (!type.startsWith(`${prefix}<`) || !type.endsWith(">")) return null;
6071 return type.slice(prefix.length + 1, -1);
6072}
6073
6074function splitGenericPair(type, prefix) {
6075 const body = splitGeneric(type, prefix);
6076 if (body === null) return null;
6077 let depth = 0;
6078 for (let index = 0; index < body.length; index += 1) {
6079 if (body[index] === "<") depth += 1;
6080 else if (body[index] === ">") depth -= 1;
6081 else if (body[index] === "," && depth === 0) return [body.slice(0, index).trim(), body.slice(index + 1).trim()];
6082 }
6083 return null;
6084}
6085
6086function valid(value) { return { value }; }
6087function invalid(issue, details = null) { return { issue, details }; }
6088
6089function validateType(type, value, path, typeId = null, external = false, ancestors = new WeakSet()) {
6090 if (type === "String") return typeof value === "string" ? valid(value) : invalid(`${path} must be String`);
6091 if (type === "Boolean") return typeof value === "boolean" ? valid(value) : invalid(`${path} must be Boolean`);
6092 if (type === "Int") return Number.isSafeInteger(value) ? valid(value) : invalid(`${path} must be Int`);
6093 if (type === "Number" || type === "Float") return typeof value === "number" && Number.isFinite(value) ? valid(value) : invalid(`${path} must be ${type}`);
6094 if (type === "Date") return typeof value === "string" && (external ? queueUtcInstant(value) !== null : !Number.isNaN(Date.parse(value))) ? valid(value) : invalid(`${path} must be a UTC ISO Date string`);
6095 const optional = splitGeneric(type, "Optional");
6096 if (optional !== null) return value == null ? valid(external ? null : value) : validateType(optional, value, path, typeId, external, ancestors);
6097 const array = splitGeneric(type, "Array");
6098 if (array !== null) {
6099 if (!Array.isArray(value)) return invalid(`${path} must be ${type}`);
6100 if (ancestors.has(value)) return invalid(`${path} must be acyclic ordinary data`);
6101 ancestors.add(value);
6102 const trusted = [];
6103 try {
6104 const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
6105 if (!lengthDescriptor || !("value" in lengthDescriptor) || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0) return invalid(`${path} must be an ordinary dense Array`);
6106 const length = lengthDescriptor.value;
6107 const keys = Reflect.ownKeys(value);
6108 if (external && keys.some((key) => key !== "length" && (typeof key !== "string" || !/^(0|[1-9]\d*)$/.test(key) || Number(key) >= length))) return invalid(`${path} must not contain undeclared array properties`);
6109 for (let index = 0; index < length; index += 1) {
6110 const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
6111 if (descriptor !== undefined && (!("value" in descriptor) || !descriptor.enumerable)) return invalid(`${path}[${index}] must be own enumerable array data`);
6112 const result = validateType(array, descriptor?.value, `${path}[${index}]`, typeId, external, ancestors);
6113 if (result.issue) return result;
6114 trusted.push(result.value);
6115 }
6116 return valid(Object.freeze(trusted));
6117 } catch {
6118 return invalid(`${path} must be an ordinary dense Array`);
6119 } finally {
6120 ancestors.delete(value);
6121 }
6122 }
6123 const map = splitGenericPair(type, "Map");
6124 if (map !== null && map[0] === "String") {
6125 let keys;
6126 try {
6127 if (value === null || typeof value !== "object" || Array.isArray(value)) return invalid(`${path} must be ${type}`);
6128 const prototype = Object.getPrototypeOf(value);
6129 if (prototype !== Object.prototype && prototype !== null) return invalid(`${path} must be ${type}`);
6130 keys = Reflect.ownKeys(value);
6131 } catch { return invalid(`${path} must be ${type}`); }
6132 if (keys.some((key) => typeof key !== "string")) return invalid(`${path} must contain String keys`);
6133 const trusted = Object.create(null);
6134 for (const key of keys.sort()) {
6135 if (key === "__proto__" || key === "constructor" || key === "prototype") return invalid(`${path}.${key} must use a safe String map key`);
6136 let descriptor;
6137 try { descriptor = Object.getOwnPropertyDescriptor(value, key); } catch {}
6138 if (descriptor === undefined || !("value" in descriptor) || !descriptor.enumerable) return invalid(`${path}.${key} must be ordinary data`);
6139 const result = validateType(map[1], descriptor.value, `${path}.${key}`, typeId, external, ancestors);
6140 if (result.issue) return result;
6141 trusted[key] = result.value;
6142 }
6143 return valid(Object.freeze(trusted));
6144 }
6145 const resultType = splitGenericPair(type, "Result");
6146 if (resultType !== null) {
6147 let keys, tag, payload;
6148 try {
6149 if (value === null || typeof value !== "object" || Array.isArray(value)) return invalid(`${path} must be ${type}`);
6150 const prototype = Object.getPrototypeOf(value);
6151 if (prototype !== Object.prototype && prototype !== null) return invalid(`${path} must be ${type}`);
6152 keys = Reflect.ownKeys(value);
6153 const tagDescriptor = Object.getOwnPropertyDescriptor(value, "tag");
6154 const valueDescriptor = Object.getOwnPropertyDescriptor(value, "value");
6155 if (!tagDescriptor || !("value" in tagDescriptor) || !tagDescriptor.enumerable || !valueDescriptor || !("value" in valueDescriptor) || !valueDescriptor.enumerable) return invalid(`${path} must be an Ok(value) or Err(error) result`);
6156 tag = tagDescriptor.value;
6157 payload = valueDescriptor.value;
6158 } catch { return invalid(`${path} must be ${type}`); }
6159 if (!keys.every((key) => key === "tag" || key === "value") || (tag !== "Ok" && tag !== "Err")) return invalid(`${path} must be an Ok(value) or Err(error) result`);
6160 const inner = tag === "Ok" ? resultType[0] : resultType[1];
6161 const checked = validateType(inner, payload, `${path}.${tag}`, typeId, external, ancestors);
6162 return checked.issue ? checked : valid(Object.freeze({ tag, value: checked.value }));
6163 }
6164 const validatorId = typeId !== null && typeof typeId === "object" ? typeId[type] ?? null : typeId;
6165 const validator = validatorId === null ? null : typeValidators[validatorId];
6166 if (typeof validator === "function") {
6167 try { return valid(validator(value, external)); }
6168 catch (cause) {
6169 return invalid(`${path} must satisfy ${type}`, typeof cause?.toJSON === "function" ? cause.toJSON() : null);
6170 }
6171 }
6172 return invalid(`${path} uses unsupported boundary type ${type}`);
6173}
6174
6175const ACTION_BODY_MAX_BYTES = 1_048_576;
6176
6177// The action body is read off the stream with a hard byte ceiling, never
6178// buffered whole and measured afterwards. `Content-Length` is a claim the
6179// caller makes: it can be absent (a chunked request) or a lie, so it is only
6180// ever an early refusal and never the enforcement. Reading stops the moment
6181// one byte past the cap arrives, and the reader is cancelled, so a request
6182// that promises 10 bytes and sends 96 MiB costs the cap, not the body.
6183async function readBoundedActionBody(request) {
6184 if (request.body === null) return { text: "" };
6185 const reader = request.body.getReader();
6186 const chunks = [];
6187 let total = 0;
6188 try {
6189 while (true) {
6190 const chunk = await reader.read();
6191 if (chunk.done) break;
6192 if (!(chunk.value instanceof Uint8Array)) return { invalid: true };
6193 total += chunk.value.byteLength;
6194 if (total > ACTION_BODY_MAX_BYTES) return { tooLarge: true };
6195 chunks.push(chunk.value);
6196 }
6197 }
6198 catch { return { invalid: true }; }
6199 // Never awaited: when the caller cloned the request the body is a `tee`
6200 // branch, and a branch's `cancel()` does not settle until every branch has
6201 // let go. Awaiting it here deadlocks the refusal it exists to deliver.
6202 finally { try { void reader.cancel().catch(() => {}); } catch {} }
6203 const bytes = new Uint8Array(total);
6204 let offset = 0;
6205 for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
6206 return { text: new TextDecoder().decode(bytes) };
6207}
6208
6209async function decodeArguments(request, schema) {
6210 const contentType = request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
6211 if (contentType !== "application/json") return { error: failure(415, "BOUNDARY_CONTENT_TYPE", "Noxid action requests require application/json", schema.id) };
6212 const declaredLength = Number(request.headers.get("content-length") ?? 0);
6213 if (Number.isFinite(declaredLength) && declaredLength > ACTION_BODY_MAX_BYTES) return { error: failure(413, "BOUNDARY_BODY_TOO_LARGE", "Noxid action request exceeds 1 MiB", schema.id) };
6214 const bounded = await readBoundedActionBody(request);
6215 if (bounded.tooLarge === true) return { error: failure(413, "BOUNDARY_BODY_TOO_LARGE", "Noxid action request exceeds 1 MiB", schema.id) };
6216 if (bounded.invalid === true) return { error: failure(400, "BOUNDARY_BODY_INVALID", "Request body is not valid JSON", schema.id) };
6217 let body;
6218 try { body = JSON.parse(bounded.text); }
6219 catch { return { error: failure(400, "BOUNDARY_BODY_INVALID", "Request body is not valid JSON", schema.id) }; }
6220 if (!body || typeof body !== "object" || Array.isArray(body) || !body.arguments || typeof body.arguments !== "object" || Array.isArray(body.arguments)) {
6221 return { error: failure(400, "BOUNDARY_BODY_INVALID", "Request body must contain an arguments object", schema.id) };
6222 }
6223 const expected = new Set(schema.parameters.map((parameter) => parameter.name));
6224 const unexpected = Object.keys(body.arguments).filter((name) => !expected.has(name));
6225 if (unexpected.length) return { error: failure(400, "BOUNDARY_ARGUMENT_UNKNOWN", `Unknown action argument: ${unexpected[0]}`, schema.id, { argument: unexpected[0] }) };
6226 const values = Object.create(null);
6227 for (const parameter of schema.parameters) {
6228 if (!Object.hasOwn(body.arguments, parameter.name)) return { error: failure(400, "BOUNDARY_ARGUMENT_MISSING", `Missing action argument: ${parameter.name}`, schema.id, { argument: parameter.name }) };
6229 const result = validateType(parameter.type, body.arguments[parameter.name], `arguments.${parameter.name}`, parameter.typeId);
6230 if (result.issue) return { error: failure(422, result.issue.includes("unsupported boundary type") ? "BOUNDARY_SCHEMA_UNSUPPORTED" : "BOUNDARY_ARGUMENT_TYPE", result.issue, schema.id, { argument: parameter.name, expected: parameter.type, validation: result.details }) };
6231 values[parameter.name] = result.value;
6232 }
6233 return { arguments: Object.freeze(values) };
6234}
6235
6236function resolveRouteScope(request, schema) {
6237 const routeId = request.headers.get("x-noxid-route-id");
6238 if (typeof routeId !== "string" || routeId.length === 0) {
6239 return { error: failure(400, "BOUNDARY_ROUTE_REQUIRED", "Noxid action requests require a compiled route identity", schema.id) };
6240 }
6241 const scope = schema.routeScopes.find((candidate) => candidate.id === routeId);
6242 if (!scope) {
6243 return { error: failure(403, "BOUNDARY_ROUTE_DENIED", "Action is not exposed by the requested route", schema.id, { route: routeId }) };
6244 }
6245 return { scope };
6246}
6247
6248function withMiddlewareHeaders(response, pairs) {
6249 if (!pairs || pairs.length === 0) return response;
6250 const headers = new Headers(response.headers);
6251 for (const [name, value] of pairs) headers.append(name, value);
6252 return __noxidTraceCopyFailure(response, new Response(response.body, { status: response.status, headers }));
6253}
6254async function applyMiddleware(request, schema, scope, environment, executionContext) {
6255 const headers = [];
6256 const inherited = inheritedMiddlewareState(scope, executionContext);
6257 if (inherited) return { failure: null, headers, context: inherited.context };
6258 const context = Object.create(null);
6259 const chain = [
6260 ...globalMiddleware.map((name) => Object.freeze({ name, handle: globalMiddlewareHandlers[name] })),
6261 ...scope.middleware.map((name) => Object.freeze({ name, handle: middlewareHandlers[name] })),
6262 ];
6263 const names = chain.map((entry) => entry.name);
6264 const route = Object.freeze({ ...scope, middleware: Object.freeze(names) });
6265 __noxidTraceRoute(request, route.pattern);
6266 for (const { name, handle } of chain) {
6267 __noxidTraceSemantic(request, "middleware", name.startsWith("middleware:") ? name : `middleware:${name}`);
6268 if (typeof handle !== "function") {
6269 return { failure: failure(500, "BOUNDARY_MIDDLEWARE_MISSING", "Required server middleware is not available", schema.id, { middleware: name, route: scope.id }), headers };
6270 }
6271 let result;
6272 try {
6273 result = await handle(__noxidDataContext({
6274 middleware: name,
6275 semanticId: schema.id,
6276 traceId: __noxidTraceIdForRequest(request),
6277 boundary: schema.boundary,
6278 target: schema.target,
6279 route,
6280 request,
6281 url: new URL(request.url),
6282 host: hostModule,
6283 environment,
6284 executionContext,
6285 context: Object.freeze({ ...context }),
6286 middlewareContext: Object.freeze({ ...context }),
6287 }, __noxidPrincipal(context, environment, __noxidAgentForRequest(request))));
6288 } catch {
6289 return { failure: failure(500, "BOUNDARY_MIDDLEWARE_FAILED", "Server middleware failed", schema.id, { middleware: name, route: scope.id }), headers };
6290 }
6291 const normalized = __noxidNormalizeMiddlewareResult(result);
6292 if (normalized.issue === "headers") {
6293 return { failure: failure(500, "BOUNDARY_MIDDLEWARE_HEADERS", "Server middleware returned a disallowed response header", schema.id, { middleware: name, route: scope.id, header: normalized.detail }), headers: [] };
6294 }
6295 if (normalized.issue === "response") {
6296 return { failure: failure(500, "BOUNDARY_MIDDLEWARE_RESPONSE", "Server middleware returned an invalid direct response", schema.id, { middleware: name, route: scope.id, validation: normalized.detail }), headers };
6297 }
6298 if (normalized.issue === "context") {
6299 return { failure: failure(500, "BOUNDARY_MIDDLEWARE_CONTEXT", "Server middleware returned invalid context", schema.id, { middleware: name, route: scope.id, validation: normalized.detail }), headers };
6300 }
6301 headers.push(...normalized.headers);
6302 if (normalized.respond !== null) {
6303 return { failure: failure(409, "BOUNDARY_MIDDLEWARE_RESPONSE", "Server action middleware cannot produce a direct response; respond belongs to SSR routes", schema.id, { middleware: name, route: scope.id }), headers };
6304 }
6305 if (normalized.redirect !== null) {
6306 return { failure: failure(409, "BOUNDARY_MIDDLEWARE_REDIRECT", "Server action middleware requested navigation", schema.id, { middleware: name, route: scope.id, redirect: normalized.redirect }), headers };
6307 }
6308 if (!normalized.allow) {
6309 return { failure: failure(403, "BOUNDARY_MIDDLEWARE_DENIED", "Server middleware denied the action", schema.id, { middleware: name, route: scope.id }), headers };
6310 }
6311 if (normalized.context !== null) Object.assign(context, normalized.context);
6312 }
6313 return { failure: null, headers, context: Object.freeze({ ...context }) };
6314}
6315
6316async function authorizeAction(request, schema, scope, environment, executionContext) {
6317 if (schema.capabilities.length === 0) return null;
6318 if (typeof authorize !== "function") {
6319 return failure(500, "BOUNDARY_AUTHORIZER_MISSING", "Action authorization is not configured", schema.id);
6320 }
6321 for (const capability of schema.capabilities) {
6322 let allowed = false;
6323 try {
6324 allowed = await authorize(Object.freeze({
6325 capability,
6326 semanticId: schema.id,
6327 traceId: __noxidTraceIdForRequest(request),
6328 target: schema.target,
6329 route: scope,
6330 request,
6331 environment,
6332 executionContext,
6333 })) === true;
6334 } catch {}
6335 if (!allowed) {
6336 return failure(403, "BOUNDARY_CAPABILITY_DENIED", "Action capability was denied", schema.id, { capability });
6337 }
6338 }
6339 return null;
6340}
6341
6342async function handleInvalidation(request, environment, executionContext) {
6343 if (request.method !== "POST") return failure(405, "CACHE_INVALIDATION_METHOD", "Cache invalidation requires POST", null, null, { allow: "POST" });
6344 const contentType = request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase();
6345 if (contentType !== "application/json") return failure(415, "CACHE_INVALIDATION_CONTENT_TYPE", "Cache invalidation body requires application/json", "cache-invalidation:on-demand");
6346 const declaredLength = Number(request.headers.get("content-length") ?? 0);
6347 if (Number.isFinite(declaredLength) && declaredLength > 1_048_576) return failure(413, "CACHE_INVALIDATION_BODY_TOO_LARGE", "Cache invalidation body exceeds 1 MiB", "cache-invalidation:on-demand");
6348 const chunks = [];
6349 let total = 0;
6350 if (request.body !== null) {
6351 let reader;
6352 try {
6353 reader = request.body.getReader();
6354 while (true) {
6355 const { done, value } = await reader.read();
6356 if (done) break;
6357 if (!(value instanceof Uint8Array)) return failure(400, "CACHE_INVALIDATION_BODY_INVALID", "Cache invalidation body stream did not yield bytes", "cache-invalidation:on-demand");
6358 total += value.byteLength;
6359 if (total > 1_048_576) {
6360 await reader.cancel("cache invalidation body too large").catch(() => {});
6361 return failure(413, "CACHE_INVALIDATION_BODY_TOO_LARGE", "Cache invalidation body exceeds 1 MiB", "cache-invalidation:on-demand");
6362 }
6363 chunks.push(value);
6364 }
6365 } catch {
6366 return failure(400, "CACHE_INVALIDATION_BODY_INVALID", "Cache invalidation body stream could not be read", "cache-invalidation:on-demand");
6367 } finally {
6368 try { reader?.releaseLock(); } catch {}
6369 }
6370 }
6371 const bytes = new Uint8Array(total);
6372 let offset = 0;
6373 for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
6374 let text;
6375 try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); }
6376 catch { return failure(400, "CACHE_INVALIDATION_BODY_INVALID", "Cache invalidation body is not valid UTF-8", "cache-invalidation:on-demand"); }
6377 let body;
6378 try { body = JSON.parse(text); }
6379 catch { return failure(400, "CACHE_INVALIDATION_BODY_INVALID", "Cache invalidation body must be JSON", "cache-invalidation:on-demand"); }
6380 let bodyKeys;
6381 let tagsDescriptor;
6382 try {
6383 if (body === null || typeof body !== "object" || Array.isArray(body) || Object.getPrototypeOf(body) !== Object.prototype) {
6384 return failure(400, "CACHE_INVALIDATION_BODY_INVALID", "Cache invalidation body must be an ordinary JSON object containing only tags", "cache-invalidation:on-demand");
6385 }
6386 bodyKeys = Reflect.ownKeys(body);
6387 tagsDescriptor = Object.getOwnPropertyDescriptor(body, "tags");
6388 } catch {
6389 return failure(400, "CACHE_INVALIDATION_BODY_INVALID", "Cache invalidation body shape could not be inspected safely", "cache-invalidation:on-demand");
6390 }
6391 if (bodyKeys.length !== 1 || bodyKeys[0] !== "tags" || tagsDescriptor === undefined || !("value" in tagsDescriptor) || tagsDescriptor.enumerable !== true || tagsDescriptor.configurable !== true || tagsDescriptor.writable !== true) {
6392 return failure(400, "CACHE_INVALIDATION_BODY_INVALID", "Cache invalidation body must declare only an ordinary own tags field", "cache-invalidation:on-demand");
6393 }
6394 const rawTags = tagsDescriptor.value;
6395 if (!Array.isArray(rawTags) || rawTags.length === 0 || rawTags.length > 32) return failure(400, "CACHE_INVALIDATION_TAGS_INVALID", "Cache invalidation requires 1 to 32 tags");
6396 const tags = [...new Set(rawTags)];
6397 if (tags.some((tag) => typeof tag !== "string" || tag.length === 0 || tag.length > 128 || !/^[a-zA-Z0-9_.:@-]+$/.test(tag))) return failure(400, "CACHE_INVALIDATION_TAGS_INVALID", "Cache invalidation tags contain unsafe values");
6398 if (typeof authorize !== "function") return failure(500, "CACHE_INVALIDATION_AUTHORIZER_MISSING", "Cache invalidation authorization is not configured");
6399 let allowed = false;
6400 try {
6401 allowed = await authorize(Object.freeze({ capability: "cache.invalidate", semanticId: "cache-invalidation:on-demand", traceId: __noxidTraceIdForRequest(request), target: "server", route: null, request, environment, executionContext })) === true;
6402 } catch {}
6403 if (!allowed) return failure(403, "CACHE_INVALIDATION_DENIED", "Cache invalidation capability was denied", "cache-invalidation:on-demand");
6404 if (typeof invalidateCache !== "function") return failure(501, "CACHE_INVALIDATION_UNAVAILABLE", "The deployment host has no cache invalidation adapter", "cache-invalidation:on-demand");
6405 try {
6406 const result = await invalidateCache(Object.freeze(tags), Object.freeze({ request, environment, executionContext, semanticId: "cache-invalidation:on-demand", traceId: __noxidTraceIdForRequest(request) }));
6407 return json(200, { ok: true, tags, result: result ?? null });
6408 } catch { return failure(500, "CACHE_INVALIDATION_FAILED", "The deployment cache invalidation adapter failed", "cache-invalidation:on-demand"); }
6409}
6410
6411function taskSchemaByName(name) {
6412 return taskSchemas.find((schema) => schema.name === name) ?? null;
6413}
6414
6415async function invokeTask(name, context = Object.create(null)) {
6416 const schema = taskSchemaByName(name);
6417 if (schema === null) throw Object.assign(new Error("Unknown scheduled task " + name), { code: "TASK_NOT_FOUND", semanticId: null });
6418 const implementation = compiledTasks[schema.id] ?? hostTasks[schema.id];
6419 if (typeof implementation !== "function") {
6420 throw Object.assign(new Error("No host implementation is registered for " + schema.id), {
6421 code: "TASK_IMPLEMENTATION_MISSING",
6422 semanticId: schema.id,
6423 });
6424 }
6425 const taskSpan = __noxidTraceBeginSemantic(context.request ?? null);
6426 const traceId = taskSpan?.trace?.id ?? null;
6427 try {
6428 return await implementation(
6429 Object.freeze(Object.create(null)),
6430 __noxidDataContext({ ...context, semanticId: schema.id, traceId, task: schema.name, schedule: schema.schedule }, __NOXID_SYSTEM_PRINCIPAL),
6431 );
6432 } finally {
6433 __noxidTraceFinishSemantic(taskSpan, "task", schema.id);
6434 }
6435}
6436
6437async function handleTaskRequest(request, url, environment, executionContext) {
6438 if (!url.pathname.startsWith(taskPrefix)) return null;
6439 if (request.method !== "POST") return failure(405, "TASK_METHOD", "Scheduled task triggers require POST", null, null, { allow: "POST" });
6440 let name;
6441 try { name = decodeURIComponent(url.pathname.slice(taskPrefix.length)); }
6442 catch { return failure(400, "TASK_NAME_INVALID", "Task name is not valid URL encoding"); }
6443 const schema = taskSchemaByName(name);
6444 if (schema === null) return failure(404, "TASK_NOT_FOUND", "Unknown scheduled task " + name);
6445 if (typeof authorize !== "function") return failure(500, "TASK_AUTHORIZER_MISSING", "Scheduled task authorization is not configured", schema.id, { capability: "tasks.run" });
6446 let allowed = false;
6447 try {
6448 allowed = await authorize(Object.freeze({
6449 capability: "tasks.run",
6450 semanticId: schema.id,
6451 traceId: __noxidTraceIdForRequest(request),
6452 target: "server",
6453 route: null,
6454 request,
6455 environment,
6456 executionContext,
6457 })) === true;
6458 } catch {}
6459 if (!allowed) return failure(403, "TASK_CAPABILITY_DENIED", "Scheduled task capability was denied", schema.id, { capability: "tasks.run" });
6460 try {
6461 const value = await invokeTask(name, { request, environment, executionContext });
6462 return json(200, { ok: true, value: value === undefined ? null : value });
6463 } catch (cause) {
6464 if (cause?.code === "TASK_IMPLEMENTATION_MISSING") {
6465 return failure(501, cause.code, "Scheduled task host implementation is missing", schema.id);
6466 }
6467 return failure(500, "TASK_EXECUTION_FAILED", "Scheduled task execution failed", schema.id);
6468 }
6469}
6470
6471function cronPartMatches(part, value, minimum, maximum) {
6472 const [base, rawStep] = part.split("/");
6473 const step = rawStep === undefined ? 1 : Number(rawStep);
6474 let start = minimum;
6475 let end = maximum;
6476 if (base !== "*") {
6477 if (base.includes("-")) [start, end] = base.split("-").map(Number);
6478 else {
6479 start = Number(base);
6480 end = rawStep === undefined ? start : maximum;
6481 }
6482 }
6483 return value >= start && value <= end && (value - start) % step === 0;
6484}
6485
6486function cronFieldMatches(field, value, minimum, maximum) {
6487 return field.split(",").some((part) => cronPartMatches(part, value, minimum, maximum));
6488}
6489
6490function cronMatches(schedule, date) {
6491 const fields = schedule.trim().split(/\s+/);
6492 const minute = cronFieldMatches(fields[0], date.getUTCMinutes(), 0, 59);
6493 const hour = cronFieldMatches(fields[1], date.getUTCHours(), 0, 23);
6494 const month = cronFieldMatches(fields[3], date.getUTCMonth() + 1, 1, 12);
6495 const dayOfMonth = cronFieldMatches(fields[2], date.getUTCDate(), 1, 31);
6496 const weekday = date.getUTCDay();
6497 const dayOfWeek = cronFieldMatches(fields[4], weekday, 0, 7)
6498 || (weekday === 0 && cronFieldMatches(fields[4], 7, 0, 7));
6499 const anyDayOfMonth = fields[2] === "*";
6500 const anyDayOfWeek = fields[4] === "*";
6501 const day = anyDayOfMonth || anyDayOfWeek
6502 ? dayOfMonth && dayOfWeek
6503 : dayOfMonth || dayOfWeek;
6504 return minute && hour && month && day;
6505}
6506
6507export function startTaskScheduler(environment = Object.create(null), executionContext = Object.create(null), options = Object.create(null)) {
6508 environment = __noxidConfiguredServerEnvironment(environment);
6509 const setTimer = options.setTimeout ?? globalThis.setTimeout;
6510 const clearTimer = options.clearTimeout ?? globalThis.clearTimeout;
6511 const now = options.now ?? (() => new Date());
6512 const origin = options.origin ?? "http://noxid.local";
6513 const report = options.onError ?? ((task, error) => console.error("Noxid scheduled task " + task + " failed", error));
6514 const running = new Map();
6515 let timer = null;
6516 let stopped = false;
6517 let stopJoin = null;
6518 const runDue = (instant) => {
6519 for (const task of taskSchedules) {
6520 if (!cronMatches(task.schedule, instant) || running.has(task.name)) continue;
6521 const request = new Request(origin + taskPrefix + encodeURIComponent(task.name), { method: "POST" });
6522 const execution = Promise.resolve(fetch(request, environment, executionContext))
6523 .then(async (response) => {
6524 if (!response.ok) {
6525 let detail = null;
6526 try { detail = await response.json(); } catch {}
6527 throw Object.assign(new Error("task trigger returned " + response.status), { response: detail });
6528 }
6529 })
6530 .catch((error) => report(task.name, error))
6531 .finally(() => running.delete(task.name));
6532 running.set(task.name, execution);
6533 }
6534 };
6535 const scheduleNext = () => {
6536 if (stopped || taskSchedules.length === 0) return;
6537 const instant = now();
6538 const nextMinute = new Date(Math.floor(instant.getTime() / 60_000) * 60_000 + 60_000);
6539 timer = setTimer(() => {
6540 if (stopped) return;
6541 runDue(nextMinute);
6542 scheduleNext();
6543 }, Math.max(0, nextMinute.getTime() - instant.getTime()));
6544 };
6545 scheduleNext();
6546 return Object.freeze({
6547 stop() {
6548 if (stopJoin !== null) return stopJoin;
6549 stopped = true;
6550 if (timer !== null) clearTimer(timer);
6551 timer = null;
6552 stopJoin = Promise.allSettled([...running.values()]);
6553 return stopJoin;
6554 },
6555 });
6556}
6557
6558/* noxid-server:live-resource-transport-runtime */
6559
6560export async function fetchEndpoint(request, environment = Object.create(null), executionContext = Object.create(null)) {
6561 environment = __noxidConfiguredServerEnvironment(environment);
6562 return withNoxidRequestTrace(request, async () => {
6563 /* noxid-server:startup */
6564 const url = new URL(request.url);
6565 /* noxid-server:live-resource-request */
6566 const queueDrainResponse = await handleQueueDrainRequest(request, url, environment, executionContext);
6567 if (queueDrainResponse !== null) return queueDrainResponse;
6568 const agentSurfaceResponse = await handleAgentSurfaceRequest(request, url, environment, executionContext);
6569 if (agentSurfaceResponse !== null) return agentSurfaceResponse;
6570 return handleEndpointRequest(request, url, environment, executionContext);
6571 });
6572}
6573
6574export async function fetch(request, environment = Object.create(null), executionContext = Object.create(null)) {
6575 environment = __noxidConfiguredServerEnvironment(environment);
6576 return withNoxidRequestTrace(request, async () => {
6577 const url = new URL(request.url);
6578 const endpointResponse = await fetchEndpoint(request, environment, executionContext);
6579 if (endpointResponse !== null) return endpointResponse;
6580 const taskResponse = await handleTaskRequest(request, url, environment, executionContext);
6581 if (taskResponse !== null) return taskResponse;
6582 if (url.pathname === invalidationEndpoint) return handleInvalidation(request, environment, executionContext);
6583 if (!url.pathname.startsWith(endpointPrefix)) return failure(404, "BOUNDARY_NOT_FOUND", "No Noxid action exists at this path");
6584 if (request.method !== "POST") return failure(405, "BOUNDARY_METHOD", "Noxid actions require POST", null, null, { allow: "POST" });
6585 let actionId;
6586 try { actionId = decodeURIComponent(url.pathname.slice(endpointPrefix.length)); }
6587 catch { return failure(400, "BOUNDARY_ID_INVALID", "Action identifier is not valid URL encoding"); }
6588 const schema = schemas[actionId];
6589 if (!schema) return failure(404, "BOUNDARY_NOT_FOUND", `Unknown Noxid action ${actionId}`, actionId);
6590 const implementation = compiledActions[actionId] ?? hostActions[actionId];
6591 if (typeof implementation !== "function") return failure(501, "BOUNDARY_IMPLEMENTATION_MISSING", `No host implementation is registered for ${actionId}`, actionId);
6592 const resolvedRoute = resolveRouteScope(request, schema);
6593 if (resolvedRoute.error) return resolvedRoute.error;
6594 const scope = resolvedRoute.scope;
6595 const middlewareOutcome = await applyMiddleware(request, schema, scope, environment, executionContext);
6596 const middlewareHeaders = middlewareOutcome.headers;
6597 if (middlewareOutcome.failure) return withMiddlewareHeaders(middlewareOutcome.failure, middlewareHeaders);
6598 const authorizationFailure = await authorizeAction(request, schema, scope, environment, executionContext);
6599 if (authorizationFailure) return withMiddlewareHeaders(authorizationFailure, middlewareHeaders);
6600 const decoded = await decodeArguments(request, schema);
6601 if (decoded.error) return withMiddlewareHeaders(decoded.error, middlewareHeaders);
6602 const actionSpan = __noxidTraceBeginSemantic(request);
6603 try {
6604 const middlewareContext = middlewareOutcome.context ?? EMPTY_MIDDLEWARE_CONTEXT;
6605 const actionPrincipal = __noxidPrincipal(middlewareContext, environment, __noxidAgentForRequest(request));
6606 const value = await implementation(decoded.arguments, __noxidDataContext({ request, environment, executionContext, semanticId: schema.id, traceId: __noxidTraceIdForRequest(request), target: schema.target, route: scope, capabilities: schema.capabilities, middlewareContext }, actionPrincipal));
6607 const result = validateType(schema.result.type, value, "result", schema.result.typeId);
6608 if (result.issue) return withMiddlewareHeaders(failure(500, "BOUNDARY_RESULT_TYPE", "Action returned a value that violates its declared result type", schema.result.id, { expected: schema.result.type, validation: result.details }), middlewareHeaders);
6609 await __noxidPublishLiveInvalidations(schema.invalidates, actionPrincipal);
6610 return withMiddlewareHeaders(json(200, { ok: true, value: result.value === undefined ? null : result.value }), middlewareHeaders);
6611 } catch (cause) {
6612 const code = typeof cause?.code === "string" ? cause.code : "BOUNDARY_EXECUTION_FAILED";
6613 const message = cause?.expose === true && typeof cause?.message === "string" ? cause.message : "Action execution failed";
6614 return withMiddlewareHeaders(failure(500, code, message, schema.id, null, {}, false), middlewareHeaders);
6615 } finally {
6616 __noxidTraceFinishSemantic(actionSpan, "action", schema.id, { route: scope?.pattern });
6617 }
6618 });
6619}
6620
6621globalThis.__NOXID_FETCH_HANDLER__ = fetch;
6622export default Object.freeze({ fetch });
6623"#;
6624
6625#[cfg(test)]
6626mod tests {
6627 use super::*;
6628 use noxid_execution_ir::{
6629 EndpointExecutionBoundary, EndpointExecutionInput, ExecutionBoundary, ExecutionParameter,
6630 ExecutionResult, ExecutionRouteScope, LiveResourceExecutionContract,
6631 PresenceExecutionContract, PresenceExecutionField, QueueExecutionField,
6632 };
6633 use noxid_ir::{
6634 EndpointCacheMode, EndpointCachePolicy, EndpointInputSection, EndpointKind,
6635 EndpointLimitPolicy, EndpointLimitScope, EndpointLimitWindow, EndpointMethod,
6636 ExecutionTarget, SemanticBinaryOp, SemanticExpr, SemanticExprKind, SemanticId,
6637 };
6638 use noxid_source::Span;
6639 use noxid_types::Type;
6640 use std::process::Command;
6641 use std::time::{SystemTime, UNIX_EPOCH};
6642
6643 fn generate(
6647 program: &ExecutionProgram,
6648 host_import: &str,
6649 validator_import: &str,
6650 middleware_import: &str,
6651 base_path: &str,
6652 server_secrets: &[String],
6653 ) -> ServerJavaScriptOutput {
6654 super::generate(
6655 program,
6656 host_import,
6657 validator_import,
6658 middleware_import,
6659 base_path,
6660 server_secrets,
6661 )
6662 .expect("every fixture body has a compiler-owned server lowering")
6663 }
6664
6665 #[allow(clippy::too_many_arguments)]
6666 fn generate_with_agent_surfaces(
6667 program: &ExecutionProgram,
6668 host_import: &str,
6669 validator_import: &str,
6670 middleware_import: &str,
6671 startup_import: Option<&str>,
6672 base_path: &str,
6673 server_secrets: &[String],
6674 agent_surfaces: AgentSurfaceOptions<'_>,
6675 ) -> ServerJavaScriptOutput {
6676 super::generate_with_agent_surfaces(
6677 program,
6678 host_import,
6679 validator_import,
6680 middleware_import,
6681 startup_import,
6682 base_path,
6683 server_secrets,
6684 agent_surfaces,
6685 )
6686 .expect("every fixture body has a compiler-owned server lowering")
6687 }
6688
6689 #[allow(clippy::too_many_arguments)]
6690 fn generate_with_runtime_options(
6691 program: &ExecutionProgram,
6692 host_import: &str,
6693 validator_import: &str,
6694 middleware_import: &str,
6695 startup_import: Option<&str>,
6696 base_path: &str,
6697 server_secrets: &[String],
6698 agent_surfaces: AgentSurfaceOptions<'_>,
6699 tracing_mode: ServerTracingMode,
6700 runtime_options: ServerRuntimeOptions,
6701 ) -> ServerJavaScriptOutput {
6702 super::generate_with_runtime_options(
6703 program,
6704 host_import,
6705 validator_import,
6706 middleware_import,
6707 startup_import,
6708 base_path,
6709 server_secrets,
6710 agent_surfaces,
6711 tracing_mode,
6712 runtime_options,
6713 )
6714 .expect("every fixture body has a compiler-owned server lowering")
6715 }
6716
6717 #[test]
6725 fn distinct_construct_and_unwrap_erase_to_the_same_plain_server_value() {
6726 let literal = SemanticExpr {
6727 kind: SemanticExprKind::String("u-1".into()),
6728 ty: Type::String,
6729 span: Span::new(0, 1),
6730 };
6731 let constructed = SemanticExpr {
6732 kind: SemanticExprKind::FunctionCall {
6733 function: SemanticId::distinct_construct("UserId"),
6734 name: "UserId".into(),
6735 arguments: vec![literal],
6736 },
6737 ty: Type::Named("UserId".into()),
6738 span: Span::new(0, 1),
6739 };
6740 let unwrapped = SemanticExpr {
6741 kind: SemanticExprKind::FunctionCall {
6742 function: SemanticId::distinct_unwrap("UserId"),
6743 name: "UserId.base".into(),
6744 arguments: vec![constructed.clone()],
6745 },
6746 ty: Type::String,
6747 span: Span::new(0, 1),
6748 };
6749 assert_eq!(
6750 compiler_body_javascript(&constructed).expect("construction lowers"),
6751 "\"u-1\""
6752 );
6753 assert_eq!(
6754 compiler_body_javascript(&unwrapped).expect("unwrap lowers"),
6755 "\"u-1\""
6756 );
6757 }
6758
6759 fn server_builtin(name: &str, arguments: Vec<SemanticExpr>, ty: Type) -> SemanticExpr {
6760 SemanticExpr {
6761 kind: SemanticExprKind::FunctionCall {
6762 function: SemanticId::function(&format!("@builtin.{name}")),
6763 name: name.into(),
6764 arguments,
6765 },
6766 ty,
6767 span: Span::new(0, 1),
6768 }
6769 }
6770
6771 fn endpoint_boundary(
6772 name: &str,
6773 method: EndpointMethod,
6774 path: &str,
6775 inputs: Vec<EndpointExecutionInput>,
6776 result: &str,
6777 ) -> EndpointExecutionBoundary {
6778 EndpointExecutionBoundary {
6779 id: SemanticId::endpoint(name),
6780 kind: EndpointKind::RequestResponse,
6781 host_key: Some(SemanticId::endpoint(name)),
6782 name: name.into(),
6783 version: 1,
6784 description: None,
6785 method: Some(method),
6786 path: Some(path.into()),
6787 inputs,
6788 result: ExecutionResult {
6789 id: SemanticId::endpoint_result(name),
6790 ty: result.into(),
6791 type_id: None,
6792 },
6793 statements: vec![],
6794 capabilities: vec![],
6795 timeout_ms: 30_000,
6796 limit: None,
6797 cache: None,
6798 idempotent: false,
6799 middleware: vec![],
6800 invalidates: vec![],
6801 span: Span::new(0, 1),
6802 }
6803 }
6804
6805 fn endpoint_input(
6806 endpoint: &str,
6807 section: EndpointInputSection,
6808 name: &str,
6809 ty: &str,
6810 ) -> EndpointExecutionInput {
6811 EndpointExecutionInput {
6812 id: SemanticId::endpoint_field(endpoint, section, name),
6813 section,
6814 name: name.into(),
6815 ty: ty.into(),
6816 type_id: None,
6817 file: None,
6818 }
6819 }
6820
6821 fn presence_program() -> ExecutionProgram {
6822 ExecutionProgram {
6823 presences: vec![PresenceExecutionContract {
6824 id: SemanticId::presence("Cursor"),
6825 component: SemanticId::component("Cursor"),
6826 component_name: "Cursor".into(),
6827 stream: SemanticId::presence_stream("Cursor"),
6828 record_type: SemanticId::type_definition("Cursor", "CursorPresenceRecord"),
6829 member_type: SemanticId::type_definition("Cursor", "CursorPresenceMember"),
6830 snapshot_type: SemanticId::type_definition("Cursor", "CursorPresenceSnapshot"),
6831 fields: vec![PresenceExecutionField {
6832 id: SemanticId::presence_field("Cursor", "name"),
6833 name: "name".into(),
6834 ty: "String".into(),
6835 type_id: None,
6836 }],
6837 capabilities: vec![],
6838 route_scopes: vec![
6839 ExecutionRouteScope {
6840 route: SemanticId::route("/room"),
6841 pattern: "/room".into(),
6842 parameters: vec![],
6843 middleware: vec![],
6844 },
6845 ExecutionRouteScope {
6846 route: SemanticId::route("/other"),
6847 pattern: "/other".into(),
6848 parameters: vec![],
6849 middleware: vec![],
6850 },
6851 ],
6852 ttl_ms: 5_000,
6853 heartbeat_ms: 1_000,
6854 }],
6855 ..ExecutionProgram::default()
6856 }
6857 }
6858
6859 #[test]
6860 fn generated_presence_handler_is_valid_and_uses_the_shared_live_surface() {
6861 let generated = generate_with_runtime_options(
6862 &presence_program(),
6863 "./host.mjs",
6864 "./validators.mjs",
6865 "./middleware.mjs",
6866 None,
6867 "/app",
6868 &[],
6869 AgentSurfaceOptions::default(),
6870 ServerTracingMode::Requests,
6871 ServerRuntimeOptions {
6872 db_pool: 10,
6873 pubsub: Some(PubSubRuntimeOptions {
6874 driver: PubSubDriver::Memory,
6875 coalescing_ms: 1,
6876 }),
6877 development_trace_capture: true,
6878 application_namespace: Some("codegen_test".into()),
6879 ..ServerRuntimeOptions::default()
6880 },
6881 );
6882 assert!(generated.handler.contains("/app/_noxid/live"));
6883 assert!(generated.handler.contains("/app/_noxid/presence"));
6884 assert!(
6885 generated
6886 .handler
6887 .contains("__noxidPubSubEvent(\"presence\"")
6888 );
6889 assert!(generated.handler.contains("developmentCapture = true"));
6890 let root = std::env::temp_dir().join(format!(
6891 "noxid-presence-syntax-{}-{}",
6892 std::process::id(),
6893 SystemTime::now()
6894 .duration_since(UNIX_EPOCH)
6895 .unwrap()
6896 .as_nanos()
6897 ));
6898 std::fs::create_dir_all(&root).unwrap();
6899 let handler = root.join("handler.mjs");
6900 std::fs::write(&handler, generated.handler).unwrap();
6901 let output = Command::new("node")
6902 .args(["--check", handler.to_str().unwrap()])
6903 .output()
6904 .expect("Node.js is required for generated presence syntax tests");
6905 let _ = std::fs::remove_dir_all(root);
6906 assert!(
6907 output.status.success(),
6908 "{}",
6909 String::from_utf8_lossy(&output.stderr)
6910 );
6911 }
6912
6913 #[test]
6914 fn emits_every_scalar_builtin_in_compiler_owned_remote_bodies() {
6915 let string = || SemanticExpr {
6916 kind: SemanticExprKind::Reference(SemanticId::action_parameter("C", "run", "name")),
6917 ty: Type::String,
6918 span: Span::new(0, 1),
6919 };
6920 let int = |value| SemanticExpr {
6921 kind: SemanticExprKind::Int(value),
6922 ty: Type::Int,
6923 span: Span::new(0, 1),
6924 };
6925 let float = |value| SemanticExpr {
6926 kind: SemanticExprKind::Float(value),
6927 ty: Type::Float,
6928 span: Span::new(0, 1),
6929 };
6930 let cases = [
6931 (
6932 server_builtin("len", vec![string()], Type::Int),
6933 "args[\"name\"].length",
6934 ),
6935 (
6936 server_builtin(
6937 "contains",
6938 vec![
6939 string(),
6940 SemanticExpr {
6941 kind: SemanticExprKind::String("a".into()),
6942 ty: Type::String,
6943 span: Span::new(0, 1),
6944 },
6945 ],
6946 Type::Boolean,
6947 ),
6948 "args[\"name\"].includes(\"a\")",
6949 ),
6950 (
6951 server_builtin(
6952 "startsWith",
6953 vec![
6954 string(),
6955 SemanticExpr {
6956 kind: SemanticExprKind::String("A".into()),
6957 ty: Type::String,
6958 span: Span::new(0, 1),
6959 },
6960 ],
6961 Type::Boolean,
6962 ),
6963 "args[\"name\"].startsWith(\"A\")",
6964 ),
6965 (
6966 server_builtin("trim", vec![string()], Type::String),
6967 "args[\"name\"].trim()",
6968 ),
6969 (
6970 server_builtin("lower", vec![string()], Type::String),
6971 "args[\"name\"].toLowerCase()",
6972 ),
6973 (
6974 server_builtin("upper", vec![string()], Type::String),
6975 "args[\"name\"].toUpperCase()",
6976 ),
6977 (
6978 server_builtin("min", vec![int(2), int(3)], Type::Int),
6979 "Math.min(2, 3)",
6980 ),
6981 (
6982 server_builtin("max", vec![float(2.5), float(3.5)], Type::Float),
6983 "Math.max(2.5, 3.5)",
6984 ),
6985 (
6986 server_builtin("abs", vec![int(-2)], Type::Int),
6987 "Math.abs(-2)",
6988 ),
6989 (
6990 server_builtin("round", vec![float(2.5)], Type::Int),
6991 "Math.round(2.5)",
6992 ),
6993 (
6994 server_builtin("floor", vec![float(2.5)], Type::Int),
6995 "Math.floor(2.5)",
6996 ),
6997 (
6998 server_builtin("ceil", vec![float(2.5)], Type::Int),
6999 "Math.ceil(2.5)",
7000 ),
7001 (server_builtin("toFloat", vec![int(2)], Type::Float), "(2)"),
7002 (
7003 server_builtin("toInt", vec![float(2.5)], Type::Int),
7004 "Math.trunc(2.5)",
7005 ),
7006 (
7007 server_builtin("toString", vec![int(2)], Type::String),
7008 "String(2)",
7009 ),
7010 ];
7011 for (expression, expected) in cases {
7012 assert_eq!(
7013 compiler_body_javascript(&expression).expect("builtin body emits"),
7014 expected
7015 );
7016 }
7017
7018 let user_function = SemanticExpr {
7019 kind: SemanticExprKind::FunctionCall {
7020 function: SemanticId::function("len"),
7021 name: "len".into(),
7022 arguments: vec![string()],
7023 },
7024 ty: Type::Int,
7025 span: Span::new(0, 1),
7026 };
7027 let error = compiler_body_javascript(&user_function)
7028 .expect_err("a user function call has no compiler-owned server lowering");
7029 assert!(
7030 error.starts_with(
7031 "error[REMOTE_ACTION_CALL_UNSUPPORTED]: the call `len(1 argument(s))`"
7032 ),
7033 "{error}"
7034 );
7035 assert!(error.contains("host-implemented body"), "{error}");
7036 }
7037
7038 #[test]
7043 fn an_external_call_fails_closed_instead_of_emitting_undefined() {
7044 let call = SemanticExpr {
7045 kind: SemanticExprKind::Call {
7046 function: SemanticId::external_function("./format.js", "shout"),
7047 name: "shout".into(),
7048 arguments: vec![SemanticExpr {
7049 kind: SemanticExprKind::Reference(SemanticId::action_parameter(
7050 "C", "save", "note",
7051 )),
7052 ty: Type::String,
7053 span: Span::new(0, 1),
7054 }],
7055 },
7056 ty: Type::String,
7057 span: Span::new(0, 1),
7058 };
7059 let error = compiler_body_javascript(&call)
7060 .expect_err("an external JavaScript call has no compiler-owned server lowering");
7061 assert!(
7062 error.starts_with(
7063 "error[REMOTE_ACTION_CALL_UNSUPPORTED]: the call `shout(1 argument(s))`"
7064 ),
7065 "{error}"
7066 );
7067 assert!(error.contains("host-implemented body"), "{error}");
7068
7069 let error = compiler_statements_javascript(
7071 &[noxid_ir::SemanticStatement::Return {
7072 value: call,
7073 span: Span::new(0, 1),
7074 }],
7075 2,
7076 )
7077 .expect_err("the statement emitter propagates the expression rejection");
7078 assert!(
7079 error.starts_with("error[REMOTE_ACTION_CALL_UNSUPPORTED]"),
7080 "{error}"
7081 );
7082 }
7083
7084 #[test]
7088 fn a_builtin_with_no_lowering_fails_closed_in_the_server_emitter() {
7089 let call = SemanticExpr {
7090 kind: SemanticExprKind::FunctionCall {
7091 function: SemanticId::function("@builtin.len"),
7092 name: "len".into(),
7093 arguments: vec![
7094 SemanticExpr {
7095 kind: SemanticExprKind::String("a".into()),
7096 ty: Type::String,
7097 span: Span::new(0, 1),
7098 },
7099 SemanticExpr {
7100 kind: SemanticExprKind::String("b".into()),
7101 ty: Type::String,
7102 span: Span::new(0, 1),
7103 },
7104 ],
7105 },
7106 ty: Type::Int,
7107 span: Span::new(0, 1),
7108 };
7109 let error =
7110 compiler_body_javascript(&call).expect_err("a two-argument `len` has no lowering");
7111 assert!(
7112 error.starts_with("error[BUILTIN_OVERLOAD_MISMATCH]: builtin `len`"),
7113 "{error}"
7114 );
7115 }
7116
7117 #[test]
7118 fn collection_queries_use_shared_emission_in_remote_bodies() {
7119 let query = SemanticExpr {
7120 kind: SemanticExprKind::CollectionQuery {
7121 base: Box::new(SemanticExpr {
7122 kind: SemanticExprKind::Reference(SemanticId::state("C", "items")),
7123 ty: Type::Array(Box::new(Type::Int)),
7124 span: Span::new(0, 1),
7125 }),
7126 kind: noxid_ir::CollectionQueryKind::Count,
7127 field: None,
7128 value: None,
7129 },
7130 ty: Type::Int,
7131 span: Span::new(0, 1),
7132 };
7133 assert_eq!(
7134 compiler_body_javascript(&query).expect("collection query emits"),
7135 "(args[\"items\"]).length"
7136 );
7137 }
7138
7139 #[test]
7140 fn map_queries_use_typed_sorted_shared_emission_in_remote_bodies() {
7141 let query = SemanticExpr {
7142 kind: SemanticExprKind::CollectionQuery {
7143 base: Box::new(SemanticExpr {
7144 kind: SemanticExprKind::Reference(SemanticId::action_parameter(
7145 "C", "read", "counts",
7146 )),
7147 ty: Type::Map(Box::new(Type::Boolean), Box::new(Type::Int)),
7148 span: Span::new(0, 1),
7149 }),
7150 kind: noxid_ir::CollectionQueryKind::MapEntries,
7151 field: None,
7152 value: None,
7153 },
7154 ty: Type::Array(Box::new(Type::MapEntry(
7155 Box::new(Type::Boolean),
7156 Box::new(Type::Int),
7157 ))),
7158 span: Span::new(0, 1),
7159 };
7160 let javascript = compiler_body_javascript(&query).expect("map query emits");
7161 let script = format!(
7162 "const args = {{ counts: Object.freeze({{true: 2, false: 1}}) }}; const result = {javascript}; if (JSON.stringify(result) !== JSON.stringify([{{key:false,value:1}},{{key:true,value:2}}])) process.exit(1);"
7163 );
7164 let output = Command::new("node")
7165 .args(["--input-type=module", "-e", &script])
7166 .output()
7167 .expect("node must execute server map query");
7168 assert!(
7169 output.status.success(),
7170 "{}",
7171 String::from_utf8_lossy(&output.stderr)
7172 );
7173 }
7174
7175 #[test]
7176 fn optional_coalescing_lowers_in_compiler_owned_remote_bodies() {
7177 let expression = SemanticExpr {
7178 kind: SemanticExprKind::Binary {
7179 left: Box::new(SemanticExpr {
7180 kind: SemanticExprKind::Reference(SemanticId::action_parameter(
7181 "C", "read", "value",
7182 )),
7183 ty: Type::Optional(Box::new(Type::Int)),
7184 span: Span::new(0, 1),
7185 }),
7186 op: SemanticBinaryOp::Coalesce,
7187 right: Box::new(SemanticExpr {
7188 kind: SemanticExprKind::Int(1),
7189 ty: Type::Int,
7190 span: Span::new(0, 1),
7191 }),
7192 },
7193 ty: Type::Int,
7194 span: Span::new(0, 1),
7195 };
7196 assert_eq!(
7197 compiler_body_javascript(&expression).expect("coalescing body emits"),
7198 "(args[\"value\"] ?? 1)"
7199 );
7200 }
7201
7202 #[test]
7203 fn server_compound_equality_matches_recursive_language_values() {
7204 let ready = |value| SemanticExpr {
7205 kind: SemanticExprKind::Variant {
7206 machine: SemanticId::machine("C", "Phase"),
7207 variant: SemanticId::variant("C", "Phase", "Ready"),
7208 payload: Some(Box::new(SemanticExpr {
7209 kind: SemanticExprKind::Int(value),
7210 ty: Type::Int,
7211 span: Span::new(0, 1),
7212 })),
7213 },
7214 ty: Type::Named("Phase".into()),
7215 span: Span::new(0, 1),
7216 };
7217 let expression = SemanticExpr {
7218 kind: SemanticExprKind::Binary {
7219 left: Box::new(SemanticExpr {
7220 kind: SemanticExprKind::Array(vec![ready(1), ready(2)]),
7221 ty: Type::Array(Box::new(Type::Named("Phase".into()))),
7222 span: Span::new(0, 1),
7223 }),
7224 op: SemanticBinaryOp::Equal,
7225 right: Box::new(SemanticExpr {
7226 kind: SemanticExprKind::Array(vec![ready(1), ready(2)]),
7227 ty: Type::Array(Box::new(Type::Named("Phase".into()))),
7228 span: Span::new(0, 1),
7229 }),
7230 },
7231 ty: Type::Boolean,
7232 span: Span::new(0, 1),
7233 };
7234 let script = format!(
7235 "{}\nif (!({})) process.exit(1);",
7236 LANGUAGE_VALUE_EQUALITY_FUNCTION,
7237 compiler_body_javascript(&expression).expect("compound equality body emits")
7238 );
7239 let output = std::process::Command::new("node")
7240 .args(["--input-type=module", "-e", &script])
7241 .output()
7242 .expect("node must execute server compound equality");
7243 assert!(
7244 output.status.success(),
7245 "{}",
7246 String::from_utf8_lossy(&output.stderr)
7247 );
7248 }
7249
7250 #[test]
7251 fn server_handler_uses_fetch_and_never_embeds_host_source() {
7252 let program = ExecutionProgram {
7253 live_resources: vec![],
7254 presences: vec![],
7255 boundaries: vec![ExecutionBoundary {
7256 id: SemanticId::execution_boundary("Account", "save", ExecutionTarget::Server),
7257 action: SemanticId::action("Account", "save"),
7258 component: SemanticId::component("Account"),
7259 component_name: "Account".into(),
7260 action_name: "save".into(),
7261 target: ExecutionTarget::Server,
7262 parameters: vec![ExecutionParameter {
7263 id: SemanticId::action_parameter("Account", "save", "id"),
7264 name: "id".into(),
7265 ty: "Int".into(),
7266 type_id: None,
7267 }],
7268 result: ExecutionResult {
7269 id: SemanticId::action_result("Account", "save"),
7270 ty: "Boolean".into(),
7271 type_id: None,
7272 },
7273 body: Some(SemanticExpr {
7274 kind: SemanticExprKind::Binary {
7275 left: Box::new(SemanticExpr {
7276 kind: SemanticExprKind::Reference(SemanticId::action_parameter(
7277 "Account", "save", "id",
7278 )),
7279 ty: Type::Int,
7280 span: Span::new(0, 1),
7281 }),
7282 op: SemanticBinaryOp::Add,
7283 right: Box::new(SemanticExpr {
7284 kind: SemanticExprKind::Int(1),
7285 ty: Type::Int,
7286 span: Span::new(0, 1),
7287 }),
7288 },
7289 ty: Type::Int,
7290 span: Span::new(0, 1),
7291 }),
7292 capabilities: vec!["account.write".into()],
7293 route_scopes: vec![ExecutionRouteScope {
7294 route: SemanticId::route("/accounts"),
7295 pattern: "/accounts".into(),
7296 parameters: vec![],
7297 middleware: vec![SemanticId::middleware("session")],
7298 }],
7299 invalidates: vec![],
7300 span: Span::new(0, 1),
7301 }],
7302 endpoints: vec![],
7303 tasks: vec![],
7304 queues: vec![],
7305 };
7306 let output = generate(
7307 &program,
7308 "./host.js",
7309 "./validators.js",
7310 "./middleware.js",
7311 "/console",
7312 &[],
7313 );
7314 assert!(
7315 output
7316 .handler
7317 .contains("export async function fetch(request")
7318 );
7319 assert!(output.handler.contains("/console/_noxid/actions/"));
7320 assert!(output.handler.contains("BOUNDARY_ARGUMENT_TYPE"));
7321 assert!(output.handler.contains("BOUNDARY_RESULT_TYPE"));
7322 assert!(output.handler.contains("BOUNDARY_CAPABILITY_DENIED"));
7323 assert!(output.handler.contains("BOUNDARY_MIDDLEWARE_DENIED"));
7324 assert!(output.handler.contains("account.write"));
7325 assert!(output.handler.contains("action:Account.save"));
7326 assert!(output.handler.contains("compiledActions"));
7327 assert!(output.handler.contains("args[\"id\"] + 1"));
7328 assert!(
7329 !output
7330 .handler
7331 .contains("import * as __noxidServerStorageRuntime")
7332 );
7333 assert_eq!(output.server_actions, 1);
7334 assert_eq!(output.edge_actions, 0);
7335 }
7336
7337 #[test]
7338 fn queue_drain_is_adapter_activated_capability_guarded_and_budgeted() {
7339 let queue_id = SemanticId::queue("Drain");
7340 let live_id = SemanticId::resource("DrainStatus");
7341 let program = ExecutionProgram {
7342 live_resources: vec![LiveResourceExecutionContract {
7343 id: live_id.clone(),
7344 name: "DrainStatus".into(),
7345 capabilities: vec![],
7346 route_scopes: vec![],
7347 }],
7348 presences: vec![],
7349 boundaries: vec![],
7350 endpoints: vec![],
7351 tasks: vec![],
7352 queues: vec![QueueExecutionBoundary {
7353 id: queue_id.clone(),
7354 host_key: Some(queue_id),
7355 name: "Drain".into(),
7356 payload: vec![QueueExecutionField {
7357 id: SemanticId::queue_payload("Drain", "fail"),
7358 name: "fail".into(),
7359 ty: "Boolean".into(),
7360 type_id: None,
7361 type_ids: vec![],
7362 }],
7363 retry: 1,
7364 backoff_ms: 60_000,
7365 statements: vec![],
7366 invalidates: vec![live_id],
7367 span: Span::new(0, 1),
7368 }],
7369 };
7370 let generated = generate_with_runtime_options(
7371 &program,
7372 "./host.mjs",
7373 "./validators.mjs",
7374 "./middleware.mjs",
7375 None,
7376 "/console",
7377 &[],
7378 AgentSurfaceOptions::default(),
7379 ServerTracingMode::Requests,
7380 ServerRuntimeOptions {
7381 db_pool: 10,
7382 pubsub: Some(PubSubRuntimeOptions {
7383 driver: PubSubDriver::Memory,
7384 coalescing_ms: 1,
7385 }),
7386 development_trace_capture: false,
7387 application_namespace: Some("codegen_test".into()),
7388 ..ServerRuntimeOptions::default()
7389 },
7390 );
7391 let root = std::env::temp_dir().join(format!(
7392 "noxid-queue-drain-handler-{}-{}",
7393 std::process::id(),
7394 SystemTime::now()
7395 .duration_since(UNIX_EPOCH)
7396 .unwrap()
7397 .as_nanos()
7398 ));
7399 std::fs::create_dir_all(root.join("node_modules/postgres")).unwrap();
7400 std::fs::write(root.join("package.json"), "{\"type\":\"module\"}\n").unwrap();
7401 std::fs::write(root.join("handler.mjs"), generated.handler).unwrap();
7402 std::fs::write(
7403 root.join("validators.mjs"),
7404 "export const typeValidators = Object.freeze({});\n",
7405 )
7406 .unwrap();
7407 std::fs::write(
7408 root.join("middleware.mjs"),
7409 "export const globalMiddleware = Object.freeze([]);\nexport const middleware = Object.freeze({});\n",
7410 )
7411 .unwrap();
7412 std::fs::write(
7413 root.join("host.mjs"),
7414 r#"export const queues = Object.freeze({
7415 "queue:Drain": async ({ fail }) => {
7416 await new Promise((resolve) => setTimeout(resolve, 12));
7417 if (fail) throw new Error("retry me");
7418 return "done";
7419 },
7420});
7421export async function authorize({ capability, environment }) {
7422 environment.authorized = (environment.authorized ?? 0) + 1;
7423 environment.capability = capability;
7424 return environment.allow === true;
7425}
7426"#,
7427 )
7428 .unwrap();
7429 std::fs::write(
7430 root.join("node_modules/postgres/package.json"),
7431 "{\"name\":\"postgres\",\"type\":\"module\",\"exports\":\"./index.js\"}\n",
7432 )
7433 .unwrap();
7434 std::fs::write(
7435 root.join("node_modules/postgres/index.js"),
7436 r#"const jobs = [
7437 { id: "one", queue: "Drain", payload: { fail: false }, principal: "system", attempts: 0, run_at: new Date(0) },
7438 { id: "two", queue: "Drain", payload: { fail: true }, principal: "system", attempts: 0, run_at: new Date(0) },
7439];
7440let claims = 0;
7441const text = (strings) => strings.join("?").replace(/\s+/g, " ").trim();
7442export function observedClaims() { return claims; }
7443export default function postgres() {
7444 const sql = async () => [];
7445 sql.unsafe = async () => [];
7446 sql.json = (value) => value;
7447 sql.begin = async (operation) => operation(async (strings) => {
7448 if (!text(strings).startsWith("SELECT ")) return [];
7449 const job = jobs.shift();
7450 if (job === undefined) return [];
7451 claims += 1;
7452 return [job];
7453 });
7454 return sql;
7455}
7456"#,
7457 )
7458 .unwrap();
7459 let script = r#"import { fetch as handle, fetchEndpoint, __noxidLiveConnectionPrincipal, __noxidPubSubSubscribe } from "./handler.mjs";
7460import { observedClaims } from "postgres";
7461const url = "http://noxid.test/console/_noxid/queue/drain";
7462const principal = __noxidLiveConnectionPrincipal(Object.create(null), Object.create(null));
7463const events = [];
7464const stop = await __noxidPubSubSubscribe("invalidation", "resource:DrainStatus", principal, (event) => events.push(event.semanticId), { schedule: (run) => { queueMicrotask(run); return 1; }, cancel: () => {} });
7465
7466let environment = { allow: true };
7467let response = await fetchEndpoint(new Request(url, { method: "POST" }), environment);
7468let body = await response.json();
7469if (response.status !== 404 || body.error?.code !== "QUEUE_DRAIN_DISABLED" || environment.authorized !== undefined || observedClaims() !== 0) throw new Error(`disabled door was exposed ${response.status} ${JSON.stringify(body)}`);
7470
7471environment = { allow: true };
7472response = await handle(new Request(url), environment, { noxidQueueDrain: true });
7473body = await response.json();
7474if (response.status !== 405 || response.headers.get("allow") !== "POST" || environment.authorized !== undefined || observedClaims() !== 0) throw new Error(`method guard failed ${response.status} ${JSON.stringify(body)}`);
7475
7476environment = { allow: true };
7477response = await fetchEndpoint(new Request(url, { method: "POST" }), environment, { noxidQueueDrain: true, queueDrainBudgetMs: 0 });
7478body = await response.json();
7479if (response.status !== 500 || body.error?.code !== "QUEUE_DRAIN_BUDGET_INVALID" || environment.authorized !== undefined || observedClaims() !== 0) throw new Error(`invalid provider budget did not fail closed ${response.status} ${JSON.stringify(body)}`);
7480
7481environment = { allow: false };
7482response = await handle(new Request(url, { method: "POST" }), environment, { noxidQueueDrain: true, queueDrainBudgetMs: 50 });
7483body = await response.json();
7484if (response.status !== 403 || body.error?.code !== "QUEUE_DRAIN_CAPABILITY_DENIED" || environment.capability !== "queue.drain" || observedClaims() !== 0) throw new Error(`capability guard failed ${response.status} ${JSON.stringify(body)}`);
7485
7486environment = { allow: true };
7487response = await handle(new Request(url, { method: "POST" }), environment, { noxidQueueDrain: true, queueDrainBudgetMs: 5 });
7488body = await response.json();
7489if (response.status !== 200 || body.budgetMs !== 5 || body.counts?.claimed !== 1 || body.counts?.completed !== 1 || body.counts?.retried !== 0 || observedClaims() !== 1) throw new Error(`time budget did not stop new claims ${response.status} ${JSON.stringify(body)}`);
7490await new Promise((resolve) => setTimeout(resolve, 5));
7491if (JSON.stringify(events) !== JSON.stringify(["resource:DrainStatus"])) throw new Error(`queue completion did not publish ${events}`);
7492
7493environment = { allow: true };
7494response = await handle(new Request(url, { method: "POST" }), environment, { noxidQueueDrain: true });
7495body = await response.json();
7496if (response.status !== 200 || body.budgetMs !== 25000 || body.counts?.claimed !== 1 || body.counts?.completed !== 0 || body.counts?.retried !== 1 || body.counts?.deadLettered !== 0 || observedClaims() !== 2) throw new Error(`default budget or retry counts failed ${response.status} ${JSON.stringify(body)}`);
7497await new Promise((resolve) => setTimeout(resolve, 5));
7498if (events.length !== 1) throw new Error(`failed queue delivery published ${events}`);
7499await stop();
7500"#;
7501 let output = Command::new("node")
7502 .args(["--input-type=module", "-e", script])
7503 .current_dir(&root)
7504 .env("DATABASE_URL", "postgres://noxid.test/fake")
7505 .output()
7506 .expect("Node.js is required for generated queue-drain tests");
7507 let _ = std::fs::remove_dir_all(&root);
7508 assert!(
7509 output.status.success(),
7510 "{}\n{}",
7511 String::from_utf8_lossy(&output.stdout),
7512 String::from_utf8_lossy(&output.stderr)
7513 );
7514 }
7515
7516 #[test]
7517 fn generated_handler_enforces_capabilities_and_result_types() {
7518 let program = ExecutionProgram {
7519 live_resources: vec![],
7520 presences: vec![],
7521 boundaries: vec![ExecutionBoundary {
7522 id: SemanticId::execution_boundary("Account", "save", ExecutionTarget::Server),
7523 action: SemanticId::action("Account", "save"),
7524 component: SemanticId::component("Account"),
7525 component_name: "Account".into(),
7526 action_name: "save".into(),
7527 target: ExecutionTarget::Server,
7528 parameters: vec![ExecutionParameter {
7529 id: SemanticId::action_parameter("Account", "save", "id"),
7530 name: "id".into(),
7531 ty: "Int".into(),
7532 type_id: None,
7533 }],
7534 result: ExecutionResult {
7535 id: SemanticId::action_result("Account", "save"),
7536 ty: "Boolean".into(),
7537 type_id: None,
7538 },
7539 body: None,
7540 capabilities: vec!["account.write".into()],
7541 route_scopes: vec![ExecutionRouteScope {
7542 route: SemanticId::route("/accounts"),
7543 pattern: "/accounts".into(),
7544 parameters: vec![],
7545 middleware: vec![SemanticId::middleware("session")],
7546 }],
7547 invalidates: vec![],
7548 span: Span::new(0, 1),
7549 }],
7550 endpoints: vec![],
7551 tasks: vec![],
7552 queues: vec![],
7553 };
7554 let output = generate(
7555 &program,
7556 "./host.mjs",
7557 "./validators.mjs",
7558 "./middleware.mjs",
7559 "/console",
7560 &[],
7561 );
7562 let no_authorizer = generate(
7563 &program,
7564 "./host-no-authorizer.mjs",
7565 "./validators.mjs",
7566 "./middleware.mjs",
7567 "/console",
7568 &[],
7569 );
7570 let unique = std::time::SystemTime::now()
7571 .duration_since(std::time::UNIX_EPOCH)
7572 .unwrap()
7573 .as_nanos();
7574 let root = std::env::temp_dir().join(format!(
7575 "noxid-server-handler-{}-{unique}",
7576 std::process::id()
7577 ));
7578 std::fs::create_dir_all(&root).unwrap();
7579 std::fs::write(root.join("handler.mjs"), output.handler).unwrap();
7580 std::fs::write(
7581 root.join("validators.mjs"),
7582 "export const typeValidators = Object.freeze({});\n",
7583 )
7584 .unwrap();
7585 std::fs::write(
7586 root.join("middleware.mjs"),
7587 r#"export const middleware = Object.freeze({
7588 session: async ({ request, environment, host }) => {
7589 environment.trace.push("middleware:session");
7590 if (typeof host?.sessionUser !== "function") throw new Error("action middleware lost the generated host registry");
7591 const userId = host.sessionUser(request);
7592 return userId === null ? { allow: false } : { allow: true, context: { userId } };
7593 },
7594});"#,
7595 )
7596 .unwrap();
7597 std::fs::write(
7598 root.join("handler-no-authorizer.mjs"),
7599 no_authorizer.handler,
7600 )
7601 .unwrap();
7602 std::fs::write(
7603 root.join("host.mjs"),
7604 r#"export const actions = Object.freeze({
7605 "action:Account.save": async ({ id }, context) => {
7606 if (!Object.isFrozen(context.capabilities)) throw new Error("capabilities were mutable");
7607 if (!Object.isFrozen(context) || context.principal?.kind !== "user" || context.principal?.scope !== "user-a") throw new Error("action context lost the sealed middleware principal");
7608 context.environment.trace.push("action");
7609 return id === 1 ? true : "invalid";
7610 },
7611});
7612export function sessionUser(request) { return request.headers.get("x-session") === "active" ? "user-a" : null; }
7613export async function authorize({ capability, request, environment }) {
7614 environment.trace.push(`capability:${capability}`);
7615 return request.headers.get("x-capability") === capability;
7616}
7617export async function invalidateCache(tags, context) {
7618 context.environment.trace.push(`invalidate:${tags.join(",")}`);
7619 return { invalidated: tags.length };
7620}
7621"#,
7622 )
7623 .unwrap();
7624 std::fs::write(
7625 root.join("host-no-authorizer.mjs"),
7626 r#"export const actions = Object.freeze({ "action:Account.save": async () => true });
7627export function sessionUser(request) { return request.headers.get("x-session") === "active" ? "user-a" : null; }"#,
7628 )
7629 .unwrap();
7630 let script = r#"import { fetch as handle } from "./handler.mjs";
7631import { fetch as handleWithoutAuthorizer } from "./handler-no-authorizer.mjs";
7632const endpoint = "http://noxid.test/console/_noxid/actions/action%3AAccount.save";
7633function request(id, allowed = false, session = true, route = "route:/accounts") {
7634 const headers = { "content-type": "application/json" };
7635 if (route !== null) headers["x-noxid-route-id"] = route;
7636 if (session) headers["x-session"] = "active";
7637 if (allowed) headers["x-capability"] = "account.write";
7638 return new Request(endpoint, { method: "POST", headers, body: JSON.stringify({ arguments: { id } }) });
7639}
7640let environment = { trace: [] };
7641let response = await handle(request(1, false, true, null), environment);
7642let body = await response.json();
7643if (response.status !== 400 || body.error.code !== "BOUNDARY_ROUTE_REQUIRED") throw new Error("missing route did not fail closed");
7644environment = { trace: [] };
7645response = await handle(request(1, false, true, "route:/other"), environment);
7646body = await response.json();
7647if (response.status !== 403 || body.error.code !== "BOUNDARY_ROUTE_DENIED" || environment.trace.length !== 0) throw new Error("forged route did not fail closed");
7648environment = { trace: [] };
7649response = await handle(request(1, false, false), environment);
7650body = await response.json();
7651if (response.status !== 403 || body.error.code !== "BOUNDARY_MIDDLEWARE_DENIED") throw new Error("middleware denial failed");
7652environment = { trace: [] };
7653response = await handle(request(1), environment);
7654body = await response.json();
7655if (response.status !== 403 || body.error.code !== "BOUNDARY_CAPABILITY_DENIED") throw new Error("denial failed");
7656environment = { trace: [] };
7657response = await handle(request(1, true), environment);
7658body = await response.json();
7659if (response.status !== 200 || body.value !== true) throw new Error("valid result failed");
7660if (JSON.stringify(environment.trace) !== JSON.stringify(["middleware:session", "capability:account.write", "action"])) throw new Error(`wrong execution order: ${JSON.stringify(environment.trace)}`);
7661response = await handle(request(2, true), { trace: [] });
7662body = await response.json();
7663if (response.status !== 500 || body.error.code !== "BOUNDARY_RESULT_TYPE" || body.error.semanticId !== "result:Account.save") throw new Error("result validation failed");
7664response = await handleWithoutAuthorizer(request(1, true), { trace: [] });
7665body = await response.json();
7666if (response.status !== 500 || body.error.code !== "BOUNDARY_AUTHORIZER_MISSING") throw new Error("missing authorizer did not fail closed");
7667const invalidationUrl = "http://noxid.test/console/_noxid/revalidate";
7668response = await handle(new Request(invalidationUrl, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ tags: ["account"] }) }), { trace: [] });
7669body = await response.json();
7670if (response.status !== 403 || body.error.code !== "CACHE_INVALIDATION_DENIED") throw new Error("cache invalidation did not require authority");
7671environment = { trace: [] };
7672response = await handle(new Request(invalidationUrl, { method: "POST", headers: { "content-type": "application/json", "x-capability": "cache.invalidate" }, body: JSON.stringify({ tags: ["account", "account"] }) }), environment);
7673body = await response.json();
7674if (response.status !== 200 || body.tags.length !== 1 || body.result.invalidated !== 1) throw new Error("cache invalidation failed");
7675if (JSON.stringify(environment.trace) !== JSON.stringify(["capability:cache.invalidate", "invalidate:account"])) throw new Error("cache invalidation order failed");
7676for (const invalidBody of [
7677 "null",
7678 "[]",
7679 JSON.stringify({ tags: ["account"], capability: "admin" }),
7680 '{"tags":["account"],"__proto__":{"tags":["forged"]}}',
7681]) {
7682 environment = { trace: [] };
7683 response = await handle(new Request(invalidationUrl, { method: "POST", headers: { "content-type": "application/json", "x-capability": "cache.invalidate" }, body: invalidBody }), environment);
7684 body = await response.json();
7685 if (response.status !== 400 || body.error.code !== "CACHE_INVALIDATION_BODY_INVALID" || environment.trace.length !== 0) throw new Error(`non-exact cache invalidation body crossed the boundary: ${invalidBody} ${response.status} ${JSON.stringify(body)} ${JSON.stringify(environment.trace)}`);
7686}
7687const nativeJsonParse = JSON.parse;
7688for (const hostileBody of [
7689 () => Object.create({ tags: ["forged"] }),
7690 () => Object.defineProperty({}, "tags", { enumerable: true, configurable: true, get() { throw new Error("CACHE_INVALIDATION_ACCESSOR_RAN"); } }),
7691 () => new Proxy({}, { getPrototypeOf() { throw new Error("CACHE_INVALIDATION_PROXY_TRAP"); } }),
7692]) {
7693 environment = { trace: [] };
7694 JSON.parse = hostileBody;
7695 try {
7696 response = await handle(new Request(invalidationUrl, { method: "POST", headers: { "content-type": "application/json", "x-capability": "cache.invalidate" }, body: "{}" }), environment);
7697 } finally {
7698 JSON.parse = nativeJsonParse;
7699 }
7700 body = await response.json();
7701 if (response.status !== 400 || body.error.code !== "CACHE_INVALIDATION_BODY_INVALID" || environment.trace.length !== 0) throw new Error(`hostile cache invalidation shape crossed the boundary: ${response.status} ${JSON.stringify(body)} ${JSON.stringify(environment.trace)}`);
7702}
7703environment = { trace: [] };
7704const failedInvalidationStream = new ReadableStream({
7705 start(controller) { controller.error(new Error("transport failed")); },
7706});
7707response = await handle(new Request(invalidationUrl, { method: "POST", headers: { "content-type": "application/json", "x-capability": "cache.invalidate" }, body: failedInvalidationStream, duplex: "half" }), environment);
7708body = await response.json();
7709if (response.status !== 400 || body.error.code !== "CACHE_INVALIDATION_BODY_INVALID" || environment.trace.length !== 0) throw new Error(`failed cache invalidation stream escaped the structured boundary: ${response.status} ${JSON.stringify(body)} ${JSON.stringify(environment.trace)}`);
7710environment = { trace: [] };
7711response = await handle(new Request(invalidationUrl, { method: "POST", headers: { "content-type": "application/json", "x-capability": "cache.invalidate" }, body: JSON.stringify({ tags: ["../../unsafe"] }) }), environment);
7712body = await response.json();
7713if (response.status !== 400 || body.error.code !== "CACHE_INVALIDATION_TAGS_INVALID" || environment.trace.length !== 0) throw new Error("unsafe cache tag crossed authorization");
7714"#;
7715 let status = std::process::Command::new("node")
7716 .args(["--input-type=module", "-e", script])
7717 .current_dir(&root)
7718 .status()
7719 .expect("Node.js is required for generated server handler tests");
7720 let _ = std::fs::remove_dir_all(&root);
7721 assert!(status.success());
7722 }
7723
7724 #[test]
7725 fn generated_endpoint_handler_enforces_typed_routing_limits_timeout_and_replay() {
7726 let mut read = endpoint_boundary(
7727 "ReadItem",
7728 EndpointMethod::Get,
7729 "/api/items/[id]",
7730 vec![
7731 endpoint_input("ReadItem", EndpointInputSection::Params, "id", "Int"),
7732 endpoint_input(
7733 "ReadItem",
7734 EndpointInputSection::Query,
7735 "tags",
7736 "Optional<Array<Int>>",
7737 ),
7738 endpoint_input(
7739 "ReadItem",
7740 EndpointInputSection::Query,
7741 "required",
7742 "Array<String>",
7743 ),
7744 ],
7745 "String",
7746 );
7747 read.capabilities = vec!["items.read".into()];
7748 read.middleware = vec!["audit".into()];
7749 read.limit = Some(EndpointLimitPolicy {
7750 requests: 3,
7751 window: EndpointLimitWindow::Minute,
7752 scope: EndpointLimitScope::Session,
7753 });
7754 read.cache = Some(EndpointCachePolicy {
7755 id: SemanticId::endpoint_cache("ReadItem"),
7756 mode: EndpointCacheMode::Swr,
7757 seconds: 60,
7758 tags: vec!["endpoint:ReadItem@1".into()],
7759 span: Span::new(0, 1),
7760 });
7761 let mut save = endpoint_boundary(
7762 "SaveItem",
7763 EndpointMethod::Post,
7764 "/api/items",
7765 vec![endpoint_input(
7766 "SaveItem",
7767 EndpointInputSection::Body,
7768 "value",
7769 "Int",
7770 )],
7771 "Result<String, String>",
7772 );
7773 save.idempotent = true;
7774 save.middleware = vec!["audit".into()];
7775 let mut slow = endpoint_boundary(
7776 "SlowItem",
7777 EndpointMethod::Delete,
7778 "/api/items/[id]",
7779 vec![endpoint_input(
7780 "SlowItem",
7781 EndpointInputSection::Params,
7782 "id",
7783 "String",
7784 )],
7785 "Boolean",
7786 );
7787 slow.timeout_ms = 10;
7788 let mut middleware_timeout = endpoint_boundary(
7789 "MiddlewareTimeout",
7790 EndpointMethod::Get,
7791 "/api/middleware-timeout",
7792 vec![],
7793 "Boolean",
7794 );
7795 middleware_timeout.timeout_ms = 10;
7796 middleware_timeout.middleware = vec!["slow".into()];
7797 let upload = endpoint_boundary(
7798 "Upload",
7799 EndpointMethod::Post,
7800 "/api/upload",
7801 vec![endpoint_input(
7802 "Upload",
7803 EndpointInputSection::Body,
7804 "text",
7805 "String",
7806 )],
7807 "Int",
7808 );
7809 let bodyless_mutation = endpoint_boundary(
7810 "BodylessMutation",
7811 EndpointMethod::Post,
7812 "/api/bodyless",
7813 vec![],
7814 "Boolean",
7815 );
7816 let measure = endpoint_boundary(
7817 "Measure",
7818 EndpointMethod::Get,
7819 "/api/measure",
7820 vec![endpoint_input(
7821 "Measure",
7822 EndpointInputSection::Query,
7823 "value",
7824 "Float",
7825 )],
7826 "Float",
7827 );
7828 let count = endpoint_boundary(
7829 "Count",
7830 EndpointMethod::Get,
7831 "/api/count",
7832 vec![endpoint_input(
7833 "Count",
7834 EndpointInputSection::Query,
7835 "value",
7836 "Int",
7837 )],
7838 "Int",
7839 );
7840 let mut fast_replay = endpoint_boundary(
7841 "FastReplay",
7842 EndpointMethod::Post,
7843 "/api/fast-replay",
7844 vec![],
7845 "Int",
7846 );
7847 fast_replay.idempotent = true;
7848 let events = endpoint_boundary(
7849 "Events",
7850 EndpointMethod::Get,
7851 "/api/events",
7852 vec![endpoint_input(
7853 "Events",
7854 EndpointInputSection::Query,
7855 "since",
7856 "Date",
7857 )],
7858 "Date",
7859 );
7860 let optional_echo = endpoint_boundary(
7861 "OptionalEcho",
7862 EndpointMethod::Post,
7863 "/api/optional",
7864 vec![endpoint_input(
7865 "OptionalEcho",
7866 EndpointInputSection::Body,
7867 "value",
7868 "Optional<String>",
7869 )],
7870 "OptionalReply",
7871 );
7872 let mut rate_save = endpoint_boundary(
7873 "RateSave",
7874 EndpointMethod::Post,
7875 "/api/rate-save",
7876 vec![endpoint_input(
7877 "RateSave",
7878 EndpointInputSection::Body,
7879 "value",
7880 "Int",
7881 )],
7882 "Int",
7883 );
7884 rate_save.limit = Some(EndpointLimitPolicy {
7885 requests: 1,
7886 window: EndpointLimitWindow::Minute,
7887 scope: EndpointLimitScope::Ip,
7888 });
7889 let put = endpoint_boundary(
7890 "PutItem",
7891 EndpointMethod::Put,
7892 "/api/put",
7893 vec![],
7894 "Boolean",
7895 );
7896 let patch = endpoint_boundary(
7897 "PatchItem",
7898 EndpointMethod::Patch,
7899 "/api/patch",
7900 vec![],
7901 "Boolean",
7902 );
7903 let mut direct = endpoint_boundary(
7904 "DirectResponse",
7905 EndpointMethod::Get,
7906 "/health",
7907 vec![],
7908 "String",
7909 );
7910 direct.middleware = vec!["direct".into()];
7911 let mut redirect = endpoint_boundary(
7912 "RedirectResponse",
7913 EndpointMethod::Get,
7914 "/login",
7915 vec![],
7916 "String",
7917 );
7918 redirect.middleware = vec!["redirect".into()];
7919 let mut denied = endpoint_boundary(
7920 "DeniedWrite",
7921 EndpointMethod::Post,
7922 "/private/[id]",
7923 vec![
7924 endpoint_input("DeniedWrite", EndpointInputSection::Params, "id", "Int"),
7925 endpoint_input("DeniedWrite", EndpointInputSection::Query, "page", "Int"),
7926 endpoint_input("DeniedWrite", EndpointInputSection::Body, "value", "Int"),
7927 ],
7928 "Int",
7929 );
7930 denied.middleware = vec!["deny".into()];
7931 let program = ExecutionProgram {
7932 live_resources: vec![],
7933 presences: vec![],
7934 boundaries: vec![],
7935 endpoints: vec![
7936 read,
7937 save,
7938 slow,
7939 middleware_timeout,
7940 upload,
7941 bodyless_mutation,
7942 measure,
7943 count,
7944 fast_replay,
7945 events,
7946 optional_echo,
7947 rate_save,
7948 put,
7949 patch,
7950 direct,
7951 redirect,
7952 denied,
7953 ],
7954 tasks: vec![],
7955 queues: vec![],
7956 };
7957 let generated = generate(
7958 &program,
7959 "./host.mjs",
7960 "./validators.mjs",
7961 "./middleware.mjs",
7962 "/console",
7963 &[],
7964 );
7965 for method in ["GET", "POST", "PUT", "PATCH", "DELETE"] {
7966 assert!(generated.handler.contains(&format!("method: \"{method}\"")));
7967 }
7968 assert!(
7969 generated
7970 .handler
7971 .contains("import * as __noxidServerStorageRuntime")
7972 );
7973 assert!(
7974 generated
7975 .handler
7976 .contains("const __noxidStorage = __noxidServerStorageRuntime.storage")
7977 );
7978 let root = std::env::temp_dir().join(format!(
7979 "noxid-endpoint-handler-{}-{}",
7980 std::process::id(),
7981 SystemTime::now()
7982 .duration_since(UNIX_EPOCH)
7983 .unwrap()
7984 .as_nanos()
7985 ));
7986 std::fs::create_dir_all(&root).unwrap();
7987 std::fs::write(root.join("package.json"), "{\"type\":\"module\"}\n").unwrap();
7988 std::fs::write(root.join("handler.mjs"), generated.handler).unwrap();
7989 std::fs::write(
7990 root.join("noxid-server.js"),
7991 r#"const namespaces = new Map();
7992export function storage(namespace) {
7993 if (!namespaces.has(namespace)) namespaces.set(namespace, new Map());
7994 const records = namespaces.get(namespace);
7995 const read = (key) => {
7996 const record = records.get(key);
7997 if (!record) return null;
7998 if (record.expiresAt !== null && record.expiresAt <= Date.now()) { records.delete(key); return null; }
7999 return structuredClone(record.value);
8000 };
8001 return Object.freeze({
8002 async get(key) { return read(key); },
8003 async set(key, value, options) { records.set(key, { value: structuredClone(value), expiresAt: options?.ttl === undefined ? null : Date.now() + options.ttl * 1000 }); },
8004 async delete(key) { return records.delete(key); },
8005 async list(prefix = "") { for (const key of records.keys()) read(key); return Object.freeze([...records.keys()].filter((key) => key.startsWith(prefix)).sort()); },
8006 });
8007}
8008"#,
8009 )
8010 .unwrap();
8011 std::fs::write(
8012 root.join("validators.mjs"),
8013 r#"function record(fields) { return (value) => {
8014 if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("record");
8015 const trusted = Object.create(null);
8016 for (const [name, validate] of Object.entries(fields)) trusted[name] = validate(value[name]);
8017 return Object.freeze(trusted);
8018}; }
8019const int = (value) => { if (!Number.isSafeInteger(value)) throw new Error("int"); return value; };
8020const float = (value) => { if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("float"); return value; };
8021const string = (value) => { if (typeof value !== "string") throw new Error("string"); return value; };
8022const boolean = (value) => { if (typeof value !== "boolean") throw new Error("boolean"); return value; };
8023const optional = (validate) => (value) => value == null ? null : validate(value);
8024const array = (validate) => (value) => {
8025 if (!Array.isArray(value)) throw new Error("array");
8026 const trusted = [];
8027 for (let index = 0; index < value.length; index += 1) trusted.push(validate(value[index]));
8028 return Object.freeze(trusted);
8029};
8030const map = (validate) => (value) => {
8031 if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("map");
8032 const trusted = Object.create(null);
8033 for (const key of Object.keys(value)) trusted[key] = validate(value[key]);
8034 return Object.freeze(trusted);
8035};
8036const date = (value) => { if (typeof value !== "string") throw new Error("date"); return value; };
8037export const typeValidators = Object.freeze({
8038 "validator:endpoint.ReadItem.params": record({ id: int }),
8039 "validator:endpoint.ReadItem.query": record({ tags: (value) => { if (value !== null && (!Array.isArray(value) || value.some((item) => !Number.isSafeInteger(item)))) throw new Error("tags"); return value === null ? null : Object.freeze([...value]); }, required: (value) => { if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) throw new Error("required"); return Object.freeze([...value]); } }),
8040 "validator:endpoint.ReadItem.result": string,
8041 "validator:endpoint.SaveItem.body": record({ value: int }),
8042 "validator:endpoint.SaveItem.result": string,
8043 "validator:endpoint.SaveItem.error": string,
8044 "validator:endpoint.SlowItem.params": record({ id: string }),
8045 "validator:endpoint.SlowItem.result": boolean,
8046 "validator:endpoint.MiddlewareTimeout.result": boolean,
8047 "validator:endpoint.Upload.body": record({ text: string }),
8048 "validator:endpoint.Upload.result": int,
8049 "validator:endpoint.BodylessMutation.result": boolean,
8050 "validator:endpoint.Measure.query": record({ value: float }),
8051 "validator:endpoint.Measure.result": float,
8052 "validator:endpoint.Count.query": record({ value: int }),
8053 "validator:endpoint.Count.result": int,
8054 "validator:endpoint.FastReplay.result": int,
8055 "validator:endpoint.Events.query": record({ since: date }),
8056 "validator:endpoint.Events.result": date,
8057 "validator:endpoint.OptionalEcho.body": record({ value: optional(string) }),
8058 "validator:endpoint.OptionalEcho.result": record({ value: optional(string), nested: record({ missing: optional(string) }), values: array(optional(string)), labels: map(optional(string)) }),
8059 "validator:endpoint.RateSave.body": record({ value: int }),
8060 "validator:endpoint.RateSave.result": int,
8061 "validator:endpoint.PutItem.result": boolean,
8062 "validator:endpoint.PatchItem.result": boolean,
8063 "validator:endpoint.DirectResponse.result": string,
8064 "validator:endpoint.RedirectResponse.result": string,
8065 "validator:endpoint.DeniedWrite.body": record({ value: int }),
8066 "validator:endpoint.DeniedWrite.params": record({ id: int }),
8067 "validator:endpoint.DeniedWrite.query": record({ page: int }),
8068 "validator:endpoint.DeniedWrite.result": int,
8069});
8070"#,
8071 )
8072 .unwrap();
8073 std::fs::write(
8074 root.join("middleware.mjs"),
8075 r#"export const globalMiddleware = Object.freeze(["global"]);
8076export const globalMiddlewareHandlers = Object.freeze({ global: async ({ environment }) => { environment.trace.push("global"); return { allow: true, headers: { "x-global": "yes", "set-cookie": ["a=1; Path=/; HttpOnly", "b=2; Path=/; HttpOnly"] }, context: { sessionId: "ctx-session" } }; } });
8077export const middleware = Object.freeze({
8078 audit: async ({ environment }) => { environment.trace.push("audit"); return { allow: true, headers: { "x-audit": "yes" } }; },
8079 slow: async ({ environment }) => { environment.trace.push("slow:start"); await new Promise((resolve) => setTimeout(resolve, 60)); environment.trace.push("slow:end"); return { allow: true }; },
8080 direct: async ({ environment, host }) => { environment.trace.push("direct"); return { respond: { status: 202, contentType: "text/plain", body: await host.endpointHealth() }, headers: { "x-direct": "yes" } }; },
8081 redirect: async ({ environment, host }) => { environment.trace.push("redirect"); return { redirect: await host.afterLoginPath(), headers: { "x-redirect": "yes" } }; },
8082 deny: async ({ environment, params, query }) => { if (params.id !== "not-int" || query.unknown !== "yes") throw new Error("raw candidates missing"); environment.trace.push("deny"); return { allow: false, headers: { "x-deny": "yes" } }; },
8083});
8084"#,
8085 )
8086 .unwrap();
8087 std::fs::write(
8088 root.join("host.mjs"),
8089 r#"let saves = 0;
8090let fastReplays = 0;
8091export const endpoints = Object.freeze({
8092 "endpoint:ReadItem@1": async ({ id, tags, required }, { environment, signal }) => { if (!(signal instanceof AbortSignal)) throw new Error("signal"); environment.trace.push("read"); return `${id}:${JSON.stringify(tags)}:${JSON.stringify(required)}`; },
8093 "endpoint:SaveItem@1": async ({ value }, { environment }) => { saves += 1; environment.trace.push(`save:${saves}`); await new Promise((resolve) => setTimeout(resolve, 20)); return { tag: value < 0 ? "Err" : "Ok", value: value === 999 ? 999 : value < 0 ? "negative" : `saved:${value}:${saves}` }; },
8094 "endpoint:SlowItem@1": async (_args, { signal, environment }) => new Promise(() => { signal.addEventListener("abort", () => { environment.aborted = signal.aborted; }, { once: true }); }),
8095 "endpoint:MiddlewareTimeout@1": async (_args, { environment }) => { environment.trace.push("middleware-timeout-host"); return true; },
8096 "endpoint:Upload@1": async ({ text }, { environment }) => { environment.uploads = (environment.uploads ?? 0) + 1; return text.length; },
8097 "endpoint:BodylessMutation@1": async (_args, { environment }) => { environment.bodylessCalls = (environment.bodylessCalls ?? 0) + 1; return true; },
8098 "endpoint:Measure@1": async ({ value }, { environment }) => { environment.numericCalls = (environment.numericCalls ?? 0) + 1; return value; },
8099 "endpoint:Count@1": async ({ value }, { environment }) => { environment.numericCalls = (environment.numericCalls ?? 0) + 1; return value; },
8100 "endpoint:FastReplay@1": async () => { fastReplays += 1; return fastReplays; },
8101 "endpoint:Events@1": async ({ since }, { environment }) => { environment.dateCalls = (environment.dateCalls ?? 0) + 1; return since; },
8102 "endpoint:OptionalEcho@1": async ({ value }, { environment }) => { environment.optionalObserved = value; return { value, nested: { missing: undefined }, values: new Array(1), labels: { missing: undefined } }; },
8103 "endpoint:RateSave@1": async ({ value }, { environment }) => { environment.rateCalls = (environment.rateCalls ?? 0) + 1; return value; },
8104 "endpoint:PutItem@1": async () => true,
8105 "endpoint:PatchItem@1": async () => true,
8106 "endpoint:DeniedWrite@1": async () => { throw new Error("DENIED_HOST_MUST_NOT_RUN"); },
8107});
8108export async function authorize({ capability, environment }) { environment.trace.push(`cap:${capability}`); return environment.deny !== true; }
8109export async function endpointHealth() { return "ready-from-host"; }
8110export async function afterLoginPath() { return "/dashboard"; }
8111export async function invalidateCache(tags, { environment }) { environment.invalidated = [...tags]; return { invalidated: tags.length }; }
8112export function fastReplayCalls() { return fastReplays; }
8113"#,
8114 )
8115 .unwrap();
8116 let script = r#"import { fetch as handle } from "./handler.mjs";
8117import { fastReplayCalls } from "./host.mjs";
8118import { storage as testStorage } from "./noxid-server.js";
8119const base = "http://noxid.test/console";
8120let env = { trace: [] };
8121for (const path of ["/console/api//items/7", "/console/api/items//7", "//console/api/items/7", "/console/api/items/7/", "/console/api/items/7///", "/console/health/"]) {
8122 env = { trace: [] };
8123 const unmatched = await handle(new Request(`http://noxid.test${path}`), env);
8124 const unmatchedBody = await unmatched.json();
8125 if (unmatched.status !== 404 || unmatchedBody.error?.code !== "BOUNDARY_NOT_FOUND" || env.trace.length !== 0) throw new Error(`empty path segment aliased an endpoint ${path}: ${unmatched.status} ${JSON.stringify(unmatchedBody)} ${JSON.stringify(env.trace)}`);
8126}
8127env = { trace: [] };
8128let response = await handle(new Request(`${base}/private/not-int?unknown=yes`, { method: "POST", body: "not-json" }), env);
8129let body = await response.json();
8130if (response.status !== 403 || body.error.code !== "ENDPOINT_MIDDLEWARE_DENIED" || response.headers.get("x-deny") !== "yes") throw new Error(`middleware denial lost to body decoding ${response.status} ${JSON.stringify(body)}`);
8131if (JSON.stringify(env.trace) !== JSON.stringify(["global", "deny"])) throw new Error(`deny-before-body order failed ${JSON.stringify(env.trace)}`);
8132env = { trace: [] };
8133response = await handle(new Request(`${base}/private/not-int?unknown=yes&bad=%FF`, { method: "POST", body: "not-json" }), env);
8134body = await response.json();
8135if (response.status !== 403 || body.error.code !== "ENDPOINT_MIDDLEWARE_DENIED" || JSON.stringify(env.trace) !== JSON.stringify(["global", "deny"])) throw new Error(`middleware denial lost to malformed query ${response.status} ${JSON.stringify(body)} ${JSON.stringify(env.trace)}`);
8136env = { trace: [] };
8137response = await handle(new Request(`${base}/api/items/7`, { headers: { "x-noxid-session-id": "one" } }), env);
8138body = await response.json();
8139if (response.status !== 200 || body.value !== "7:null:[]") throw new Error(`optional/empty arrays failed ${response.status} ${JSON.stringify(body)}`);
8140if (response.headers.get("x-global") !== "yes" || response.headers.get("x-audit") !== "yes") throw new Error("middleware headers missing");
8141if (response.headers.get("x-noxid-cache-mode") !== "swr" || response.headers.get("x-noxid-cache-revalidate") !== "60" || response.headers.get("x-noxid-cache-stale") !== "60" || response.headers.get("x-noxid-cache-tags") !== "endpoint:ReadItem@1" || !response.headers.get("cache-control")?.includes("stale-while-revalidate=60")) throw new Error("endpoint cache headers missing");
8142if (JSON.stringify(env.trace) !== JSON.stringify(["global", "audit", "cap:items.read", "read"])) throw new Error(`order ${JSON.stringify(env.trace)}`);
8143env = { trace: [] };
8144response = await handle(new Request(`${base}/_noxid/revalidate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ tags: ["endpoint:ReadItem@1"] }) }), env);
8145body = await response.json();
8146if (response.status !== 200 || body.result?.invalidated !== 1 || JSON.stringify(env.invalidated) !== JSON.stringify(["endpoint:ReadItem@1"])) throw new Error(`endpoint tag revalidation failed ${response.status} ${JSON.stringify(body)}`);
8147response = await handle(new Request(`${base}/_noxid/revalidate`, { method: "POST", headers: { "content-type": "text/plain" }, body: JSON.stringify({ tags: ["endpoint:ReadItem@1"] }) }), env);
8148body = await response.json();
8149if (response.status !== 415 || body.error?.code !== "CACHE_INVALIDATION_CONTENT_TYPE") throw new Error(`invalidation media type boundary failed ${response.status} ${JSON.stringify(body)}`);
8150response = await handle(new Request(`${base}/_noxid/revalidate`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ tags: ["endpoint:ReadItem@1"], padding: "x".repeat(1_048_576) }) }), env);
8151body = await response.json();
8152if (response.status !== 413 || body.error?.code !== "CACHE_INVALIDATION_BODY_TOO_LARGE") throw new Error(`invalidation byte boundary failed ${response.status} ${JSON.stringify(body)}`);
8153env = { trace: [] };
8154response = await handle(new Request(`${base}/api/items/8?tags=%5B%5D&required=%5B%5D`, { headers: { "x-noxid-session-id": "one" } }), env);
8155body = await response.json();
8156if (response.status !== 200 || body.value !== "8:[]:[]") throw new Error("optional Some(empty array) collapsed into None");
8157response = await handle(new Request(`${base}/api/items/9?tags=%5B1%2C2%5D&required=%5B%22%22%5D`, { headers: { "x-noxid-session-id": "one" } }), env);
8158body = await response.json();
8159if (response.status !== 200 || body.value !== "9:[1,2]:[\"\"]") throw new Error("JSON array query decoding failed");
8160for (const query of ["required=%FF", "%FF=value"]) {
8161 env = { trace: [] };
8162 response = await handle(new Request(`${base}/api/items/12?${query}`), env);
8163 body = await response.json();
8164 if (response.status !== 400 || body.error.code !== "ENDPOINT_QUERY_ENCODING_INVALID" || JSON.stringify(env.trace) !== JSON.stringify(["global", "audit", "cap:items.read"])) throw new Error(`malformed query crossed endpoint boundary ${query} ${response.status} ${JSON.stringify(body)} ${JSON.stringify(env.trace)}`);
8165}
8166response = await handle(new Request(`${base}/api/items/11`, { headers: { "x-noxid-session-id": "denied" } }), { trace: [], deny: true });
8167if (response.status !== 403) throw new Error("capability denial failed");
8168response = await handle(new Request(`${base}/api/items/10`, { headers: { "x-noxid-session-id": "one" } }), { trace: [] });
8169if (response.status !== 429 || response.headers.get("retry-after") === null) throw new Error("rate limit failed");
8170response = await handle(new Request(`${base}/api/items/7`, { method: "POST" }), { trace: [] });
8171if (response.status !== 405 || response.headers.get("allow") !== "DELETE, GET") throw new Error(`method routing failed ${response.status} ${response.headers.get("allow")}`);
8172const saveRequest = () => new Request(`${base}/api/items`, { method: "POST", headers: { "content-type": "application/json", "idempotency-key": "same" }, body: JSON.stringify({ value: 4 }) });
8173env = { trace: [] };
8174response = await handle(new Request(`${base}/api/items`, { method: "POST", body: JSON.stringify({ value: 4 }) }), env);
8175if (response.status !== 415) throw new Error("content type refusal failed");
8176if (JSON.stringify(env.trace) !== JSON.stringify(["global", "audit"])) throw new Error(`allowed middleware did not precede body validation ${JSON.stringify(env.trace)}`);
8177response = await handle(new Request(`${base}/api/items`, { method: "POST", headers: { "content-type": "application/json", "content-length": "1048577", "idempotency-key": "large" }, body: "{}" }), { trace: [] });
8178if (response.status !== 413) throw new Error("body size refusal failed");
8179response = await handle(new Request(`${base}/api/items`, { method: "POST", headers: { "content-type": "application/json", "idempotency-key": "typed" }, body: JSON.stringify({ value: "wrong" }) }), { trace: [] });
8180if (response.status !== 422) throw new Error("body type refusal failed");
8181response = await handle(new Request(`${base}/api/items`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ value: 4 }) }), { trace: [] });
8182if (response.status !== 400) throw new Error("idempotency key refusal failed");
8183env = { trace: [] };
8184const [first, second] = await Promise.all([handle(saveRequest(), env), handle(saveRequest(), env)]);
8185const [firstText, secondText] = await Promise.all([first.text(), second.text()]);
8186if (first.status !== 200 || second.status !== 200 || firstText !== secondText) throw new Error("concurrent replay body/status failed");
8187if (JSON.parse(firstText).value !== "saved:4:1") throw new Error(`body refusals invoked host before allowed request ${firstText}`);
8188if ([...first.headers].toString() !== [...second.headers].toString() || first.headers.get("x-global") !== "yes" || first.headers.get("x-audit") !== "yes") throw new Error("full replay headers failed");
8189if (typeof first.headers.getSetCookie === "function" && (first.headers.getSetCookie().length !== 2 || second.headers.getSetCookie().length !== 2)) throw new Error("full replay set-cookie headers failed");
8190if (env.trace.filter((entry) => entry.startsWith("save:")).length !== 1) throw new Error(`duplicate implementation ran ${JSON.stringify(env.trace)}`);
8191response = await handle(saveRequest(), { trace: [] });
8192if (await response.text() !== firstText) throw new Error("sequential replay failed");
8193response = await handle(new Request(`${base}/api/items`, { method: "POST", headers: { "content-type": "application/json", "idempotency-key": "err" }, body: JSON.stringify({ value: -1 }) }), { trace: [] });
8194body = await response.json();
8195if (response.status !== 422 || body.error.value !== "negative") throw new Error("typed Result Err failed");
8196response = await handle(new Request(`${base}/api/items`, { method: "POST", headers: { "content-type": "application/json", "idempotency-key": "bad-result" }, body: JSON.stringify({ value: 999 }) }), { trace: [] });
8197body = await response.json();
8198if (response.status !== 500 || body.error.code !== "ENDPOINT_RESULT_TYPE") throw new Error("result validation refusal failed");
8199env = { trace: [] };
8200response = await handle(new Request(`${base}/api/items/a`, { method: "DELETE" }), env);
8201body = await response.json();
8202if (response.status !== 504 || body.error.code !== "ENDPOINT_TIMEOUT" || env.aborted !== true) throw new Error(`timeout did not abort signal ${response.status} ${JSON.stringify(body)} ${env.aborted}`);
8203env = { trace: [] };
8204const middlewareStarted = Date.now();
8205response = await handle(new Request(`${base}/api/middleware-timeout`), env);
8206body = await response.json();
8207const middlewareElapsed = Date.now() - middlewareStarted;
8208if (response.status !== 504 || body.error.code !== "ENDPOINT_TIMEOUT" || middlewareElapsed >= 50) throw new Error(`middleware escaped deadline ${response.status} ${middlewareElapsed} ${JSON.stringify(body)}`);
8209await new Promise((resolve) => setTimeout(resolve, 70));
8210if (env.trace.includes("middleware-timeout-host")) throw new Error(`host ran after middleware timeout ${JSON.stringify(env.trace)}`);
8211const oversizedText = "é".repeat(600_000);
8212const oversizedWire = JSON.stringify({ text: oversizedText });
8213if (new TextEncoder().encode(oversizedWire).byteLength <= 1_048_576 || oversizedWire.length >= 1_048_576) throw new Error("UTF-8 byte fixture is invalid");
8214env = { trace: [], uploads: 0 };
8215response = await handle(new Request(`${base}/api/upload`, { method: "POST", headers: { "content-type": "application/json" }, body: oversizedWire }), env);
8216body = await response.json();
8217if (response.status !== 413 || body.error.code !== "ENDPOINT_BODY_TOO_LARGE" || env.uploads !== 0) throw new Error(`UTF-8 byte limit failed ${response.status} ${JSON.stringify(body)} ${env.uploads}`);
8218const oversizedUndeclaredWire = JSON.stringify({ ignored: "x".repeat(1_048_576) });
8219env = { trace: [], bodylessCalls: 0 };
8220response = await handle(new Request(`${base}/api/bodyless`, { method: "POST", headers: { "content-type": "application/json" }, body: oversizedUndeclaredWire }), env);
8221body = await response.json();
8222if (response.status !== 413 || body.error.code !== "ENDPOINT_BODY_TOO_LARGE" || env.bodylessCalls !== 0) throw new Error(`bodyless mutation skipped transport cap ${response.status} ${JSON.stringify(body)} ${env.bodylessCalls}`);
8223response = await handle(new Request(`${base}/api/bodyless`, { method: "POST" }), env);
8224if (response.status !== 200 || env.bodylessCalls !== 1) throw new Error(`absent empty body was not accepted ${response.status} ${env.bodylessCalls}`);
8225response = await handle(new Request(`${base}/api/bodyless`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }), env);
8226if (response.status !== 200 || env.bodylessCalls !== 2) throw new Error(`declared empty JSON body was not accepted ${response.status} ${env.bodylessCalls}`);
8227response = await handle(new Request(`${base}/api/bodyless`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ignored: true }) }), env);
8228body = await response.json();
8229if (response.status !== 400 || body.error.code !== "ENDPOINT_BODY_UNKNOWN" || env.bodylessCalls !== 2) throw new Error(`undeclared body field reached host ${response.status} ${JSON.stringify(body)} ${env.bodylessCalls}`);
8230for (const value of ["0x10", "0b10", "0o10", "Infinity", "1e3", "+1.0", "1", ".5", "1.", "01.5", " "]) {
8231 env = { trace: [], numericCalls: 0 };
8232 response = await handle(new Request(`${base}/api/measure?value=${encodeURIComponent(value)}`), env);
8233 body = await response.json();
8234 if (response.status !== 422 || body.error.code !== "ENDPOINT_QUERY_TYPE" || env.numericCalls !== 0) throw new Error(`noncanonical Float passed ${JSON.stringify(value)} ${response.status} ${JSON.stringify(body)} ${env.numericCalls}`);
8235}
8236env = { trace: [], numericCalls: 0 };
8237response = await handle(new Request(`${base}/api/measure?value=-1.25`), env);
8238body = await response.json();
8239if (response.status !== 200 || body.value !== -1.25 || env.numericCalls !== 1) throw new Error(`canonical Float failed ${response.status} ${JSON.stringify(body)} ${env.numericCalls}`);
8240for (const value of ["0x10", "1.0", "1e2", "+1", "01", " "]) {
8241 env = { trace: [], numericCalls: 0 };
8242 response = await handle(new Request(`${base}/api/count?value=${encodeURIComponent(value)}`), env);
8243 body = await response.json();
8244 if (response.status !== 422 || body.error.code !== "ENDPOINT_QUERY_TYPE" || env.numericCalls !== 0) throw new Error(`noncanonical Int passed ${JSON.stringify(value)} ${response.status} ${JSON.stringify(body)} ${env.numericCalls}`);
8245}
8246env = { trace: [], numericCalls: 0 };
8247response = await handle(new Request(`${base}/api/count?value=-12`), env);
8248body = await response.json();
8249if (response.status !== 200 || body.value !== -12 || env.numericCalls !== 1) throw new Error(`canonical Int failed ${response.status} ${JSON.stringify(body)} ${env.numericCalls}`);
8250for (const since of ["01/02/2020", "2021-02-29T00:00:00Z", "2020-01-01T24:00:00Z", "2020-01-01T00:00:00+00:00"]) {
8251 env = { trace: [], dateCalls: 0 };
8252 response = await handle(new Request(`${base}/api/events?since=${encodeURIComponent(since)}`), env);
8253 body = await response.json();
8254 if (response.status !== 422 || body.error.code !== "ENDPOINT_QUERY_TYPE" || env.dateCalls !== 0) throw new Error(`invalid Date crossed endpoint boundary ${since} ${response.status} ${JSON.stringify(body)} ${env.dateCalls}`);
8255}
8256env = { trace: [], dateCalls: 0 };
8257response = await handle(new Request(`${base}/api/events?since=${encodeURIComponent("2020-02-29T23:59:59.123Z")}`), env);
8258body = await response.json();
8259if (response.status !== 200 || body.value !== "2020-02-29T23:59:59.123Z" || env.dateCalls !== 1) throw new Error(`valid UTC Date failed ${response.status} ${JSON.stringify(body)} ${env.dateCalls}`);
8260env = { trace: [], optionalObserved: "unset" };
8261response = await handle(new Request(`${base}/api/optional`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}" }), env);
8262body = await response.json();
8263if (response.status !== 200 || env.optionalObserved !== null || !Object.hasOwn(body.value, "value") || body.value.value !== null || body.value.nested.missing !== null || body.value.values[0] !== null || body.value.labels.missing !== null) throw new Error(`recursive Optional None was not serialized as null ${response.status} ${JSON.stringify(body)} ${String(env.optionalObserved)}`);
8264env = { trace: [], rateCalls: 0 };
8265response = await handle(new Request(`${base}/api/rate-save`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ value: "wrong" }) }), env);
8266body = await response.json();
8267if (response.status !== 422 || body.error.code !== "ENDPOINT_INPUT_TYPE" || env.rateCalls !== 0) throw new Error(`rate identity preempted invalid input ${response.status} ${JSON.stringify(body)} ${env.rateCalls}`);
8268response = await handle(new Request(`${base}/api/rate-save`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ value: 1 }) }), env);
8269body = await response.json();
8270if (response.status !== 403 || body.error.code !== "ENDPOINT_RATE_IDENTITY_REQUIRED" || env.rateCalls !== 0) throw new Error(`valid input did not reach rate identity boundary ${response.status} ${JSON.stringify(body)} ${env.rateCalls}`);
8271env = { trace: [] };
8272response = await handle(new Request(`${base}/api/items/%FF`), env);
8273body = await response.json();
8274if (response.status !== 400 || body.error.code !== "ENDPOINT_PATH_ENCODING_INVALID" || env.trace.length !== 0) throw new Error(`malformed dynamic path escaped endpoint ownership ${response.status} ${JSON.stringify(body)} ${JSON.stringify(env.trace)}`);
8275const fastReplayRequest = (key) => new Request(`${base}/api/fast-replay`, { method: "POST", headers: { "idempotency-key": key } });
8276for (let index = 0; index < 1024; index += 1) {
8277 response = await handle(fastReplayRequest(`capacity-${index}`), { trace: [] });
8278 if (response.status !== 200) throw new Error(`idempotency capacity fill failed ${index} ${response.status}`);
8279}
8280if (fastReplayCalls() !== 1024) throw new Error(`capacity fill executed wrong count ${fastReplayCalls()}`);
8281response = await handle(fastReplayRequest("capacity-0"), { trace: [] });
8282body = await response.json();
8283if (response.status !== 200 || body.value !== 1 || fastReplayCalls() !== 1024) throw new Error(`capacity replay evicted itself ${response.status} ${JSON.stringify(body)} ${fastReplayCalls()}`);
8284response = await handle(fastReplayRequest("capacity-new"), { trace: [] });
8285if (response.status !== 200 || fastReplayCalls() !== 1025) throw new Error(`new-key capacity admission failed ${response.status} ${fastReplayCalls()}`);
8286const idempotencyStorage = testStorage("noxid:endpoint-idempotency");
8287for (const key of await idempotencyStorage.list()) await idempotencyStorage.delete(key);
8288for (let index = 0; index < 1024; index += 1) await idempotencyStorage.set(`corrupt-${index}`, { malformed: true });
8289response = await handle(fastReplayRequest("after-corruption"), { trace: [] });
8290if (response.status !== 200 || (await idempotencyStorage.list()).length !== 1) throw new Error(`corrupt records defeated idempotency capacity ${response.status} ${(await idempotencyStorage.list()).length}`);
8291for (const key of await idempotencyStorage.list()) await idempotencyStorage.delete(key);
8292await idempotencyStorage.set("endpoint:SaveItem@1\nsession:ctx-session\ncorrupt-snapshot", { created: Date.now(), status: 200, headers: [], body: "%%%" });
8293response = await handle(new Request(`${base}/api/items`, { method: "POST", headers: { "content-type": "application/json", "idempotency-key": "corrupt-snapshot" }, body: JSON.stringify({ value: 10 }) }), { trace: [] });
8294body = await response.json();
8295if (response.status !== 500 || body.error.code !== "ENDPOINT_STORAGE_FAILED") throw new Error(`invalid stored response escaped containment ${response.status} ${JSON.stringify(body)}`);
8296response = await handle(new Request(`${base}/api/put`, { method: "PUT" }), { trace: [] });
8297if (response.status !== 200) throw new Error("put failed");
8298response = await handle(new Request(`${base}/api/patch`, { method: "PATCH" }), { trace: [] });
8299if (response.status !== 200) throw new Error("patch failed");
8300response = await handle(new Request(`${base}/api/items/nope`, { method: "DELETE" }), { trace: [] });
8301if (response.status !== 504) throw new Error("delete matcher failed");
8302response = await handle(new Request(`${base}/api/missing`), { trace: [] });
8303if (response.status !== 404) throw new Error("unknown endpoint failed");
8304env = { trace: [] };
8305response = await handle(new Request(`${base}/health`), env);
8306if (response.status !== 202 || await response.text() !== "ready-from-host" || response.headers.get("x-global") !== "yes" || response.headers.get("x-direct") !== "yes") throw new Error("host-backed direct middleware response/headers failed");
8307if (JSON.stringify(env.trace) !== JSON.stringify(["global", "direct"])) throw new Error(`direct middleware order failed ${JSON.stringify(env.trace)}`);
8308env = { trace: [] };
8309response = await handle(new Request(`${base}/login`), env);
8310if (response.status !== 307 || response.headers.get("location") !== "/console/dashboard" || response.headers.get("x-global") !== "yes" || response.headers.get("x-redirect") !== "yes") throw new Error("host-backed middleware redirect/headers failed");
8311if (JSON.stringify(env.trace) !== JSON.stringify(["global", "redirect"])) throw new Error(`redirect middleware order failed ${JSON.stringify(env.trace)}`);
8312"#;
8313 let output = Command::new("node")
8314 .args(["--input-type=module", "-e", script])
8315 .current_dir(&root)
8316 .output()
8317 .expect("Node.js is required for generated endpoint handler tests");
8318 let _ = std::fs::remove_dir_all(&root);
8319 assert!(
8320 output.status.success(),
8321 "{}\n{}",
8322 String::from_utf8_lossy(&output.stdout),
8323 String::from_utf8_lossy(&output.stderr)
8324 );
8325 }
8326
8327 #[test]
8328 fn generated_stream_endpoint_validates_frames_resumes_times_out_and_cancels() {
8329 let mut events = endpoint_boundary(
8330 "Events",
8331 EndpointMethod::Get,
8332 "/api/events",
8333 vec![endpoint_input(
8334 "Events",
8335 EndpointInputSection::Query,
8336 "project",
8337 "String",
8338 )],
8339 "Int",
8340 );
8341 events.kind = EndpointKind::Stream;
8342 events.capabilities = vec!["events.read".into()];
8343 events.timeout_ms = 500;
8344 let mut invalid = endpoint_boundary(
8345 "InvalidEvents",
8346 EndpointMethod::Get,
8347 "/api/invalid-events",
8348 vec![],
8349 "Int",
8350 );
8351 invalid.kind = EndpointKind::Stream;
8352 invalid.timeout_ms = 500;
8353 let mut slow = endpoint_boundary(
8354 "SlowEvents",
8355 EndpointMethod::Get,
8356 "/api/slow-events",
8357 vec![],
8358 "Int",
8359 );
8360 slow.kind = EndpointKind::Stream;
8361 slow.timeout_ms = 20;
8362 let mut cancellable = endpoint_boundary(
8363 "CancellableEvents",
8364 EndpointMethod::Get,
8365 "/api/cancellable-events",
8366 vec![],
8367 "Int",
8368 );
8369 cancellable.kind = EndpointKind::Stream;
8370 cancellable.timeout_ms = 500;
8371 let mut missing = endpoint_boundary(
8372 "MissingEvents",
8373 EndpointMethod::Get,
8374 "/api/missing-events",
8375 vec![],
8376 "Int",
8377 );
8378 missing.kind = EndpointKind::Stream;
8379 missing.timeout_ms = 500;
8380 let mut result_events = endpoint_boundary(
8381 "ResultEvents",
8382 EndpointMethod::Get,
8383 "/api/result-events",
8384 vec![],
8385 "Result<Int, String>",
8386 );
8387 result_events.kind = EndpointKind::Stream;
8388 result_events.timeout_ms = 500;
8389 let program = ExecutionProgram {
8390 live_resources: vec![],
8391 presences: vec![],
8392 boundaries: vec![],
8393 endpoints: vec![events, invalid, slow, cancellable, missing, result_events],
8394 tasks: vec![],
8395 queues: vec![],
8396 };
8397 let output = generate(
8398 &program,
8399 "./host.mjs",
8400 "./validators.mjs",
8401 "./middleware.mjs",
8402 "/",
8403 &[],
8404 );
8405 assert!(
8406 output
8407 .handler
8408 .contains("ENDPOINT_STREAM_HEARTBEAT_MS = 15_000")
8409 );
8410 assert!(output.handler.contains("ENDPOINT_STREAM_MAX_EVENTS = 256"));
8411 assert!(
8412 !output
8413 .handler
8414 .contains("validator:endpoint.ResultEvents.error")
8415 );
8416
8417 let unique = SystemTime::now()
8418 .duration_since(UNIX_EPOCH)
8419 .unwrap()
8420 .as_nanos();
8421 let root = std::env::temp_dir().join(format!(
8422 "noxid-stream-endpoint-handler-{}-{unique}",
8423 std::process::id()
8424 ));
8425 std::fs::create_dir_all(&root).unwrap();
8426 std::fs::write(root.join("handler.mjs"), output.handler).unwrap();
8427 std::fs::write(
8428 root.join("validators.mjs"),
8429 r#"const int = (value) => { if (!Number.isSafeInteger(value)) throw new Error("Int"); return value; };
8430export const typeValidators = Object.freeze({
8431 "validator:endpoint.Events.query": (value) => { if (!value || typeof value.project !== "string") throw new Error("project"); return Object.freeze({ project: value.project }); },
8432 "validator:endpoint.Events.result": int,
8433 "validator:endpoint.InvalidEvents.result": int,
8434 "validator:endpoint.SlowEvents.result": int,
8435 "validator:endpoint.CancellableEvents.result": int,
8436 "validator:endpoint.MissingEvents.result": int,
8437});
8438"#,
8439 )
8440 .unwrap();
8441 std::fs::write(
8442 root.join("middleware.mjs"),
8443 "export const middleware = Object.freeze({});\nexport const globalMiddleware = Object.freeze([]);\n",
8444 )
8445 .unwrap();
8446 std::fs::write(
8447 root.join("host.mjs"),
8448 r#"export const endpoints = Object.freeze({
8449 "endpoint:Events@1": async function* (_args, context) {
8450 context.environment.eventCalls = (context.environment.eventCalls ?? 0) + 1;
8451 yield 1; yield 2; yield 3;
8452 },
8453 "endpoint:InvalidEvents@1": async function* () { yield 1; yield "bad"; yield 3; },
8454 "endpoint:SlowEvents@1": async function* () { yield 1; await new Promise((resolve) => setTimeout(resolve, 100)); yield 2; },
8455 "endpoint:CancellableEvents@1": async function* (_args, context) {
8456 try { yield 1; await new Promise((resolve) => setTimeout(resolve, 100)); yield 2; }
8457 finally { context.environment.cancelled = (context.environment.cancelled ?? 0) + 1; }
8458 },
8459});
8460export async function authorize({ capability, request }) {
8461 return capability === "events.read" && request.headers.get("x-events") === "yes";
8462}
8463"#,
8464 )
8465 .unwrap();
8466 let script = r#"import { fetch as handle } from "./handler.mjs";
8467const base = "http://noxid.test";
8468let environment = { eventCalls: 0 };
8469environment.sessionId = "session-a";
8470let response = await handle(new Request(`${base}/api/events?project=A`), environment);
8471let denied = await response.json();
8472if (response.status !== 403 || denied.error.code !== "ENDPOINT_CAPABILITY_DENIED" || environment.eventCalls !== 0) throw new Error(`stream authorization failed ${response.status} ${JSON.stringify(denied)}`);
8473
8474response = await handle(new Request(`${base}/api/events?project=A`, { headers: { "x-events": "yes" } }), environment);
8475if (response.status !== 200 || response.headers.get("content-type") !== "text/event-stream; charset=utf-8" || response.headers.get("cache-control") !== "no-store") throw new Error("SSE headers failed");
8476const first = await response.text();
8477if (environment.eventCalls !== 1) throw new Error(`host call count ${environment.eventCalls}`);
8478const ids = [...first.matchAll(/^id: ([^\n]+)$/gm)].map((match) => match[1]);
8479if (ids.length !== 3 || !ids.every((id, index) => id.endsWith(`:${index + 1}`))) throw new Error(`SSE ids failed ${JSON.stringify(ids)} ${first}`);
8480if (!first.includes("event: message\ndata: 1\n\n") || !first.includes("event: message\ndata: 3\n\n")) throw new Error(`typed SSE frames failed ${first}`);
8481
8482response = await handle(new Request(`${base}/api/events?project=A`, { headers: { "x-events": "yes", "last-event-id": ids[0] } }), environment);
8483const replay = await response.text();
8484if (environment.eventCalls !== 1 || replay.includes("data: 1\n") || !replay.includes("data: 2\n") || !replay.includes("data: 3\n")) throw new Error(`resume failed calls=${environment.eventCalls} ${replay}`);
8485response = await handle(new Request(`${base}/api/events?project=B`, { headers: { "x-events": "yes", "last-event-id": ids[0] } }), environment);
8486const crossArgument = await response.text();
8487if (!crossArgument.includes("STREAM_RESUME_UNAVAILABLE") || !crossArgument.includes("request-mismatch") || environment.eventCalls !== 1 || crossArgument.includes("data: 2\n")) throw new Error(`cross-argument replay escaped isolation ${crossArgument}`);
8488const otherIdentity = { eventCalls: 0, sessionId: "session-b" };
8489response = await handle(new Request(`${base}/api/events?project=A`, { headers: { "x-events": "yes", "last-event-id": ids[0] } }), otherIdentity);
8490const crossIdentity = await response.text();
8491if (!crossIdentity.includes("STREAM_RESUME_UNAVAILABLE") || !crossIdentity.includes("request-mismatch") || otherIdentity.eventCalls !== 0 || crossIdentity.includes("data: 2\n")) throw new Error(`cross-identity replay escaped isolation ${crossIdentity}`);
8492const disconnected = new AbortController();
8493disconnected.abort("already disconnected");
8494const disconnectedEnvironment = { eventCalls: 0, sessionId: "session-a" };
8495response = await handle(new Request(`${base}/api/events?project=A`, { headers: { "x-events": "yes" }, signal: disconnected.signal }), disconnectedEnvironment);
8496if (response.status !== 200 || await response.text() !== "" || disconnectedEnvironment.eventCalls !== 0) throw new Error(`already-aborted request invoked stream host ${disconnectedEnvironment.eventCalls}`);
8497response = await handle(new Request(`${base}/api/events?project=A`, { headers: { "x-events": "yes", "last-event-id": "forged" } }), environment);
8498const malformed = await response.text();
8499if (!malformed.includes("event: noxid-error") || !malformed.includes("STREAM_RESUME_UNAVAILABLE") || environment.eventCalls !== 1) throw new Error(`malformed resume did not fail closed ${malformed}`);
8500
8501response = await handle(new Request(`${base}/api/invalid-events`));
8502const invalid = await response.text();
8503if (!invalid.includes("data: 1\n") || invalid.includes("data: 3\n") || !invalid.includes("event: noxid-error") || !invalid.includes("STREAM_EVENT_TYPE")) throw new Error(`invalid event did not terminate ${invalid}`);
8504
8505response = await handle(new Request(`${base}/api/missing-events`));
8506const missing = await response.text();
8507if (response.status !== 200 || !missing.includes("event: noxid-error") || !missing.includes("STREAM_IMPLEMENTATION_MISSING")) throw new Error(`missing implementation was not structured SSE ${response.status} ${missing}`);
8508
8509const started = Date.now();
8510response = await handle(new Request(`${base}/api/slow-events`));
8511const timedOut = await response.text();
8512const elapsed = Date.now() - started;
8513if (!timedOut.includes("data: 1\n") || timedOut.includes("data: 2\n") || !timedOut.includes("ENDPOINT_TIMEOUT") || elapsed >= 80) throw new Error(`connection timeout failed elapsed=${elapsed} ${timedOut}`);
8514
8515environment = { cancelled: 0 };
8516response = await handle(new Request(`${base}/api/cancellable-events`), environment);
8517const reader = response.body.getReader();
8518const firstChunk = await reader.read();
8519if (firstChunk.done || !new TextDecoder().decode(firstChunk.value).includes("data: 1")) throw new Error("cancellable stream did not start");
8520await reader.cancel("client disconnected");
8521await new Promise((resolve) => setTimeout(resolve, 10));
8522if (environment.cancelled !== 1) throw new Error(`disconnect did not cancel iterator ${environment.cancelled}`);
8523"#;
8524 let output = Command::new("node")
8525 .args(["--input-type=module", "-e", script])
8526 .current_dir(&root)
8527 .output()
8528 .expect("Node.js is required for generated stream endpoint tests");
8529 let _ = std::fs::remove_dir_all(&root);
8530 assert!(
8531 output.status.success(),
8532 "{}\n{}",
8533 String::from_utf8_lossy(&output.stdout),
8534 String::from_utf8_lossy(&output.stderr)
8535 );
8536 }
8537
8538 #[test]
8539 fn generated_mcp_surface_lists_exact_openapi_tools_and_reuses_endpoint_pipeline() {
8540 let mut widget = endpoint_boundary(
8541 "GetWidget",
8542 EndpointMethod::Post,
8543 "/api/widgets/[id]",
8544 vec![
8545 endpoint_input("GetWidget", EndpointInputSection::Params, "id", "String"),
8546 endpoint_input(
8547 "GetWidget",
8548 EndpointInputSection::Query,
8549 "tags",
8550 "Optional<Array<String>>",
8551 ),
8552 endpoint_input(
8553 "GetWidget",
8554 EndpointInputSection::Body,
8555 "input",
8556 "WidgetInput",
8557 ),
8558 ],
8559 "Result<Widget, String>",
8560 );
8561 widget.description = Some("Fetch one typed widget".into());
8562 widget.capabilities = vec!["widgets.read".into()];
8563 widget.middleware = vec!["audit".into()];
8564 widget.inputs[2].type_id = Some(SemanticId::type_definition("Api", "WidgetInput"));
8565 widget.result.type_id = Some(SemanticId::type_definition("Api", "Widget"));
8566 let mut events = endpoint_boundary(
8567 "WidgetEvents",
8568 EndpointMethod::Get,
8569 "/api/widget-events",
8570 vec![],
8571 "Widget",
8572 );
8573 events.kind = EndpointKind::Stream;
8574 events.description = Some("Watch typed widgets".into());
8575 events.timeout_ms = 200;
8576 events.result.type_id = Some(SemanticId::type_definition("Api", "Widget"));
8577 let program = ExecutionProgram {
8578 live_resources: vec![],
8579 presences: vec![],
8580 boundaries: vec![],
8581 endpoints: vec![widget, events],
8582 tasks: vec![],
8583 queues: vec![],
8584 };
8585 let openapi = r##"{"openapi":"3.1.0","info":{"title":"MCP fixture","version":"0.1.0"},"paths":{"/api/widgets/{id}":{"post":{"operationId":"GetWidget","description":"Fetch one typed widget","x-noxid-endpoint-id":"endpoint:GetWidget@1","x-noxid-signature":"GetWidget(params { id: String }, query { tags: Optional<Array<String>> }, body { input: WidgetInput }) -> Result<Widget, String>","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"tags","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"input":{"$ref":"#/components/schemas/WidgetInput"}},"required":["input"],"additionalProperties":false}}}},"responses":{"200":{"description":"Typed endpoint result","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"const":true},"value":{"$ref":"#/components/schemas/Widget"}},"required":["ok","value"],"additionalProperties":false}}}}}}},"/api/widget-events":{"get":{"operationId":"WidgetEvents","description":"Watch typed widgets","x-noxid-endpoint-id":"endpoint:WidgetEvents@1","x-noxid-signature":"WidgetEvents() -> Stream<Widget>","responses":{"200":{"description":"SSE event stream","content":{"text/event-stream":{"schema":{"type":"string"},"x-noxid-event-schema":{"$ref":"#/components/schemas/Widget"}}}}}}}},"components":{"schemas":{"Nested":{"type":"object","properties":{"scores":{"type":"array","items":{"type":"integer"}}},"required":["scores"],"additionalProperties":false},"WidgetInput":{"type":"object","properties":{"label":{"type":"string"},"note":{"anyOf":[{"type":"string"},{"type":"null"}]},"nested":{"$ref":"#/components/schemas/Nested"}},"required":["label","nested"],"additionalProperties":false},"Widget":{"type":"object","properties":{"name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"nested":{"$ref":"#/components/schemas/Nested"}},"required":["name","tags","nested"],"additionalProperties":false},"NoxidErrorResponse":{"type":"object","properties":{"ok":{"const":false},"error":{"type":"object"}},"required":["ok","error"]}}}}"##;
8586 let enabled = generate_with_agent_surfaces(
8587 &program,
8588 "./host.mjs",
8589 "./validators.mjs",
8590 "./middleware.mjs",
8591 None,
8592 "/console",
8593 &[],
8594 AgentSurfaceOptions {
8595 openapi_json: Some(openapi),
8596 serve_openapi: false,
8597 mcp: true,
8598 principal_authority_import: None,
8599 },
8600 );
8601 let docs = generate_with_agent_surfaces(
8602 &program,
8603 "./host.mjs",
8604 "./validators.mjs",
8605 "./middleware.mjs",
8606 None,
8607 "/console",
8608 &[],
8609 AgentSurfaceOptions {
8610 openapi_json: Some(openapi),
8611 serve_openapi: true,
8612 mcp: false,
8613 principal_authority_import: None,
8614 },
8615 );
8616 let off = generate(
8617 &program,
8618 "./host.mjs",
8619 "./validators.mjs",
8620 "./middleware.mjs",
8621 "/console",
8622 &[],
8623 );
8624
8625 let unique = SystemTime::now()
8626 .duration_since(UNIX_EPOCH)
8627 .unwrap()
8628 .as_nanos();
8629 let root = std::env::temp_dir().join(format!(
8630 "noxid-mcp-endpoint-handler-{}-{unique}",
8631 std::process::id()
8632 ));
8633 std::fs::create_dir_all(&root).unwrap();
8634 std::fs::write(root.join("handler.mjs"), enabled.handler).unwrap();
8635 std::fs::write(root.join("handler-docs.mjs"), docs.handler).unwrap();
8636 std::fs::write(root.join("handler-off.mjs"), off.handler).unwrap();
8637 std::fs::write(root.join("openapi.json"), openapi).unwrap();
8638 std::fs::write(
8639 root.join("validators.mjs"),
8640 r#"const fail = (message) => { throw new Error(message); };
8641const nested = (value) => value && Array.isArray(value.scores) && value.scores.every(Number.isSafeInteger) ? Object.freeze({ scores: Object.freeze([...value.scores]) }) : fail("Nested");
8642const input = (value) => value && typeof value.label === "string" && (value.note === null || value.note === undefined || typeof value.note === "string") ? Object.freeze({ label: value.label, note: value.note ?? null, nested: nested(value.nested) }) : fail("WidgetInput");
8643const widget = (value) => value && typeof value.name === "string" && Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string") ? Object.freeze({ name: value.name, tags: Object.freeze([...value.tags]), nested: nested(value.nested) }) : fail("Widget");
8644export const typeValidators = Object.freeze({
8645 "validator:endpoint.GetWidget.params": (value) => typeof value.id === "string" ? Object.freeze({ id: value.id }) : fail("params"),
8646 "validator:endpoint.GetWidget.query": (value) => value.tags === null || (Array.isArray(value.tags) && value.tags.every((tag) => typeof tag === "string")) ? Object.freeze({ tags: value.tags === null ? null : Object.freeze([...value.tags]) }) : fail("query"),
8647 "validator:endpoint.GetWidget.body": (value) => Object.freeze({ input: input(value.input) }),
8648 "validator:endpoint.GetWidget.result": widget,
8649 "validator:endpoint.GetWidget.error": (value) => typeof value === "string" ? value : fail("error"),
8650 "validator:endpoint.WidgetEvents.result": widget,
8651});
8652"#,
8653 )
8654 .unwrap();
8655 std::fs::write(
8656 root.join("middleware.mjs"),
8657 r#"export const globalMiddleware = Object.freeze([]);
8658export const middleware = Object.freeze({ audit: async ({ request, environment }) => {
8659 environment.trace.push(`middleware:${new URL(request.url).pathname}:${request.headers.get("x-session")}`);
8660 return { allow: true, context: { sessionId: request.headers.get("x-session") } };
8661} });
8662"#,
8663 )
8664 .unwrap();
8665 std::fs::write(
8666 root.join("host.mjs"),
8667 r#"export const endpoints = Object.freeze({
8668 "endpoint:GetWidget@1": async ({ id, tags, input }, context) => {
8669 context.environment.trace.push(`host:${id}:${context.middlewareContext.sessionId}`);
8670 context.environment.hostCalls = (context.environment.hostCalls ?? 0) + 1;
8671 return { tag: "Ok", value: { name: input.label, tags: tags ?? [], nested: input.nested } };
8672 },
8673 "endpoint:WidgetEvents@1": async function* () {
8674 yield { name: "one", tags: ["live"], nested: { scores: [1] } };
8675 yield { name: "two", tags: [], nested: { scores: [2, 3] } };
8676 },
8677});
8678export async function authorize({ capability, request, environment }) {
8679 environment.trace.push(`authorize:${capability}`);
8680 return request.headers.get("x-capability") === capability;
8681}
8682"#,
8683 )
8684 .unwrap();
8685 let script = r##"import { readFile } from "node:fs/promises";
8686import { fetch as handle } from "./handler.mjs";
8687import { fetch as handleDocs } from "./handler-docs.mjs";
8688import { fetch as handleOff } from "./handler-off.mjs";
8689const expectedOpenApi = await readFile("./openapi.json", "utf8");
8690const url = "http://noxid.test/console/_noxid/mcp";
8691const request = (id, method, params = {}, headers = {}) => new Request(url, {
8692 method: "POST",
8693 headers: { "content-type": "application/json", accept: "application/json, text/event-stream", "mcp-protocol-version": "2025-11-25", ...headers },
8694 body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
8695});
8696
8697let response = await handleOff(request(1, "tools/list"), { trace: [] });
8698if (response.status !== 404 || (await response.json()).error.code !== "AGENT_SURFACE_DISABLED") throw new Error("default-off MCP door did not 404");
8699response = await handleOff(new Request("http://noxid.test/console/_noxid/openapi.json"), { trace: [] });
8700if (response.status !== 404) throw new Error("default-off OpenAPI door did not 404");
8701response = await handle(new Request("http://noxid.test/console/_noxid/openapi.json"), { trace: [] });
8702if (response.status !== 404) throw new Error("MCP-only build exposed OpenAPI");
8703response = await handleDocs(new Request("http://noxid.test/console/_noxid/openapi.json"), { trace: [] });
8704if (response.status !== 200 || await response.text() !== expectedOpenApi) throw new Error("OpenAPI document was not served exactly");
8705
8706response = await handle(request(2, "initialize", { protocolVersion: "2025-11-25", capabilities: {}, clientInfo: { name: "test", version: "1" } }), { trace: [] });
8707let rpc = await response.json();
8708if (response.status !== 200 || rpc.result.protocolVersion !== "2025-11-25" || rpc.result.serverInfo.name !== "noxid-endpoints") throw new Error(`initialize failed ${JSON.stringify(rpc)}`);
8709response = await handle(request(3, "tools/list"), { trace: [] });
8710rpc = await response.json();
8711const tools = rpc.result.tools;
8712if (tools.length !== 2 || new Set(tools.map((tool) => tool.name)).size !== 2 || tools.map((tool) => tool.name).sort().join(",") !== "GetWidget,WidgetEvents") throw new Error(`tool list diverged ${JSON.stringify(tools)}`);
8713const get = tools.find((tool) => tool.name === "GetWidget");
8714if (get.description !== "Fetch one typed widget" || get["x-noxid-endpoint"].signature.includes("Result<Widget, String>") !== true) throw new Error("description/signature missing");
8715if (get.inputSchema.properties.input.$ref !== "#/$defs/WidgetInput" || get.inputSchema.$defs.WidgetInput.properties.nested.$ref !== "#/$defs/Nested" || get.inputSchema.$defs.Nested.properties.scores.items.type !== "integer") throw new Error(`nested input schema drifted ${JSON.stringify(get.inputSchema)}`);
8716if (get.inputSchema.properties.tags.anyOf[0].items.type !== "string" || !get.inputSchema.required.includes("input") || get.inputSchema.required.includes("tags")) throw new Error("optional/array schema drifted");
8717if (get.outputSchema.properties.body.properties.value.$ref !== "#/$defs/Widget" || get.outputSchema.$defs.Widget.properties.nested.$ref !== "#/$defs/Nested") throw new Error("Result success schema drifted");
8718const stream = tools.find((tool) => tool.name === "WidgetEvents");
8719if (stream.outputSchema.properties.body.properties.events.items.$ref !== "#/$defs/Widget") throw new Error("stream event schema drifted");
8720
8721let environment = { trace: [], hostCalls: 0, sessionId: "environment-session" };
8722const args = { id: "w-1", tags: ["a", "b"], input: { label: "ready", note: null, nested: { scores: [7, 8] } } };
8723response = await handle(request(4, "tools/call", { name: "GetWidget", arguments: args }, { "x-session": "mcp-session" }), environment);
8724rpc = await response.json();
8725if (rpc.result.isError !== true || rpc.result.structuredContent.body.error.code !== "ENDPOINT_CAPABILITY_DENIED" || environment.hostCalls !== 0) throw new Error(`capability denial did not reuse endpoint pipeline ${JSON.stringify(rpc)} ${JSON.stringify(environment)}`);
8726if (JSON.stringify(environment.trace) !== JSON.stringify(["middleware:/console/api/widgets/w-1:mcp-session", "authorize:widgets.read"])) throw new Error(`denial order diverged ${JSON.stringify(environment.trace)}`);
8727environment = { trace: [], hostCalls: 0 };
8728response = await handle(request(41, "tools/call", { name: "GetWidget", arguments: {} }, { "x-session": "mcp-session" }), environment);
8729rpc = await response.json();
8730if (rpc.result.structuredContent.body.error.code !== "ENDPOINT_CAPABILITY_DENIED" || environment.hostCalls !== 0 || environment.trace[1] !== "authorize:widgets.read") throw new Error(`malformed unauthorized MCP call bypassed endpoint capability order ${JSON.stringify(rpc)} ${JSON.stringify(environment)}`);
8731
8732environment = { trace: [], hostCalls: 0 };
8733response = await handle(request(5, "tools/call", { name: "GetWidget", arguments: args }, { "x-session": "mcp-session", "x-capability": "widgets.read" }), environment);
8734rpc = await response.json();
8735if (rpc.result.isError !== false || rpc.result.structuredContent.status !== 200 || rpc.result.structuredContent.body.value.name !== "ready" || rpc.result.structuredContent.body.value.nested.scores[1] !== 8 || environment.hostCalls !== 1) throw new Error(`typed tool roundtrip failed ${JSON.stringify(rpc)} ${JSON.stringify(environment)}`);
8736if (JSON.stringify(environment.trace) !== JSON.stringify(["middleware:/console/api/widgets/w-1:mcp-session", "authorize:widgets.read", "host:w-1:mcp-session"])) throw new Error(`MCP pipeline order/session diverged ${JSON.stringify(environment.trace)}`);
8737
8738environment = { trace: [], hostCalls: 0 };
8739const invalid = { ...args, input: { ...args.input, nested: { scores: ["bad"] } } };
8740response = await handle(request(6, "tools/call", { name: "GetWidget", arguments: invalid }, { "x-session": "mcp-session", "x-capability": "widgets.read" }), environment);
8741rpc = await response.json();
8742if (rpc.result.isError !== true || rpc.result.structuredContent.status !== 422 || rpc.result.structuredContent.body.error.code !== "ENDPOINT_INPUT_TYPE" || environment.hostCalls !== 0) throw new Error(`MCP input escaped endpoint validation ${JSON.stringify(rpc)}`);
8743
8744response = await handle(request(7, "tools/call", { name: "WidgetEvents", arguments: {} }), { trace: [] });
8745rpc = await response.json();
8746if (rpc.result.isError !== false || rpc.result.structuredContent.body.events.length !== 2 || rpc.result.structuredContent.body.events[1].nested.scores[1] !== 3) throw new Error(`stream endpoint tool mapping failed ${JSON.stringify(rpc)}`);
8747"##;
8748 let output = Command::new("node")
8749 .args(["--input-type=module", "-e", script])
8750 .current_dir(&root)
8751 .output()
8752 .expect("Node.js is required for generated MCP endpoint tests");
8753 let _ = std::fs::remove_dir_all(&root);
8754 assert!(
8755 output.status.success(),
8756 "{}\n{}",
8757 String::from_utf8_lossy(&output.stdout),
8758 String::from_utf8_lossy(&output.stderr)
8759 );
8760 }
8761}