@portel/photon 1.26.0 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/auto-ui/beam/routes/api-daemon.d.ts +1 -0
  2. package/dist/auto-ui/beam/routes/api-daemon.d.ts.map +1 -1
  3. package/dist/auto-ui/beam/routes/api-daemon.js +35 -1
  4. package/dist/auto-ui/beam/routes/api-daemon.js.map +1 -1
  5. package/dist/beam-form.bundle.js +41 -1
  6. package/dist/beam-form.bundle.js.map +2 -2
  7. package/dist/beam.bundle.js +1661 -252
  8. package/dist/beam.bundle.js.map +4 -4
  9. package/dist/cli/commands/daemon.d.ts.map +1 -1
  10. package/dist/cli/commands/daemon.js +157 -0
  11. package/dist/cli/commands/daemon.js.map +1 -1
  12. package/dist/daemon/client.d.ts +1 -0
  13. package/dist/daemon/client.d.ts.map +1 -1
  14. package/dist/daemon/client.js +110 -23
  15. package/dist/daemon/client.js.map +1 -1
  16. package/dist/daemon/in-process-bridge.d.ts +29 -0
  17. package/dist/daemon/in-process-bridge.d.ts.map +1 -0
  18. package/dist/daemon/in-process-bridge.js +26 -0
  19. package/dist/daemon/in-process-bridge.js.map +1 -0
  20. package/dist/daemon/manager.d.ts +103 -1
  21. package/dist/daemon/manager.d.ts.map +1 -1
  22. package/dist/daemon/manager.js +313 -92
  23. package/dist/daemon/manager.js.map +1 -1
  24. package/dist/daemon/protocol.d.ts +1 -1
  25. package/dist/daemon/protocol.d.ts.map +1 -1
  26. package/dist/daemon/protocol.js +1 -0
  27. package/dist/daemon/protocol.js.map +1 -1
  28. package/dist/daemon/server.js +832 -37
  29. package/dist/daemon/server.js.map +1 -1
  30. package/dist/loader.d.ts.map +1 -1
  31. package/dist/loader.js +11 -0
  32. package/dist/loader.js.map +1 -1
  33. package/package.json +1 -1
  34. package/templates/cloudflare/worker.ts.template +94 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portel/photon",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "description": "You focus on the business logic. We'll enable the rest. Build MCP servers and CLI tools in a single TypeScript file.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -535,6 +535,65 @@ function spreadArgs(toolDef: any, args: Record<string, unknown>): unknown[] {
535
535
  return [args];
536
536
  }
537
537
 
538
+ /**
539
+ * Match a request against the `@get`/`@post` route table. Two passes:
540
+ * exact paths first (cheaper, also gives them precedence over patterns
541
+ * that would also match), then `:param` patterns. Returns the matched
542
+ * route plus any extracted path params, or null when nothing matches.
543
+ *
544
+ * Patterns: `/b/:token` matches `/b/abc123` -> { token: 'abc123' }.
545
+ * Segment counts must be equal -- `/b/:token` does not match `/b/x/y`.
546
+ *
547
+ * Tested in tests/cf-template-route-matcher.test.ts. Keep the two
548
+ * definitions in sync if either is edited.
549
+ */
550
+ function matchHttpRoute(
551
+ routes: { method: string; path: string; handler: string }[],
552
+ method: string,
553
+ pathname: string
554
+ ): {
555
+ route: { method: string; path: string; handler: string };
556
+ params: Record<string, string>;
557
+ } | null {
558
+ for (const route of routes) {
559
+ if (route.method !== method) continue;
560
+ if (!route.path.includes(':') && route.path === pathname) {
561
+ return { route, params: {} };
562
+ }
563
+ }
564
+ for (const route of routes) {
565
+ if (route.method !== method) continue;
566
+ if (!route.path.includes(':')) continue;
567
+ const params = matchPathPattern(route.path, pathname);
568
+ if (params) return { route, params };
569
+ }
570
+ return null;
571
+ }
572
+
573
+ function matchPathPattern(
574
+ pattern: string,
575
+ pathname: string
576
+ ): Record<string, string> | null {
577
+ const patternParts = pattern.split('/').filter(Boolean);
578
+ const pathParts = pathname.split('/').filter(Boolean);
579
+ if (patternParts.length !== pathParts.length) return null;
580
+ const params: Record<string, string> = {};
581
+ for (let i = 0; i < patternParts.length; i++) {
582
+ const pp = patternParts[i];
583
+ const rp = pathParts[i];
584
+ if (pp.startsWith(':')) {
585
+ try {
586
+ params[pp.slice(1)] = decodeURIComponent(rp);
587
+ } catch {
588
+ return null;
589
+ }
590
+ } else if (pp !== rp) {
591
+ return null;
592
+ }
593
+ }
594
+ return params;
595
+ }
596
+
538
597
  async function handleMCPRequest(
539
598
  request: any,
540
599
  photon: any,
@@ -718,23 +777,35 @@ abstract class BasePhotonDO extends DurableObject<Env> {
718
777
  return new Response(null, { status: 204, headers: CORS_HEADERS });
719
778
  }
720
779
 
721
- // Info endpoint
780
+ // Info endpoint with content negotiation. A photon may declare
781
+ // `@get /` to serve a public HTML homepage; in that case JSON
782
+ // requests still get the discovery payload and everything else
783
+ // (browsers, default curl, anything not asking for JSON) falls
784
+ // through to the photon's handler in the route table below.
722
785
  if (url.pathname === '/' && request.method === 'GET') {
723
- return Response.json(
724
- {
725
- name: this.photonName,
726
- instance: this.instanceName,
727
- transport: 'streamable-http',
728
- runtime: 'cloudflare-workers',
729
- endpoints: {
730
- mcp: '/mcp',
731
- events: '/events?channel=<name>',
732
- ...(DEV_MODE ? { playground: '/playground' } : {}),
733
- },
734
- tools: this.toolDefinitions.length,
735
- },
736
- { headers: CORS_HEADERS }
786
+ const accept = (request.headers.get('accept') || '').toLowerCase();
787
+ const wantsJson = accept.includes('application/json');
788
+ const userHomeRoute = this.httpRoutes.find(
789
+ (r) => r.method === 'GET' && r.path === '/'
737
790
  );
791
+ if (wantsJson || !userHomeRoute) {
792
+ return Response.json(
793
+ {
794
+ name: this.photonName,
795
+ instance: this.instanceName,
796
+ transport: 'streamable-http',
797
+ runtime: 'cloudflare-workers',
798
+ endpoints: {
799
+ mcp: '/mcp',
800
+ events: '/events?channel=<name>',
801
+ ...(DEV_MODE ? { playground: '/playground' } : {}),
802
+ },
803
+ tools: this.toolDefinitions.length,
804
+ },
805
+ { headers: CORS_HEADERS }
806
+ );
807
+ }
808
+ // Fall through to the @get/@post route table (handles `@get /`).
738
809
  }
739
810
 
740
811
  // Dev-only endpoints
@@ -767,15 +838,16 @@ abstract class BasePhotonDO extends DurableObject<Env> {
767
838
  }
768
839
  }
769
840
 
770
- // @get / @post HTTP routes — dispatch to photon method, bypass MCP
771
- const httpRoute = this.httpRoutes.find(
772
- (r) => r.method === request.method && r.path === url.pathname
773
- );
774
- if (httpRoute) {
775
- const fn = (this.photon as any)[httpRoute.handler];
841
+ // @get / @post HTTP routes — dispatch to photon method, bypass MCP.
842
+ // Handler signature: (request: Request, params?: Record<string, string>)
843
+ // the second arg carries `:param` values extracted from the path so a
844
+ // route declared `@get /b/:token` receives `{ token }`.
845
+ const matchedRoute = matchHttpRoute(this.httpRoutes, request.method, url.pathname);
846
+ if (matchedRoute) {
847
+ const fn = (this.photon as any)[matchedRoute.route.handler];
776
848
  if (typeof fn === 'function') {
777
849
  try {
778
- const response = await fn.call(this.photon, request);
850
+ const response = await fn.call(this.photon, request, matchedRoute.params);
779
851
  if (response instanceof Response) return response;
780
852
  return Response.json(response, { headers: CORS_HEADERS });
781
853
  } catch (error: any) {