@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.
- package/README.md +1 -0
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +77 -43
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts +7 -0
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +228 -29
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/types.d.ts +9 -1
- package/dist/auto-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/types.js.map +1 -1
- package/dist/beam.bundle.js +32 -2
- package/dist/beam.bundle.js.map +2 -2
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +123 -15
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +45 -11
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/server.js +41 -0
- package/dist/daemon/server.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +82 -2
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.d.ts.map +1 -1
- package/dist/editor-support/docblock-tag-catalog.js +32 -2
- package/dist/editor-support/docblock-tag-catalog.js.map +1 -1
- package/dist/format/registry.d.ts +83 -0
- package/dist/format/registry.d.ts.map +1 -0
- package/dist/format/registry.js +139 -0
- package/dist/format/registry.js.map +1 -0
- package/dist/format/seed.d.ts +18 -0
- package/dist/format/seed.d.ts.map +1 -0
- package/dist/format/seed.js +246 -0
- package/dist/format/seed.js.map +1 -0
- package/dist/loader.d.ts +18 -0
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +130 -22
- package/dist/loader.js.map +1 -1
- package/dist/photons/maker.photon.d.ts +2 -2
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +5 -6
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +5 -6
- package/dist/resource-server.d.ts +52 -12
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +205 -50
- package/dist/resource-server.js.map +1 -1
- package/dist/server.d.ts +75 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +515 -53
- package/dist/server.js.map +1 -1
- package/dist/shared/asset-encoding.d.ts +30 -0
- package/dist/shared/asset-encoding.d.ts.map +1 -0
- package/dist/shared/asset-encoding.js +0 -0
- package/dist/shared/asset-encoding.js.map +1 -0
- package/dist/shared/cross-origin-headers.d.ts +47 -0
- package/dist/shared/cross-origin-headers.d.ts.map +1 -0
- package/dist/shared/cross-origin-headers.js +61 -0
- package/dist/shared/cross-origin-headers.js.map +1 -0
- package/dist/shared/expose-route-extractor.d.ts +36 -0
- package/dist/shared/expose-route-extractor.d.ts.map +1 -0
- package/dist/shared/expose-route-extractor.js +64 -0
- package/dist/shared/expose-route-extractor.js.map +1 -0
- package/dist/shared/extract-claims.d.ts +33 -0
- package/dist/shared/extract-claims.d.ts.map +1 -0
- package/dist/shared/extract-claims.js +60 -0
- package/dist/shared/extract-claims.js.map +1 -0
- package/dist/shared/http-route-extractor.d.ts +6 -0
- package/dist/shared/http-route-extractor.d.ts.map +1 -1
- package/dist/shared/http-route-extractor.js +29 -5
- package/dist/shared/http-route-extractor.js.map +1 -1
- package/dist/shared/instance-binding.d.ts +53 -0
- package/dist/shared/instance-binding.d.ts.map +1 -0
- package/dist/shared/instance-binding.js +85 -0
- package/dist/shared/instance-binding.js.map +1 -0
- package/dist/types/server-types.d.ts +1 -0
- package/dist/types/server-types.d.ts.map +1 -1
- package/package.json +6 -3
- package/templates/cloudflare/worker.ts.template +90 -3
- 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;
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
let
|
|
6
|
-
while ((
|
|
7
|
-
|
|
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":"
|
|
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"}
|
|
@@ -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;
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
1138
|
+
} catch (err) {
|
|
1139
|
+
console.warn('extractInstance: JWT parse failed', err);
|
|
1053
1140
|
}
|
|
1054
1141
|
}
|
|
1055
1142
|
}
|