@portel/photon 1.29.0 → 1.31.1

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 (139) hide show
  1. package/README.md +41 -11
  2. package/dist/asset-resolver.d.ts +44 -0
  3. package/dist/asset-resolver.d.ts.map +1 -0
  4. package/dist/asset-resolver.js +105 -0
  5. package/dist/asset-resolver.js.map +1 -0
  6. package/dist/auto-ui/beam/external-mcp-manager.d.ts +73 -0
  7. package/dist/auto-ui/beam/external-mcp-manager.d.ts.map +1 -0
  8. package/dist/auto-ui/beam/external-mcp-manager.js +65 -0
  9. package/dist/auto-ui/beam/external-mcp-manager.js.map +1 -0
  10. package/dist/auto-ui/beam/external-mcp.d.ts.map +1 -1
  11. package/dist/auto-ui/beam/external-mcp.js +25 -1
  12. package/dist/auto-ui/beam/external-mcp.js.map +1 -1
  13. package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
  14. package/dist/auto-ui/beam/photon-management.js +11 -8
  15. package/dist/auto-ui/beam/photon-management.js.map +1 -1
  16. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  17. package/dist/auto-ui/beam/routes/api-browse.js +7 -4
  18. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  19. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  20. package/dist/auto-ui/beam/routes/api-config.js +3 -2
  21. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  22. package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
  23. package/dist/auto-ui/beam/routes/api-marketplace.js +6 -2
  24. package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
  25. package/dist/auto-ui/beam/startup.js.map +1 -1
  26. package/dist/auto-ui/beam/types.d.ts +5 -2
  27. package/dist/auto-ui/beam/types.d.ts.map +1 -1
  28. package/dist/auto-ui/beam.d.ts.map +1 -1
  29. package/dist/auto-ui/beam.js +162 -45
  30. package/dist/auto-ui/beam.js.map +1 -1
  31. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  32. package/dist/auto-ui/bridge/index.js +11 -0
  33. package/dist/auto-ui/bridge/index.js.map +1 -1
  34. package/dist/auto-ui/bridge/types.d.ts +2 -0
  35. package/dist/auto-ui/bridge/types.d.ts.map +1 -1
  36. package/dist/auto-ui/openapi-generator.js +1 -4
  37. package/dist/auto-ui/openapi-generator.js.map +1 -1
  38. package/dist/auto-ui/photon-bridge.d.ts +4 -0
  39. package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
  40. package/dist/auto-ui/photon-bridge.js.map +1 -1
  41. package/dist/auto-ui/photon-host.js.map +1 -1
  42. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  43. package/dist/auto-ui/streamable-http-transport.js +24 -14
  44. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  45. package/dist/auto-ui/types.d.ts +15 -1
  46. package/dist/auto-ui/types.d.ts.map +1 -1
  47. package/dist/auto-ui/types.js.map +1 -1
  48. package/dist/beam.bundle.js +170 -22
  49. package/dist/beam.bundle.js.map +3 -3
  50. package/dist/capability-negotiator.d.ts +39 -1
  51. package/dist/capability-negotiator.d.ts.map +1 -1
  52. package/dist/capability-negotiator.js +5 -0
  53. package/dist/capability-negotiator.js.map +1 -1
  54. package/dist/cf-bindings-parser.d.ts +15 -0
  55. package/dist/cf-bindings-parser.d.ts.map +1 -0
  56. package/dist/cf-bindings-parser.js +98 -0
  57. package/dist/cf-bindings-parser.js.map +1 -0
  58. package/dist/cf-usage-scanner.d.ts +76 -0
  59. package/dist/cf-usage-scanner.d.ts.map +1 -0
  60. package/dist/cf-usage-scanner.js +179 -0
  61. package/dist/cf-usage-scanner.js.map +1 -0
  62. package/dist/cli/commands/build.js +1 -1
  63. package/dist/cli/commands/cf.d.ts +18 -0
  64. package/dist/cli/commands/cf.d.ts.map +1 -0
  65. package/dist/cli/commands/cf.js +207 -0
  66. package/dist/cli/commands/cf.js.map +1 -0
  67. package/dist/cli/commands/info.js +1 -1
  68. package/dist/cli/commands/info.js.map +1 -1
  69. package/dist/cli/commands/init.d.ts.map +1 -1
  70. package/dist/cli/commands/init.js +59 -46
  71. package/dist/cli/commands/init.js.map +1 -1
  72. package/dist/cli/commands/run.d.ts.map +1 -1
  73. package/dist/cli/commands/run.js +3 -0
  74. package/dist/cli/commands/run.js.map +1 -1
  75. package/dist/cli/index.d.ts.map +1 -1
  76. package/dist/cli/index.js +43 -6
  77. package/dist/cli/index.js.map +1 -1
  78. package/dist/daemon/client.d.ts.map +1 -1
  79. package/dist/daemon/client.js +40 -33
  80. package/dist/daemon/client.js.map +1 -1
  81. package/dist/daemon/manager.d.ts +6 -2
  82. package/dist/daemon/manager.d.ts.map +1 -1
  83. package/dist/daemon/manager.js +30 -9
  84. package/dist/daemon/manager.js.map +1 -1
  85. package/dist/daemon/server.js +34 -13
  86. package/dist/daemon/server.js.map +1 -1
  87. package/dist/daemon/worker-host.js.map +1 -1
  88. package/dist/deploy/cloudflare.d.ts +27 -0
  89. package/dist/deploy/cloudflare.d.ts.map +1 -1
  90. package/dist/deploy/cloudflare.js +129 -2
  91. package/dist/deploy/cloudflare.js.map +1 -1
  92. package/dist/embedded-runtime.js.map +1 -1
  93. package/dist/loader.d.ts +43 -66
  94. package/dist/loader.d.ts.map +1 -1
  95. package/dist/loader.js +185 -305
  96. package/dist/loader.js.map +1 -1
  97. package/dist/photon-cli-runner.d.ts.map +1 -1
  98. package/dist/photon-cli-runner.js +20 -11
  99. package/dist/photon-cli-runner.js.map +1 -1
  100. package/dist/resource-server.d.ts +3 -3
  101. package/dist/resource-server.d.ts.map +1 -1
  102. package/dist/resource-server.js.map +1 -1
  103. package/dist/runtime/cf-local.d.ts +157 -0
  104. package/dist/runtime/cf-local.d.ts.map +1 -0
  105. package/dist/runtime/cf-local.js +406 -0
  106. package/dist/runtime/cf-local.js.map +1 -0
  107. package/dist/server.d.ts +42 -2
  108. package/dist/server.d.ts.map +1 -1
  109. package/dist/server.js +166 -14
  110. package/dist/server.js.map +1 -1
  111. package/dist/settings-persistence.d.ts +50 -0
  112. package/dist/settings-persistence.d.ts.map +1 -0
  113. package/dist/settings-persistence.js +188 -0
  114. package/dist/settings-persistence.js.map +1 -0
  115. package/dist/shared/audit-sqlite.d.ts.map +1 -1
  116. package/dist/shared/audit-sqlite.js +0 -1
  117. package/dist/shared/audit-sqlite.js.map +1 -1
  118. package/dist/shared/error-handler.d.ts.map +1 -1
  119. package/dist/shared/error-handler.js +3 -1
  120. package/dist/shared/error-handler.js.map +1 -1
  121. package/dist/shared/io.d.ts.map +1 -1
  122. package/dist/shared/io.js +5 -2
  123. package/dist/shared/io.js.map +1 -1
  124. package/dist/shared/logger.js.map +1 -1
  125. package/dist/shared/sqlite-runtime.d.ts.map +1 -1
  126. package/dist/shared/sqlite-runtime.js +0 -1
  127. package/dist/shared/sqlite-runtime.js.map +1 -1
  128. package/dist/task-executor.js.map +1 -1
  129. package/dist/telemetry/sdk.d.ts.map +1 -1
  130. package/dist/telemetry/sdk.js +0 -1
  131. package/dist/telemetry/sdk.js.map +1 -1
  132. package/dist/test-runner.d.ts.map +1 -1
  133. package/dist/test-runner.js.map +1 -1
  134. package/dist/types/server-types.d.ts +16 -8
  135. package/dist/types/server-types.d.ts.map +1 -1
  136. package/package.json +11 -4
  137. package/templates/cloudflare/worker.ts.template +338 -11
  138. package/templates/cloudflare/wrangler.toml.template +1 -6
  139. package/templates/photon.template.ts +13 -0
@@ -298,6 +298,54 @@ function createCallProvider(env: Env, callerName: string) {
298
298
  };
299
299
  }
300
300
 
301
+ /**
302
+ * Build a `Cloudflare` adapter for the deployed Worker. Each scoped
303
+ * category resolves to `env[bindingNameFor(photon, category, qualifier)]`
304
+ * — the same auto-naming convention the local miniflare sandbox seeds,
305
+ * so photon source is unchanged across runtimes. Shared categories
306
+ * (ai/images/browser) read fixed canonical env keys.
307
+ *
308
+ * Mirrors photon-core's `createCloudflareFromEnv` line-for-line. We
309
+ * inline it rather than import to keep the template self-contained
310
+ * (the bundled Worker shouldn't depend on `@portel/photon-core` at
311
+ * runtime — too much surface for what's needed here).
312
+ */
313
+ function bindingNameFor(photon: string, category: string, qualifier?: string): string {
314
+ const safePhoton = photon.toLowerCase().replace(/-/g, '_');
315
+ if (qualifier && qualifier.length > 0) {
316
+ const safeQualifier = qualifier.toLowerCase().replace(/-/g, '_');
317
+ return `${safePhoton}_${safeQualifier}_${category}`;
318
+ }
319
+ return `${safePhoton}_${category}`;
320
+ }
321
+
322
+ function createCloudflareFromEnv(env: Env, photonName: string): any {
323
+ const scoped = (category: string) => (qualifier?: string) => {
324
+ const name = bindingNameFor(photonName, category, qualifier);
325
+ const binding = (env as any)[name];
326
+ if (!binding) {
327
+ throw new Error(
328
+ `cf.${category}(${qualifier ? JSON.stringify(qualifier) : ''}) requires ` +
329
+ `binding "${name}" on the Worker env, but it is not defined. Add it to ` +
330
+ `wrangler.toml (or run \`photon host deploy cloudflare\` to regenerate ` +
331
+ `bindings from the photon's source).`
332
+ );
333
+ }
334
+ return binding;
335
+ };
336
+ return {
337
+ kv: scoped('kv'),
338
+ r2: scoped('r2'),
339
+ d1: scoped('d1'),
340
+ queue: scoped('queue'),
341
+ vectorize: scoped('vectorize'),
342
+ ai: (env as any).AI,
343
+ images: (env as any).IMAGES,
344
+ browser: (env as any).BROWSER,
345
+ fetch: (input: any, init?: any) => fetch(input, init),
346
+ };
347
+ }
348
+
301
349
  // ════════════════════════════════════════════════════════════════════════════
302
350
  // Capability shim — wires this.* on the photon instance
303
351
  // ════════════════════════════════════════════════════════════════════════════
@@ -316,15 +364,76 @@ function withCfCapabilities(
316
364
  });
317
365
 
318
366
  Object.defineProperty(instance, 'emit', {
319
- value: (event: { channel?: string; [k: string]: unknown }) => {
320
- const channel = (event && typeof event === 'object' && event.channel) || 'default';
321
- const payload = JSON.stringify(event);
322
- for (const ws of ctx.getWebSockets(channel)) {
323
- try {
324
- ws.send(payload);
325
- } catch {
326
- // Socket closing; webSocketClose hook will reap.
327
- }
367
+ value: (event: { emit?: string; [k: string]: unknown }) => {
368
+ // SSE forwarding for in-flight tool calls. When an SSE
369
+ // request context is active (i.e. the caller used
370
+ // `Accept: text/event-stream`), `this.emit({ emit: 'progress' })`,
371
+ // `this.emit({ emit: 'status' })`, etc. become real
372
+ // `notifications/progress` / `notifications/message` JSON-RPC
373
+ // notifications written to the open stream — the client sees
374
+ // them arrive as the tool runs, not in a single batch at the
375
+ // end. Mirrors `src/server.ts:1340-1397` for runtime parity
376
+ // between local and deployed transports.
377
+ const reqCtx = requestContext.getStore();
378
+ if (!reqCtx) return;
379
+ const kind = (event && typeof event === 'object' ? event.emit : undefined) as string | undefined;
380
+ if (!kind) return;
381
+ const progressToken = reqCtx.progressToken;
382
+ if (kind === 'progress') {
383
+ const rawValue = typeof event.value === 'number' ? (event.value as number) : 0;
384
+ const progress = rawValue <= 1 ? rawValue * 100 : rawValue;
385
+ void reqCtx.send({
386
+ jsonrpc: '2.0',
387
+ method: 'notifications/progress',
388
+ params: {
389
+ ...(progressToken !== undefined ? { progressToken } : {}),
390
+ progress,
391
+ total: 100,
392
+ ...(typeof event.message === 'string' ? { message: event.message } : {}),
393
+ },
394
+ });
395
+ } else if (kind === 'status') {
396
+ void reqCtx.send({
397
+ jsonrpc: '2.0',
398
+ method: 'notifications/progress',
399
+ params: {
400
+ ...(progressToken !== undefined ? { progressToken } : {}),
401
+ progress: 0,
402
+ total: 100,
403
+ message: typeof event.message === 'string' ? event.message : '',
404
+ },
405
+ });
406
+ } else if (kind === 'log') {
407
+ void reqCtx.send({
408
+ jsonrpc: '2.0',
409
+ method: 'notifications/message',
410
+ params: {
411
+ level: typeof event.level === 'string' ? event.level : 'info',
412
+ data: typeof event.message === 'string' ? event.message : '',
413
+ },
414
+ });
415
+ } else if (kind === 'render') {
416
+ void reqCtx.send({
417
+ jsonrpc: '2.0',
418
+ method: 'notifications/message',
419
+ params: {
420
+ level: 'info',
421
+ data: JSON.stringify({
422
+ _render: true,
423
+ format: event.format,
424
+ value: event.value,
425
+ }),
426
+ },
427
+ });
428
+ } else if (kind === 'render:clear') {
429
+ void reqCtx.send({
430
+ jsonrpc: '2.0',
431
+ method: 'notifications/message',
432
+ params: {
433
+ level: 'info',
434
+ data: JSON.stringify({ _render: true, clear: true }),
435
+ },
436
+ });
328
437
  }
329
438
  },
330
439
  writable: false,
@@ -332,6 +441,80 @@ function withCfCapabilities(
332
441
  configurable: false,
333
442
  });
334
443
 
444
+ // Emit-based convenience helpers — `this.status(...)`, `this.log(...)`,
445
+ // `this.progress(...)`, `this.toast(...)`, `this.thinking(...)`,
446
+ // `this.render(...)`. The classic loader injects these on every
447
+ // plain-class instance (`src/loader.ts:injectEmitHelpers`); the
448
+ // deployed Worker must do the same so a photon written against the
449
+ // local runtime behaves identically when served from CF. Without
450
+ // these, calls like `this.status?.('one')` short-circuit silently
451
+ // on the Worker and the whole chain of progress notifications is
452
+ // lost — which is exactly the symptom that prompted this wiring.
453
+ // Each helper guards on `in instance` so user-declared methods on
454
+ // the photon class always win.
455
+ const emitFn = (data: { [k: string]: unknown }) =>
456
+ (instance as { emit: (d: unknown) => void }).emit(data);
457
+ if (!('render' in instance)) {
458
+ (instance as { render?: (format?: string, value?: unknown) => void }).render = (
459
+ format?: string,
460
+ value?: unknown,
461
+ ) => {
462
+ if (format === undefined) return emitFn({ emit: 'render:clear' });
463
+ if (format === 'status') {
464
+ return emitFn(
465
+ typeof value === 'string'
466
+ ? { emit: 'status', message: value }
467
+ : { emit: 'status', ...(value as object) }
468
+ );
469
+ }
470
+ if (format === 'progress') {
471
+ return emitFn(
472
+ typeof value === 'number'
473
+ ? { emit: 'progress', value }
474
+ : { emit: 'progress', ...(value as object) }
475
+ );
476
+ }
477
+ if (format === 'toast') {
478
+ return emitFn(
479
+ typeof value === 'string'
480
+ ? { emit: 'toast', message: value }
481
+ : { emit: 'toast', ...(value as object) }
482
+ );
483
+ }
484
+ emitFn({ emit: 'render', format, value });
485
+ };
486
+ }
487
+ if (!('toast' in instance)) {
488
+ (instance as { toast?: (m: string, o?: { type?: string; duration?: number }) => void }).toast = (
489
+ message: string,
490
+ opts: { type?: string; duration?: number } = {},
491
+ ) => emitFn({ emit: 'toast', message, ...opts });
492
+ }
493
+ if (!('log' in instance)) {
494
+ (instance as { log?: (m: string, o?: { level?: string; data?: unknown }) => void }).log = (
495
+ message: string,
496
+ opts: { level?: string; data?: unknown } = {},
497
+ ) => emitFn({ emit: 'log', message, level: opts.level ?? 'info', data: opts.data });
498
+ }
499
+ if (!('status' in instance)) {
500
+ // Signature mirrors `src/loader.ts:288` exactly. Photons that pass a
501
+ // second arg (e.g. `this.status('one', { n: 1 })`) keep working —
502
+ // the extra arg is dropped on both transports for parity. If we
503
+ // ever decide to surface it, change both call sites at once.
504
+ (instance as { status?: (m: string) => void }).status = (message: string) =>
505
+ emitFn({ emit: 'status', message });
506
+ }
507
+ if (!('progress' in instance)) {
508
+ (instance as { progress?: (v: number, m?: string) => void }).progress = (
509
+ value: number,
510
+ message?: string,
511
+ ) => emitFn({ emit: 'progress', value, message });
512
+ }
513
+ if (!('thinking' in instance)) {
514
+ (instance as { thinking?: (a?: boolean) => void }).thinking = (active = true) =>
515
+ emitFn({ emit: 'thinking', active });
516
+ }
517
+
335
518
  Object.defineProperty(instance, 'schedule', {
336
519
  value: createScheduleProvider(ctx, photonName),
337
520
  writable: false,
@@ -366,6 +549,33 @@ function withCfCapabilities(
366
549
  configurable: false,
367
550
  });
368
551
 
552
+ // `this.mcpAuthed` — true when the active /mcp request passed the
553
+ // PHOTON_MCP_BEARER check, false when no secret is configured or the
554
+ // request hit a path outside MCP dispatch (e.g. /api/* or /__call).
555
+ // User code can guard sensitive methods with:
556
+ // if (!this.mcpAuthed) throw new Error('unauthorized');
557
+ // The flag is per-tool-call (AsyncLocalStorage scoped); it doesn't
558
+ // persist across awaits that escape the dispatch context.
559
+ Object.defineProperty(instance, 'mcpAuthed', {
560
+ get() {
561
+ return mcpAuthContext.getStore()?.authed === true;
562
+ },
563
+ enumerable: false,
564
+ configurable: false,
565
+ });
566
+
567
+ // `this.cf` — the wrapped Cloudflare surface. Auto-naming
568
+ // (`bindingNameFor`) resolves `cf.kv()` to `<photon>_kv`,
569
+ // `cf.kv('cache')` to `<photon>_cache_kv`, etc. The deploy
570
+ // pipeline emits matching wrangler.toml entries from the same
571
+ // convention, so bindings always line up across runtimes.
572
+ Object.defineProperty(instance, 'cf', {
573
+ value: createCloudflareFromEnv(env, photonName),
574
+ writable: false,
575
+ enumerable: false,
576
+ configurable: false,
577
+ });
578
+
369
579
  // Human/LLM-in-the-loop primitives. Each one wraps an MCP server-initiated
370
580
  // request (sampling/createMessage, elicitation/create), pushed over the
371
581
  // active tool call's SSE response stream and awaited on a Promise keyed by
@@ -407,6 +617,14 @@ interface RequestContext {
407
617
  send: (msg: unknown) => Promise<void>;
408
618
  /** Shared pending map; the DO's POST /mcp handler resolves entries here. */
409
619
  pendingRequests: Map<string, PendingRequest>;
620
+ /**
621
+ * Client-supplied progress token from the originating request's
622
+ * `_meta.progressToken`. Echoed back in every `notifications/progress`
623
+ * event we send during the call so the client can correlate progress
624
+ * with its outstanding request. Falls back to a synthesized token if
625
+ * the client didn't supply one.
626
+ */
627
+ progressToken?: string | number;
410
628
  }
411
629
 
412
630
  /**
@@ -418,6 +636,73 @@ interface RequestContext {
418
636
  */
419
637
  const requestContext = new AsyncLocalStorage<RequestContext>();
420
638
 
639
+ /**
640
+ * Per-tool-call MCP auth context. Set when the bearer check on `/mcp`
641
+ * has passed (or skipped because no `PHOTON_MCP_BEARER` secret is
642
+ * configured) so user code reading `this.mcpAuthed` sees the right
643
+ * value for the dispatched method. The flag is scoped to the single
644
+ * `tools/call` invocation, not the DO lifetime.
645
+ */
646
+ const mcpAuthContext = new AsyncLocalStorage<{ authed: boolean }>();
647
+
648
+ /**
649
+ * Constant-time string comparison to avoid leaking the bearer through
650
+ * timing differences. Falls back to a manual byte loop because Workers
651
+ * doesn't always expose `crypto.timingSafeEqual`.
652
+ */
653
+ function timingSafeEqualString(a: string, b: string): boolean {
654
+ if (a.length !== b.length) return false;
655
+ let mismatch = 0;
656
+ for (let i = 0; i < a.length; i++) {
657
+ mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
658
+ }
659
+ return mismatch === 0;
660
+ }
661
+
662
+ /**
663
+ * Validate the Authorization header against `env.PHOTON_MCP_BEARER`.
664
+ * Returns:
665
+ * - `{ enforced: false }` when no secret is configured (back-compat).
666
+ * - `{ enforced: true, ok: true }` when the bearer matches.
667
+ * - `{ enforced: true, ok: false, reason }` when missing or wrong.
668
+ *
669
+ * Methods that don't dispatch user code (`tools/list`, `initialize`,
670
+ * `notifications/*`, `ping`) bypass this check at the call site.
671
+ */
672
+ function checkMcpBearer(
673
+ request: Request,
674
+ env: Env
675
+ ): { enforced: false } | { enforced: true; ok: boolean; reason?: string } {
676
+ const expected = (env as Record<string, unknown>).PHOTON_MCP_BEARER;
677
+ if (typeof expected !== 'string' || expected.length === 0) {
678
+ return { enforced: false };
679
+ }
680
+ const header = request.headers.get('Authorization') ?? '';
681
+ const match = header.match(/^Bearer\s+(.+)$/i);
682
+ if (!match) {
683
+ return { enforced: true, ok: false, reason: 'Authorization: Bearer <token> header missing' };
684
+ }
685
+ const presented = match[1].trim();
686
+ if (!timingSafeEqualString(presented, expected)) {
687
+ return { enforced: true, ok: false, reason: 'bearer token does not match PHOTON_MCP_BEARER' };
688
+ }
689
+ return { enforced: true, ok: true };
690
+ }
691
+
692
+ /**
693
+ * Methods that may pass through `/mcp` without a bearer because they
694
+ * don't dispatch into user code. `tools/list` advertises the catalog,
695
+ * which is generally safe to expose; if a deployment wants to gate it
696
+ * the photon owner can set CF Access on the route.
697
+ */
698
+ const MCP_METHODS_BYPASSING_BEARER = new Set([
699
+ 'initialize',
700
+ 'notifications/initialized',
701
+ 'notifications/cancelled',
702
+ 'ping',
703
+ 'tools/list',
704
+ ]);
705
+
421
706
  function requireRequestContext(which: string): RequestContext {
422
707
  const ctx = requestContext.getStore();
423
708
  if (!ctx) {
@@ -983,6 +1268,40 @@ abstract class BasePhotonDO extends DurableObject<Env> {
983
1268
  return new Response(null, { status: 204, headers: CORS_HEADERS });
984
1269
  }
985
1270
 
1271
+ // Bearer auth gate. When `PHOTON_MCP_BEARER` is set on the deployed
1272
+ // Worker (e.g. via `wrangler secret put PHOTON_MCP_BEARER`), every
1273
+ // method that dispatches into user code requires a matching
1274
+ // `Authorization: Bearer <token>` header. Discovery + handshake
1275
+ // methods (tools/list, initialize, ping, notifications/*) pass
1276
+ // through unauthed so MCP clients can complete capability
1277
+ // negotiation before authenticating. When the secret is unset the
1278
+ // gate is a no-op so existing deployments keep working.
1279
+ const method = typeof body?.method === 'string' ? body.method : '';
1280
+ const authResult = checkMcpBearer(request, (this as any).env);
1281
+ const requiresAuth = method !== '' && !MCP_METHODS_BYPASSING_BEARER.has(method);
1282
+ if (authResult.enforced && requiresAuth && !authResult.ok) {
1283
+ return new Response(
1284
+ JSON.stringify({
1285
+ jsonrpc: '2.0',
1286
+ id: body?.id ?? null,
1287
+ error: {
1288
+ code: -32001,
1289
+ message: 'Unauthorized',
1290
+ data: { reason: (authResult as { reason: string }).reason },
1291
+ },
1292
+ }),
1293
+ {
1294
+ status: 401,
1295
+ headers: {
1296
+ 'Content-Type': 'application/json',
1297
+ 'WWW-Authenticate': 'Bearer realm="photon"',
1298
+ ...CORS_HEADERS,
1299
+ },
1300
+ }
1301
+ );
1302
+ }
1303
+ const authed = authResult.enforced ? authResult.ok === true : false;
1304
+
986
1305
  // Tool calls from clients that signal SSE support get a streamed
987
1306
  // response so this.sample / this.confirm / this.elicit can push
988
1307
  // server-initiated requests inline. All other JSON-RPC methods (and
@@ -990,10 +1309,12 @@ abstract class BasePhotonDO extends DurableObject<Env> {
990
1309
  const accept = request.headers.get('Accept') ?? '';
991
1310
  const wantsSse = accept.includes('text/event-stream');
992
1311
  if (body?.method === 'tools/call' && wantsSse) {
993
- return this._streamToolCall(body);
1312
+ return mcpAuthContext.run({ authed }, () => this._streamToolCall(body));
994
1313
  }
995
1314
 
996
- const result = await handleMCPRequest(body, this.photon, this.photonName, this.toolDefinitions);
1315
+ const result = await mcpAuthContext.run({ authed }, () =>
1316
+ handleMCPRequest(body, this.photon, this.photonName, this.toolDefinitions)
1317
+ );
997
1318
  return Response.json(result, { headers: CORS_HEADERS });
998
1319
  }
999
1320
 
@@ -1011,9 +1332,15 @@ abstract class BasePhotonDO extends DurableObject<Env> {
1011
1332
  await writer.write(encoder.encode(`data: ${JSON.stringify(msg)}\n\n`));
1012
1333
  };
1013
1334
 
1335
+ // Per MCP spec, echo the client-supplied progressToken so any
1336
+ // streamed progress notifications correlate with this request.
1337
+ const clientProgressToken = (rpcRequest?.params as { _meta?: { progressToken?: string | number } })
1338
+ ?._meta?.progressToken;
1339
+ const toolName = (rpcRequest?.params as { name?: string })?.name ?? 'tool';
1014
1340
  const ctx: RequestContext = {
1015
1341
  send,
1016
1342
  pendingRequests: this.pendingRequests,
1343
+ progressToken: clientProgressToken ?? `progress_${toolName}`,
1017
1344
  };
1018
1345
 
1019
1346
  // Fire-and-forget: the response stream stays open as long as the writer
@@ -7,12 +7,7 @@ main = "src/worker.ts"
7
7
  compatibility_date = "2024-09-23"
8
8
  compatibility_flags = ["nodejs_compat"]
9
9
  __OBSERVABILITY__
10
- # Workers AI binding — gives every photon access to CF's edge inference
11
- # (embeddings, classification, small LLMs) via `(this as any).env.AI.run(...)`.
12
- # Free tier covers thousands of calls/day; charges only kick in at scale.
13
- # Photons that don't use it pay nothing for having the binding declared.
14
- [ai]
15
- binding = "AI"
10
+ __CF_BINDINGS__
16
11
 
17
12
  # Durable Objects backing every photon class bundled into this Worker. The
18
13
  # host photon is always bound to `PHOTON`; each `@photons` sibling gets
@@ -7,6 +7,19 @@
7
7
  */
8
8
 
9
9
  export default class TemplateName {
10
+ // User-configurable knobs. Photon auto-generates a `settings` MCP tool
11
+ // from this object and persists changes to
12
+ // ~/.photon/state/<photon>/<instance>-settings.json.
13
+ //
14
+ // Reach for this whenever a value should be runtime-configurable. Use a
15
+ // constructor parameter only for primitive secrets (API keys, tokens)
16
+ // that should live in .env. Uncomment and edit:
17
+ //
18
+ // protected settings = {
19
+ // /** Friendly description shown in the settings tool */
20
+ // greeting: 'Hello',
21
+ // };
22
+
10
23
  /**
11
24
  * Optional initialization hook
12
25
  * Called once when the MCP is loaded