@portel/photon 1.28.2 → 1.29.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 (81) hide show
  1. package/README.md +1 -0
  2. package/dist/auto-ui/beam.d.ts.map +1 -1
  3. package/dist/auto-ui/beam.js +77 -43
  4. package/dist/auto-ui/beam.js.map +1 -1
  5. package/dist/auto-ui/streamable-http-transport.d.ts +7 -0
  6. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  7. package/dist/auto-ui/streamable-http-transport.js +228 -29
  8. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  9. package/dist/auto-ui/types.d.ts +9 -1
  10. package/dist/auto-ui/types.d.ts.map +1 -1
  11. package/dist/auto-ui/types.js.map +1 -1
  12. package/dist/beam.bundle.js +32 -2
  13. package/dist/beam.bundle.js.map +2 -2
  14. package/dist/cli/commands/build.d.ts.map +1 -1
  15. package/dist/cli/commands/build.js +123 -15
  16. package/dist/cli/commands/build.js.map +1 -1
  17. package/dist/daemon/manager.d.ts.map +1 -1
  18. package/dist/daemon/manager.js +45 -11
  19. package/dist/daemon/manager.js.map +1 -1
  20. package/dist/daemon/server.js +41 -0
  21. package/dist/daemon/server.js.map +1 -1
  22. package/dist/deploy/cloudflare.d.ts.map +1 -1
  23. package/dist/deploy/cloudflare.js +82 -2
  24. package/dist/deploy/cloudflare.js.map +1 -1
  25. package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
  26. package/dist/editor-support/docblock-tag-catalog.js +32 -2
  27. package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
  28. package/dist/format/registry.d.ts +83 -0
  29. package/dist/format/registry.d.ts.map +1 -0
  30. package/dist/format/registry.js +139 -0
  31. package/dist/format/registry.js.map +1 -0
  32. package/dist/format/seed.d.ts +18 -0
  33. package/dist/format/seed.d.ts.map +1 -0
  34. package/dist/format/seed.js +246 -0
  35. package/dist/format/seed.js.map +1 -0
  36. package/dist/loader.d.ts +18 -0
  37. package/dist/loader.d.ts.map +1 -1
  38. package/dist/loader.js +130 -22
  39. package/dist/loader.js.map +1 -1
  40. package/dist/photons/maker.photon.d.ts +2 -2
  41. package/dist/photons/maker.photon.d.ts.map +1 -1
  42. package/dist/photons/maker.photon.js +5 -6
  43. package/dist/photons/maker.photon.js.map +1 -1
  44. package/dist/photons/maker.photon.ts +5 -6
  45. package/dist/resource-server.d.ts +52 -12
  46. package/dist/resource-server.d.ts.map +1 -1
  47. package/dist/resource-server.js +205 -50
  48. package/dist/resource-server.js.map +1 -1
  49. package/dist/server.d.ts +75 -0
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +515 -53
  52. package/dist/server.js.map +1 -1
  53. package/dist/shared/asset-encoding.d.ts +30 -0
  54. package/dist/shared/asset-encoding.d.ts.map +1 -0
  55. package/dist/shared/asset-encoding.js +0 -0
  56. package/dist/shared/asset-encoding.js.map +1 -0
  57. package/dist/shared/cross-origin-headers.d.ts +47 -0
  58. package/dist/shared/cross-origin-headers.d.ts.map +1 -0
  59. package/dist/shared/cross-origin-headers.js +61 -0
  60. package/dist/shared/cross-origin-headers.js.map +1 -0
  61. package/dist/shared/expose-route-extractor.d.ts +36 -0
  62. package/dist/shared/expose-route-extractor.d.ts.map +1 -0
  63. package/dist/shared/expose-route-extractor.js +64 -0
  64. package/dist/shared/expose-route-extractor.js.map +1 -0
  65. package/dist/shared/extract-claims.d.ts +33 -0
  66. package/dist/shared/extract-claims.d.ts.map +1 -0
  67. package/dist/shared/extract-claims.js +60 -0
  68. package/dist/shared/extract-claims.js.map +1 -0
  69. package/dist/shared/http-route-extractor.d.ts +6 -0
  70. package/dist/shared/http-route-extractor.d.ts.map +1 -1
  71. package/dist/shared/http-route-extractor.js +29 -5
  72. package/dist/shared/http-route-extractor.js.map +1 -1
  73. package/dist/shared/instance-binding.d.ts +53 -0
  74. package/dist/shared/instance-binding.d.ts.map +1 -0
  75. package/dist/shared/instance-binding.js +85 -0
  76. package/dist/shared/instance-binding.js.map +1 -0
  77. package/dist/types/server-types.d.ts +1 -0
  78. package/dist/types/server-types.d.ts.map +1 -1
  79. package/package.json +6 -3
  80. package/templates/cloudflare/worker.ts.template +90 -3
  81. package/templates/cloudflare/wrangler.toml.template +1 -1
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Source-level extractor for `@expose` declarations.
3
+ *
4
+ * v1.29 Track C. Methods tagged `@expose` are auto-bound to a kebab-cased
5
+ * `/api/<method>` POST endpoint so a same-origin SPA can call them via
6
+ * fetch without writing per-method `@post` directives. `@expose public`
7
+ * widens this from "SameSite-cookie required" to "any caller" — useful
8
+ * for anonymous public surfaces (iCal feeds, billing portals, etc.).
9
+ *
10
+ * Like `http-route-extractor.ts`, this runs against raw source so the
11
+ * extractor doesn't depend on whatever shape the published photon-core
12
+ * SchemaExtractor returns. Both the runtime dispatcher and the Cloudflare
13
+ * deploy code-gen consume the same output.
14
+ *
15
+ * /‍** @expose *‍/ async getCurrentUser() { ... } → private
16
+ * /‍** @expose public *‍/ async billing() { ... } → public
17
+ * (no @expose tag) async listUsers() { ... } → MCP-only
18
+ */
19
+ export type ExposeVisibility = 'private' | 'public';
20
+ export interface ExposeDef {
21
+ /** Method on the photon class. */
22
+ handler: string;
23
+ /** SameSite cookie required (`private`) or anonymous OK (`public`). */
24
+ visibility: ExposeVisibility;
25
+ }
26
+ export declare function extractExposesFromSource(source: string): ExposeDef[];
27
+ /**
28
+ * Convert a method name to its kebab-cased route segment. Matches the
29
+ * convention the bridge fetch fallback uses (`fetch('/api/<kebab>', ...)`).
30
+ *
31
+ * getCurrentUser → get-current-user
32
+ * listUsers → list-users
33
+ * billing → billing
34
+ */
35
+ export declare function methodToKebab(name: string): string;
36
+ //# sourceMappingURL=expose-route-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expose-route-extractor.d.ts","sourceRoot":"","sources":["../../src/shared/expose-route-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,SAAS;IACxB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,UAAU,EAAE,gBAAgB,CAAC;CAC9B;AAWD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,CAqBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKlD"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Source-level extractor for `@expose` declarations.
3
+ *
4
+ * v1.29 Track C. Methods tagged `@expose` are auto-bound to a kebab-cased
5
+ * `/api/<method>` POST endpoint so a same-origin SPA can call them via
6
+ * fetch without writing per-method `@post` directives. `@expose public`
7
+ * widens this from "SameSite-cookie required" to "any caller" — useful
8
+ * for anonymous public surfaces (iCal feeds, billing portals, etc.).
9
+ *
10
+ * Like `http-route-extractor.ts`, this runs against raw source so the
11
+ * extractor doesn't depend on whatever shape the published photon-core
12
+ * SchemaExtractor returns. Both the runtime dispatcher and the Cloudflare
13
+ * deploy code-gen consume the same output.
14
+ *
15
+ * /‍** @expose *‍/ async getCurrentUser() { ... } → private
16
+ * /‍** @expose public *‍/ async billing() { ... } → public
17
+ * (no @expose tag) async listUsers() { ... } → MCP-only
18
+ */
19
+ const JSDOC_BLOCK_RE = /\/\*\*([\s\S]*?)\*\//g;
20
+ const METHOD_RE = /^\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(\w+)\s*\(/;
21
+ // Anchor `@expose` to a JSDoc tag position — either the very start of the
22
+ // block body (single-line `/** @expose */`) or a JSDoc line that begins
23
+ // with `*`. Prevents prose mentions like `No @expose — MCP-only` from
24
+ // tripping the matcher. The visibility capture excludes `*` so it stops
25
+ // at the JSDoc terminator on single-line blocks.
26
+ const EXPOSE_TAG_RE = /(?:^\s*|\n\s*\*\s*)@expose\b(?:[ \t]+([^\s*]+))?/im;
27
+ export function extractExposesFromSource(source) {
28
+ const exposes = [];
29
+ JSDOC_BLOCK_RE.lastIndex = 0;
30
+ let block;
31
+ while ((block = JSDOC_BLOCK_RE.exec(source)) !== null) {
32
+ const jsdocBody = block[1];
33
+ const exposeMatch = jsdocBody.match(EXPOSE_TAG_RE);
34
+ if (!exposeMatch)
35
+ continue;
36
+ const after = source.slice(block.index + block[0].length);
37
+ const methodMatch = after.match(METHOD_RE);
38
+ if (!methodMatch)
39
+ continue;
40
+ // The argument after @expose, if present, is one of: 'public' (widens
41
+ // exposure to anonymous callers) or anything else (treated as no
42
+ // visibility hint — defaults to private). This is intentionally loose:
43
+ // adding new visibility levels later doesn't break existing callers
44
+ // that already wrote `@expose public`.
45
+ const visibility = exposeMatch[1]?.trim().toLowerCase() === 'public' ? 'public' : 'private';
46
+ exposes.push({ handler: methodMatch[1], visibility });
47
+ }
48
+ return exposes;
49
+ }
50
+ /**
51
+ * Convert a method name to its kebab-cased route segment. Matches the
52
+ * convention the bridge fetch fallback uses (`fetch('/api/<kebab>', ...)`).
53
+ *
54
+ * getCurrentUser → get-current-user
55
+ * listUsers → list-users
56
+ * billing → billing
57
+ */
58
+ export function methodToKebab(name) {
59
+ return name
60
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
61
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
62
+ .toLowerCase();
63
+ }
64
+ //# sourceMappingURL=expose-route-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expose-route-extractor.js","sourceRoot":"","sources":["../../src/shared/expose-route-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAWH,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAC/C,MAAM,SAAS,GAAG,mEAAmE,CAAC;AACtF,0EAA0E;AAC1E,wEAAwE;AACxE,sEAAsE;AACtE,wEAAwE;AACxE,iDAAiD;AACjD,MAAM,aAAa,GAAG,oDAAoD,CAAC;AAE3E,MAAM,UAAU,wBAAwB,CAAC,MAAc;IACrD,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,cAAc,CAAC,SAAS,GAAG,CAAC,CAAC;IAC7B,IAAI,KAA6B,CAAC;IAClC,OAAO,CAAC,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACtD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QACnD,IAAI,CAAC,WAAW;YAAE,SAAS;QAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,WAAW;YAAE,SAAS;QAC3B,sEAAsE;QACtE,iEAAiE;QACjE,uEAAuE;QACvE,oEAAoE;QACpE,uCAAuC;QACvC,MAAM,UAAU,GACd,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;QAC3E,OAAO,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI;SACR,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,uBAAuB,EAAE,OAAO,CAAC;SACzC,WAAW,EAAE,CAAC;AACnB,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Extract auth claims from incoming HTTP request headers.
3
+ *
4
+ * Used by the standalone HTTP server (Track C closure) to discover which
5
+ * claim bag the per-claim instance pool should key on. Two sources are
6
+ * recognized today, matching what `extractInstance` in the Cloudflare
7
+ * Worker template recognizes — same headers, same precedence:
8
+ *
9
+ * 1. `Cf-Access-Authenticated-User-Email` — set by Cloudflare Access at
10
+ * the edge after JWT verification. Trusted (origin is behind CF).
11
+ * 2. `Cf-Access-Jwt-Assertion` — the raw JWT. Decoded payload fields
12
+ * (email, sub, ...) are merged into the claim bag. The signature is
13
+ * NOT re-verified; CF already validated it before forwarding to the
14
+ * origin. If you run the standalone server outside Cloudflare and
15
+ * want a different trust boundary, add a verification step here.
16
+ *
17
+ * The standalone server is the only consumer right now. The MCP daemon
18
+ * pulls claims from its CLI / streamable-HTTP authentication path; the
19
+ * Cloudflare Worker template does its own header read. Keep the three
20
+ * surfaces in sync when adding a new claim source.
21
+ */
22
+ import type { IncomingHttpHeaders } from 'node:http';
23
+ /**
24
+ * Read claims from CF Access headers. Returns undefined when no
25
+ * recognized auth headers are present so the caller can fall back to the
26
+ * default instance without a bound claim.
27
+ *
28
+ * The returned bag uses lowercased standard claim names (`email`, `sub`,
29
+ * etc.) so `resolveInstanceFromClaims` finds them by their canonical key
30
+ * regardless of the JWT's casing.
31
+ */
32
+ export declare function extractClaimsFromHeaders(headers: IncomingHttpHeaders): Record<string, unknown> | undefined;
33
+ //# sourceMappingURL=extract-claims.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-claims.d.ts","sourceRoot":"","sources":["../../src/shared/extract-claims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGrD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,mBAAmB,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CA8BrC"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Extract auth claims from incoming HTTP request headers.
3
+ *
4
+ * Used by the standalone HTTP server (Track C closure) to discover which
5
+ * claim bag the per-claim instance pool should key on. Two sources are
6
+ * recognized today, matching what `extractInstance` in the Cloudflare
7
+ * Worker template recognizes — same headers, same precedence:
8
+ *
9
+ * 1. `Cf-Access-Authenticated-User-Email` — set by Cloudflare Access at
10
+ * the edge after JWT verification. Trusted (origin is behind CF).
11
+ * 2. `Cf-Access-Jwt-Assertion` — the raw JWT. Decoded payload fields
12
+ * (email, sub, ...) are merged into the claim bag. The signature is
13
+ * NOT re-verified; CF already validated it before forwarding to the
14
+ * origin. If you run the standalone server outside Cloudflare and
15
+ * want a different trust boundary, add a verification step here.
16
+ *
17
+ * The standalone server is the only consumer right now. The MCP daemon
18
+ * pulls claims from its CLI / streamable-HTTP authentication path; the
19
+ * Cloudflare Worker template does its own header read. Keep the three
20
+ * surfaces in sync when adding a new claim source.
21
+ */
22
+ import { Buffer } from 'node:buffer';
23
+ /**
24
+ * Read claims from CF Access headers. Returns undefined when no
25
+ * recognized auth headers are present so the caller can fall back to the
26
+ * default instance without a bound claim.
27
+ *
28
+ * The returned bag uses lowercased standard claim names (`email`, `sub`,
29
+ * etc.) so `resolveInstanceFromClaims` finds them by their canonical key
30
+ * regardless of the JWT's casing.
31
+ */
32
+ export function extractClaimsFromHeaders(headers) {
33
+ const claims = {};
34
+ const cfEmail = headers['cf-access-authenticated-user-email'];
35
+ if (typeof cfEmail === 'string' && cfEmail.length > 0) {
36
+ claims.email = cfEmail;
37
+ }
38
+ const cfJwt = headers['cf-access-jwt-assertion'];
39
+ if (typeof cfJwt === 'string' && cfJwt.includes('.')) {
40
+ try {
41
+ const part = cfJwt.split('.')[1];
42
+ const padded = part + '='.repeat((4 - (part.length % 4)) % 4);
43
+ const decoded = Buffer.from(padded.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8');
44
+ const payload = JSON.parse(decoded);
45
+ // Don't overwrite a claim already set from a more specific header.
46
+ for (const [k, v] of Object.entries(payload)) {
47
+ if (!(k in claims))
48
+ claims[k] = v;
49
+ }
50
+ }
51
+ catch {
52
+ // Malformed JWT — silently ignore, the email header (if present)
53
+ // already populated the bag. A misformed JWT shouldn't 500 the
54
+ // request; the auth gate downstream decides what to do with no
55
+ // claims.
56
+ }
57
+ }
58
+ return Object.keys(claims).length > 0 ? claims : undefined;
59
+ }
60
+ //# sourceMappingURL=extract-claims.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-claims.js","sourceRoot":"","sources":["../../src/shared/extract-claims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAA4B;IAE5B,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,MAAM,OAAO,GAAG,OAAO,CAAC,oCAAoC,CAAC,CAAC;IAC9D,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtD,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAC1F,OAAO,CACR,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAA4B,CAAC;YAC/D,mEAAmE;YACnE,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC7C,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;oBAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,iEAAiE;YACjE,+DAA+D;YAC/D,+DAA+D;YAC/D,UAAU;QACZ,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC7D,CAAC"}
@@ -18,6 +18,12 @@ export interface HttpRouteDef {
18
18
  method: string;
19
19
  path: string;
20
20
  handler: string;
21
+ /**
22
+ * Optional `@format <name>` declared on the same JSDoc block. Used by the
23
+ * HTTP dispatcher's content-negotiation path (Track A) when the handler
24
+ * returns a plain value rather than a Response.
25
+ */
26
+ format?: string;
21
27
  }
22
28
  export declare function extractHttpRoutesFromSource(source: string): HttpRouteDef[];
23
29
  //# sourceMappingURL=http-route-extractor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-route-extractor.d.ts","sourceRoot":"","sources":["../../src/shared/http-route-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAQ1E"}
1
+ {"version":3,"file":"http-route-extractor.d.ts","sourceRoot":"","sources":["../../src/shared/http-route-extractor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAYD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAqB1E"}
@@ -1,10 +1,34 @@
1
- const ROUTE_RE = /\/\*\*[\s\S]*?@(get|post)\s+(\/[^\s*]*)[\s\S]*?\*\/\s*(?:async\s+)?(\w+)\s*\(/gi;
1
+ // Find each JSDoc close `*/` and the method declaration that follows.
2
+ // Crucially we use the JSDoc body BETWEEN the opener and THIS specific close —
3
+ // not greedy back to a far-earlier `/**`. That keeps tag scans (@format, ...)
4
+ // scoped to the SAME block as the route directive, rather than leaking from
5
+ // a class-level docblock that happens to mention `@format` in prose.
6
+ const JSDOC_BLOCK_RE = /\/\*\*([\s\S]*?)\*\//g;
7
+ const METHOD_RE = /^\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?(\w+)\s*\(/;
8
+ const ROUTE_TAG_RE = /@(get|post)\s+(\/[^\s*]*)/i;
9
+ const FORMAT_RE = /@format\s+([\w:-]+)/i;
2
10
  export function extractHttpRoutesFromSource(source) {
3
11
  const routes = [];
4
- ROUTE_RE.lastIndex = 0;
5
- let m;
6
- while ((m = ROUTE_RE.exec(source)) !== null) {
7
- routes.push({ method: m[1].toUpperCase(), path: m[2], handler: m[3] });
12
+ JSDOC_BLOCK_RE.lastIndex = 0;
13
+ let block;
14
+ while ((block = JSDOC_BLOCK_RE.exec(source)) !== null) {
15
+ const jsdocBody = block[1];
16
+ const routeMatch = jsdocBody.match(ROUTE_TAG_RE);
17
+ if (!routeMatch)
18
+ continue;
19
+ const after = source.slice(block.index + block[0].length);
20
+ const methodMatch = after.match(METHOD_RE);
21
+ if (!methodMatch)
22
+ continue;
23
+ const route = {
24
+ method: routeMatch[1].toUpperCase(),
25
+ path: routeMatch[2],
26
+ handler: methodMatch[1],
27
+ };
28
+ const formatMatch = jsdocBody.match(FORMAT_RE);
29
+ if (formatMatch)
30
+ route.format = formatMatch[1];
31
+ routes.push(route);
8
32
  }
9
33
  return routes;
10
34
  }
@@ -1 +1 @@
1
- {"version":3,"file":"http-route-extractor.js","sourceRoot":"","sources":["../../src/shared/http-route-extractor.ts"],"names":[],"mappings":"AAsBA,MAAM,QAAQ,GAAG,iFAAiF,CAAC;AAEnG,MAAM,UAAU,2BAA2B,CAAC,MAAc;IACxD,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,QAAQ,CAAC,SAAS,GAAG,CAAC,CAAC;IACvB,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"http-route-extractor.js","sourceRoot":"","sources":["../../src/shared/http-route-extractor.ts"],"names":[],"mappings":"AA4BA,sEAAsE;AACtE,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,qEAAqE;AACrE,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAC/C,MAAM,SAAS,GAAG,mEAAmE,CAAC;AACtF,MAAM,YAAY,GAAG,4BAA4B,CAAC;AAClD,MAAM,SAAS,GAAG,sBAAsB,CAAC;AAEzC,MAAM,UAAU,2BAA2B,CAAC,MAAc;IACxD,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,cAAc,CAAC,SAAS,GAAG,CAAC,CAAC;IAC7B,IAAI,KAA6B,CAAC;IAClC,OAAO,CAAC,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACtD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,UAAU,GAAG,SAAS,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QACjD,IAAI,CAAC,UAAU;YAAE,SAAS;QAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,WAAW;YAAE,SAAS;QAC3B,MAAM,KAAK,GAAiB;YAC1B,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;YACnC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;YACnB,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;SACxB,CAAC;QACF,MAAM,WAAW,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,WAAW;YAAE,KAAK,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QAC/C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Auth → instance binding registry (Track C).
3
+ *
4
+ * For multi-tenant `@stateful` photons: when a caller authenticates, the
5
+ * loader looks up the photon's `@auth` scheme here to find which JWT/identity
6
+ * claim should drive the per-user instance name. Photons get one isolated
7
+ * `this.memory` namespace per claim value without their authors writing
8
+ * per-call routing.
9
+ *
10
+ * /‍** @auth cf-access *‍/ → caller.claims.email → instance "alice@x.com"
11
+ * /‍** @auth oauth *‍/ → caller.claims.sub → instance "user_42"
12
+ *
13
+ * The registry is a pure module (no I/O, no state). The Cloudflare Worker
14
+ * template runs the equivalent mapping at the outer Worker layer (per-DO
15
+ * selection); see `extractInstance` in `templates/cloudflare/worker.ts.template`.
16
+ *
17
+ * Scope note: this fills `parameters._targetInstance` for downstream
18
+ * consumers (the daemon at `daemon/server.ts:4408` and the streamable-HTTP
19
+ * transport at `:1928-1933`). The standalone `photon mcp` server is
20
+ * single-tenant by design — it loads one photon instance per process — so
21
+ * the binding has no effect there. Multi-tenant deploys go through the
22
+ * daemon or Cloudflare.
23
+ */
24
+ /**
25
+ * Resolve the instance name a caller should be routed to, based on their
26
+ * auth scheme + claims. Returns undefined when the scheme is unknown, the
27
+ * required claim is missing, or claims is empty — the caller should leave
28
+ * `_targetInstance` unset and fall back to the default singleton.
29
+ *
30
+ * @param scheme Value from `@auth <scheme>` (e.g. `cf-access`, `oauth`)
31
+ * @param claims Authenticated caller's claim bag (from `caller.claims`)
32
+ * @param override Optional explicit claim name from `@auth <scheme> claim=<name>`
33
+ * — wins over the scheme default. Lets a photon route by
34
+ * org id, tenant slug, etc., without a new scheme entry.
35
+ */
36
+ export declare function resolveInstanceFromClaims(scheme: string | undefined, claims: Record<string, unknown> | undefined, override?: string): string | undefined;
37
+ /**
38
+ * Parse the `claim=<name>` modifier from an `@auth` directive value.
39
+ *
40
+ * `cf-access` → { scheme: 'cf-access', claim: undefined }
41
+ * `cf-access claim=email` → { scheme: 'cf-access', claim: 'email' }
42
+ * `oauth claim=org_id` → { scheme: 'oauth', claim: 'org_id' }
43
+ *
44
+ * Loader callers pass the resulting `claim` field to
45
+ * `resolveInstanceFromClaims` as the override.
46
+ */
47
+ export declare function parseAuthDirective(value: string | undefined): {
48
+ scheme: string | undefined;
49
+ claim: string | undefined;
50
+ };
51
+ /** Exposed for tests so the table stays in sync with the CF worker template. */
52
+ export declare function getSchemeDefaultClaim(scheme: string): string | undefined;
53
+ //# sourceMappingURL=instance-binding.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-binding.d.ts","sourceRoot":"","sources":["../../src/shared/instance-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAYH;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC3C,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAOpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG;IAC7D,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B,CAaA;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAExE"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Auth → instance binding registry (Track C).
3
+ *
4
+ * For multi-tenant `@stateful` photons: when a caller authenticates, the
5
+ * loader looks up the photon's `@auth` scheme here to find which JWT/identity
6
+ * claim should drive the per-user instance name. Photons get one isolated
7
+ * `this.memory` namespace per claim value without their authors writing
8
+ * per-call routing.
9
+ *
10
+ * /‍** @auth cf-access *‍/ → caller.claims.email → instance "alice@x.com"
11
+ * /‍** @auth oauth *‍/ → caller.claims.sub → instance "user_42"
12
+ *
13
+ * The registry is a pure module (no I/O, no state). The Cloudflare Worker
14
+ * template runs the equivalent mapping at the outer Worker layer (per-DO
15
+ * selection); see `extractInstance` in `templates/cloudflare/worker.ts.template`.
16
+ *
17
+ * Scope note: this fills `parameters._targetInstance` for downstream
18
+ * consumers (the daemon at `daemon/server.ts:4408` and the streamable-HTTP
19
+ * transport at `:1928-1933`). The standalone `photon mcp` server is
20
+ * single-tenant by design — it loads one photon instance per process — so
21
+ * the binding has no effect there. Multi-tenant deploys go through the
22
+ * daemon or Cloudflare.
23
+ */
24
+ /**
25
+ * Default claim name for each well-known `@auth` scheme. Matches the
26
+ * Cloudflare Worker template's `extractInstance` behavior so an auth-bound
27
+ * photon picks the same instance name on the local daemon and on CF.
28
+ */
29
+ const SCHEME_DEFAULT_CLAIM = {
30
+ 'cf-access': 'email',
31
+ oauth: 'sub',
32
+ };
33
+ /**
34
+ * Resolve the instance name a caller should be routed to, based on their
35
+ * auth scheme + claims. Returns undefined when the scheme is unknown, the
36
+ * required claim is missing, or claims is empty — the caller should leave
37
+ * `_targetInstance` unset and fall back to the default singleton.
38
+ *
39
+ * @param scheme Value from `@auth <scheme>` (e.g. `cf-access`, `oauth`)
40
+ * @param claims Authenticated caller's claim bag (from `caller.claims`)
41
+ * @param override Optional explicit claim name from `@auth <scheme> claim=<name>`
42
+ * — wins over the scheme default. Lets a photon route by
43
+ * org id, tenant slug, etc., without a new scheme entry.
44
+ */
45
+ export function resolveInstanceFromClaims(scheme, claims, override) {
46
+ if (!claims || typeof claims !== 'object')
47
+ return undefined;
48
+ const claimName = override ?? (scheme ? SCHEME_DEFAULT_CLAIM[scheme] : undefined);
49
+ if (!claimName)
50
+ return undefined;
51
+ const value = claims[claimName];
52
+ if (typeof value === 'string' && value.length > 0)
53
+ return value;
54
+ return undefined;
55
+ }
56
+ /**
57
+ * Parse the `claim=<name>` modifier from an `@auth` directive value.
58
+ *
59
+ * `cf-access` → { scheme: 'cf-access', claim: undefined }
60
+ * `cf-access claim=email` → { scheme: 'cf-access', claim: 'email' }
61
+ * `oauth claim=org_id` → { scheme: 'oauth', claim: 'org_id' }
62
+ *
63
+ * Loader callers pass the resulting `claim` field to
64
+ * `resolveInstanceFromClaims` as the override.
65
+ */
66
+ export function parseAuthDirective(value) {
67
+ if (!value)
68
+ return { scheme: undefined, claim: undefined };
69
+ const parts = value.trim().split(/\s+/);
70
+ const scheme = parts[0];
71
+ let claim;
72
+ for (const part of parts.slice(1)) {
73
+ const m = part.match(/^claim=(.+)$/);
74
+ if (m) {
75
+ claim = m[1];
76
+ break;
77
+ }
78
+ }
79
+ return { scheme, claim };
80
+ }
81
+ /** Exposed for tests so the table stays in sync with the CF worker template. */
82
+ export function getSchemeDefaultClaim(scheme) {
83
+ return SCHEME_DEFAULT_CLAIM[scheme];
84
+ }
85
+ //# sourceMappingURL=instance-binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instance-binding.js","sourceRoot":"","sources":["../../src/shared/instance-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;GAIG;AACH,MAAM,oBAAoB,GAA2B;IACnD,WAAW,EAAE,OAAO;IACpB,KAAK,EAAE,KAAK;CACb,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,yBAAyB,CACvC,MAA0B,EAC1B,MAA2C,EAC3C,QAAiB;IAEjB,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC5D,MAAM,SAAS,GAAG,QAAQ,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAClF,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAChE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAyB;IAI1D,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAC3D,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,KAAyB,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACrC,IAAI,CAAC,EAAE,CAAC;YACN,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACb,MAAM;QACR,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AAC3B,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,qBAAqB,CAAC,MAAc;IAClD,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC"}
@@ -23,6 +23,7 @@ export interface PhotonClassWithMeta extends PhotonClassExtended {
23
23
  method: string;
24
24
  path: string;
25
25
  handler: string;
26
+ format?: string;
26
27
  }>;
27
28
  }
28
29
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"server-types.d.ts","sourceRoot":"","sources":["../../src/types/server-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,uCAAuC;IACvC,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxE;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,+CAA+C;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gDAAgD;IAChD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
1
+ {"version":3,"file":"server-types.d.ts","sourceRoot":"","sources":["../../src/types/server-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,mBAAoB,SAAQ,mBAAmB;IAC9D,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oDAAoD;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,uCAAuC;IACvC,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzF;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClF,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,+CAA+C;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gDAAgD;IAChD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portel/photon",
3
- "version": "1.28.2",
3
+ "version": "1.29.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",
@@ -28,7 +28,7 @@
28
28
  "prepublishOnly": "node -e \"const p=require('./package.json'); if(JSON.stringify(p.dependencies).includes('file:')) { console.error('ERROR: file: dependency found.'); process.exit(1); }\" && node -e \"const fs=require('fs'),path=require('path'); const p=require('./package.json'); for(const d of Object.keys(p.dependencies||{})){const t=path.join('node_modules',d); if(fs.lstatSync(t).isSymbolicLink()){console.error('ERROR: '+d+' is npm-linked. Run: npm unlink '+d+' && npm install '+d); process.exit(1);}}\" && npm run build && npm run build:beam",
29
29
  "test": "bash scripts/run-tests.sh",
30
30
  "test:chain": "npm run test:all",
31
- "test:all": "npm run build && npm run test:security && npm run test:schema && npm run test:marketplace && npm run test:loader && npm run test:server && npm run test:integration && npm run test:ui-resources && npm run test:client-adaptive && npm run test:zero-config && npm run test:mcp-config && npm run test:cli && npm run test:logger && npm run test:error-handler && npm run test:validation && npm run test:daemon-pubsub && npm run test:daemon-buffer && npm run test:instance-drift && npm run test:daemon-watcher && npm run test:ui-rendering && npm run test:photon-instance-manager && npm run test:viewport-aware-proxy && npm run test:viewport-manager && npm run test:pagination-integration && npm run test:pagination-performance && npm run test:pagination-phase5 && npm run test:pagination-phase5c && npm run test:pagination-phase5d && npm run test:phase6a && npm run test:phase6b && npm run test:phase6c && npm run test:phase6d && npm run test:promises && npm run test:readme",
31
+ "test:all": "npm run build && npm run test:security && npm run test:schema && npm run test:marketplace && npm run test:loader && npm run test:server && npm run test:integration && npm run test:byte-compat && npm run test:format-registry && npm run test:content-negotiation && npm run test:ui-resources && npm run test:client-adaptive && npm run test:zero-config && npm run test:mcp-config && npm run test:cli && npm run test:logger && npm run test:error-handler && npm run test:validation && npm run test:daemon-pubsub && npm run test:daemon-buffer && npm run test:instance-drift && npm run test:daemon-watcher && npm run test:ui-rendering && npm run test:photon-instance-manager && npm run test:viewport-aware-proxy && npm run test:viewport-manager && npm run test:pagination-integration && npm run test:pagination-performance && npm run test:pagination-phase5 && npm run test:pagination-phase5c && npm run test:pagination-phase5d && npm run test:phase6a && npm run test:phase6b && npm run test:phase6c && npm run test:phase6d && npm run test:promises && npm run test:readme",
32
32
  "test:daemon-watcher": "npx tsx tests/daemon-watcher.test.ts",
33
33
  "test:instance-drift": "npx tsx tests/instance-drift.test.ts",
34
34
  "test:photon-instance-manager": "npx vitest run tests/photon-instance-manager.test.ts",
@@ -63,6 +63,9 @@
63
63
  "test:loader": "npx tsx tests/loader.test.ts",
64
64
  "test:server": "npx tsx tests/server.test.ts",
65
65
  "test:integration": "npx tsx tests/integration.test.ts",
66
+ "test:byte-compat": "RUN_E2E=1 npx vitest run tests/v128-byte-compat.test.ts",
67
+ "test:format-registry": "npx vitest run tests/format-registry.test.ts",
68
+ "test:content-negotiation": "RUN_E2E=1 npx vitest run tests/http-content-negotiation.test.ts",
66
69
  "test:ui-resources": "npx tsx tests/ui-resources.test.ts",
67
70
  "test:ui-rendering": "npx tsx --test tests/ui/result-rendering.test.ts",
68
71
  "test:client-adaptive": "npx tsx tests/client-adaptive.test.ts",
@@ -113,7 +116,7 @@
113
116
  "@modelcontextprotocol/ext-apps": "^1.0.1",
114
117
  "@modelcontextprotocol/sdk": "^1.25.2",
115
118
  "@portel/cli": "^1.1.0",
116
- "@portel/photon-core": "^2.25.0",
119
+ "@portel/photon-core": "^2.26.0",
117
120
  "boxen": "^8.0.1",
118
121
  "chalk": "^5.4.1",
119
122
  "chart.js": "^4.5.1",
@@ -570,6 +570,19 @@ function matchHttpRoute(
570
570
  return null;
571
571
  }
572
572
 
573
+ /**
574
+ * camelCase → kebab-case for `/api/<kebab>` route segments.
575
+ * Mirrors `src/shared/expose-route-extractor.ts#methodToKebab`. Keep both
576
+ * implementations in lock-step so an `@expose`'d method binds to the
577
+ * same path on the local server and on Cloudflare.
578
+ */
579
+ function methodToKebab(name: string): string {
580
+ return name
581
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
582
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
583
+ .toLowerCase();
584
+ }
585
+
573
586
  function matchPathPattern(
574
587
  pattern: string,
575
588
  pathname: string
@@ -698,6 +711,14 @@ abstract class BasePhotonDO extends DurableObject<Env> {
698
711
  protected abstract readonly photonName: string;
699
712
  protected abstract readonly toolDefinitions: any[];
700
713
  protected readonly httpRoutes: { method: string; path: string; handler: string }[] = [];
714
+ /**
715
+ * Methods declared with `@expose` (Track C). Bound to `POST /api/<kebab>`
716
+ * with a SameSite-style visibility check. `private` requires a browser-set
717
+ * `Sec-Fetch-Site: same-origin` (or `same-site`) header — anything else,
718
+ * or no header, is denied. `public` skips the check (anonymous third-party
719
+ * callers like RSS readers).
720
+ */
721
+ protected readonly exposes: { handler: string; visibility: 'private' | 'public' }[] = [];
701
722
  protected abstract createPhoton(): any;
702
723
 
703
724
  protected photon: any;
@@ -856,6 +877,67 @@ abstract class BasePhotonDO extends DurableObject<Env> {
856
877
  }
857
878
  }
858
879
 
880
+ // Track C: @expose auto-RPC. POST /api/<kebab> dispatches to an
881
+ // `@expose`'d method. `@get`/`@post` already won the matchedRoute
882
+ // check above, so this only fires when no explicit HTTP route
883
+ // matched. Visibility gate: `private` requires a browser-set
884
+ // `Sec-Fetch-Site: same-origin` (or `same-site`) header. The Worker
885
+ // is on the public internet — there is no localhost analog — so an
886
+ // absent header means deny.
887
+ if (
888
+ this.exposes.length > 0 &&
889
+ request.method === 'POST' &&
890
+ url.pathname.startsWith('/api/')
891
+ ) {
892
+ const segment = url.pathname.slice('/api/'.length);
893
+ const exposed = this.exposes.find((e) => methodToKebab(e.handler) === segment);
894
+ if (exposed) {
895
+ if (exposed.visibility === 'private') {
896
+ const sfs = request.headers.get('sec-fetch-site')?.toLowerCase() ?? '';
897
+ if (sfs !== 'same-origin' && sfs !== 'same-site') {
898
+ return new Response('Forbidden: cross-site @expose call', {
899
+ status: 403,
900
+ headers: CORS_HEADERS,
901
+ });
902
+ }
903
+ }
904
+ const fn = (this.photon as any)[exposed.handler];
905
+ if (typeof fn !== 'function') {
906
+ return new Response(`Unknown handler: ${exposed.handler}`, {
907
+ status: 500,
908
+ headers: CORS_HEADERS,
909
+ });
910
+ }
911
+ let parsed: unknown = {};
912
+ try {
913
+ const text = await request.text();
914
+ if (text.length > 0) parsed = JSON.parse(text);
915
+ } catch {
916
+ return new Response('Invalid JSON body', { status: 400, headers: CORS_HEADERS });
917
+ }
918
+ try {
919
+ const result = await fn.call(this.photon, parsed);
920
+ if (result instanceof Response) return result;
921
+ return Response.json(result, { headers: CORS_HEADERS });
922
+ } catch (error: any) {
923
+ return new Response(error?.message ?? 'Internal Server Error', {
924
+ status: 500,
925
+ headers: CORS_HEADERS,
926
+ });
927
+ }
928
+ }
929
+ }
930
+
931
+ // Fall through to the [assets] binding (Track E). The wrangler.toml
932
+ // emits this binding only when the host photon has an `assets/`
933
+ // companion folder, so the typeof guard keeps non-asset deploys
934
+ // quiet (no extra fetch round-trip when the binding is absent).
935
+ const assets = (this.env as any).ASSETS as { fetch: (req: Request) => Promise<Response> } | undefined;
936
+ if (assets && request.method === 'GET') {
937
+ const assetResponse = await assets.fetch(request);
938
+ if (assetResponse.status !== 404) return assetResponse;
939
+ }
940
+
859
941
  return new Response('Not Found', { status: 404, headers: CORS_HEADERS });
860
942
  }
861
943
 
@@ -1043,13 +1125,18 @@ const CF_ACCESS_ENABLED = __CF_ACCESS_ENABLED__;
1043
1125
  */
1044
1126
  function extractInstance(request: Request): string {
1045
1127
  if (CF_ACCESS_ENABLED) {
1128
+ const headerEmail = request.headers.get('Cf-Access-Authenticated-User-Email');
1129
+ if (headerEmail) return headerEmail;
1046
1130
  const jwt = request.headers.get('Cf-Access-Jwt-Assertion');
1047
1131
  if (jwt) {
1048
1132
  try {
1049
- const payload = JSON.parse(atob(jwt.split('.')[1]));
1133
+ const part = jwt.split('.')[1];
1134
+ const b64 = part.replace(/-/g, '+').replace(/_/g, '/');
1135
+ const padded = b64 + '==='.slice((b64.length + 3) % 4);
1136
+ const payload = JSON.parse(atob(padded));
1050
1137
  if (payload?.email) return payload.email as string;
1051
- } catch {
1052
- // malformed JWT fall through to default resolution
1138
+ } catch (err) {
1139
+ console.warn('extractInstance: JWT parse failed', err);
1053
1140
  }
1054
1141
  }
1055
1142
  }
@@ -27,7 +27,7 @@ __DURABLE_OBJECT_BINDINGS__
27
27
  [[migrations]]
28
28
  tag = "v1"
29
29
  new_sqlite_classes = __SQLITE_CLASSES__
30
-
30
+ __ASSETS_BLOCK__
31
31
  # Uncomment to add environment variables
32
32
  # [vars]
33
33
  # API_KEY = "your-api-key"