@portel/photon-core 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +121 -0
  2. package/dist/base.d.ts +27 -0
  3. package/dist/base.d.ts.map +1 -1
  4. package/dist/base.js +27 -0
  5. package/dist/base.js.map +1 -1
  6. package/dist/cf.d.ts +121 -0
  7. package/dist/cf.d.ts.map +1 -0
  8. package/dist/cf.js +65 -0
  9. package/dist/cf.js.map +1 -0
  10. package/dist/cloudflare.d.ts +110 -0
  11. package/dist/cloudflare.d.ts.map +1 -0
  12. package/dist/cloudflare.js +138 -0
  13. package/dist/cloudflare.js.map +1 -0
  14. package/dist/collections/Collection.d.ts +12 -0
  15. package/dist/collections/Collection.d.ts.map +1 -1
  16. package/dist/collections/Collection.js +19 -0
  17. package/dist/collections/Collection.js.map +1 -1
  18. package/dist/env.d.ts +29 -0
  19. package/dist/env.d.ts.map +1 -0
  20. package/dist/env.js +2 -0
  21. package/dist/env.js.map +1 -0
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/mixins.d.ts.map +1 -1
  27. package/dist/mixins.js +10 -0
  28. package/dist/mixins.js.map +1 -1
  29. package/dist/photon-loader-lite.d.ts +17 -0
  30. package/dist/photon-loader-lite.d.ts.map +1 -1
  31. package/dist/photon-loader-lite.js +150 -1
  32. package/dist/photon-loader-lite.js.map +1 -1
  33. package/dist/schema-extractor.d.ts +4 -1
  34. package/dist/schema-extractor.d.ts.map +1 -1
  35. package/dist/schema-extractor.js +54 -0
  36. package/dist/schema-extractor.js.map +1 -1
  37. package/dist/types.d.ts +1 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/types.js.map +1 -1
  40. package/package.json +3 -2
  41. package/src/base.ts +29 -0
  42. package/src/cf.ts +165 -0
  43. package/src/cloudflare.ts +198 -0
  44. package/src/collections/Collection.ts +19 -0
  45. package/src/env.ts +28 -0
  46. package/src/index.ts +27 -0
  47. package/src/mixins.ts +12 -0
  48. package/src/photon-loader-lite.ts +185 -0
  49. package/src/schema-extractor.ts +74 -1
  50. package/src/types.ts +16 -1
package/src/cf.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Cloudflare capability surface — `this.cf.*`
3
+ *
4
+ * Photons reach into Cloudflare features (R2, KV, D1, Workers AI, Queues,
5
+ * Vectorize, Images, Browser Rendering, Durable Objects) through one
6
+ * namespace. The runtime adapter that wraps a photon decides what backs
7
+ * each binding:
8
+ *
9
+ * - Local: `miniflare` provides an in-process sandbox seeded from the
10
+ * photon's `protected cfBindings = { ... }` config.
11
+ * - Deployed Worker: the real `env` is proxied through.
12
+ *
13
+ * Both paths satisfy the same shape, so the photon source is unchanged.
14
+ *
15
+ * Typing: we publish minimal structural shapes here rather than re-exporting
16
+ * `@cloudflare/workers-types`. The cost of importing the full workers-types
17
+ * tree (~6k lines of declarations) into every photon's transform pipeline
18
+ * showed up as flaky `EPIPE` errors under tsx. Photons that already use
19
+ * workers-types get exact compatibility through structural typing — assign
20
+ * a `R2Bucket` from workers-types into our `R2BucketLike` and TS accepts
21
+ * it. Photons that don't use workers-types still see typed methods on
22
+ * the namespace and full autocomplete on the runtime methods we expose
23
+ * here.
24
+ */
25
+
26
+ /**
27
+ * Minimal R2 bucket surface — covers the methods photons actually call.
28
+ * Structurally compatible with `@cloudflare/workers-types` `R2Bucket`.
29
+ */
30
+ export interface R2BucketLike {
31
+ head(key: string): Promise<unknown>;
32
+ get(key: string, options?: unknown): Promise<unknown>;
33
+ put(key: string, value: unknown, options?: unknown): Promise<unknown>;
34
+ delete(keys: string | string[]): Promise<void>;
35
+ list(options?: unknown): Promise<unknown>;
36
+ }
37
+
38
+ /** Minimal KV namespace surface. */
39
+ export interface KVNamespaceLike {
40
+ get(key: string, options?: unknown): Promise<unknown>;
41
+ put(key: string, value: string | ArrayBuffer | ArrayBufferView | ReadableStream, options?: unknown): Promise<void>;
42
+ delete(key: string): Promise<void>;
43
+ list(options?: unknown): Promise<unknown>;
44
+ }
45
+
46
+ /** Minimal D1 database surface. */
47
+ export interface D1DatabaseLike {
48
+ prepare(query: string): D1PreparedStatementLike;
49
+ batch<T = unknown>(statements: D1PreparedStatementLike[]): Promise<T[]>;
50
+ exec(query: string): Promise<unknown>;
51
+ dump(): Promise<ArrayBuffer>;
52
+ }
53
+
54
+ /** Minimal D1 prepared statement surface. */
55
+ export interface D1PreparedStatementLike {
56
+ bind(...values: unknown[]): D1PreparedStatementLike;
57
+ first<T = unknown>(colName?: string): Promise<T | null>;
58
+ run<T = unknown>(): Promise<T>;
59
+ all<T = unknown>(): Promise<T>;
60
+ raw<T = unknown>(): Promise<T[]>;
61
+ }
62
+
63
+ /** Minimal queue surface. */
64
+ export interface QueueLike<Body = unknown> {
65
+ send(message: Body, options?: unknown): Promise<void>;
66
+ sendBatch(messages: { body: Body }[], options?: unknown): Promise<void>;
67
+ }
68
+
69
+ /** Minimal vectorize surface. */
70
+ export interface VectorizeIndexLike {
71
+ insert(vectors: unknown[]): Promise<unknown>;
72
+ upsert(vectors: unknown[]): Promise<unknown>;
73
+ query(vector: number[], options?: unknown): Promise<unknown>;
74
+ getByIds(ids: string[]): Promise<unknown>;
75
+ deleteByIds(ids: string[]): Promise<unknown>;
76
+ }
77
+
78
+ /** Minimal Workers AI surface. */
79
+ export interface AiLike {
80
+ run(model: string, inputs: unknown, options?: unknown): Promise<unknown>;
81
+ }
82
+
83
+ /** Minimal Cloudflare Images binding surface. */
84
+ export interface ImagesBindingLike {
85
+ info(stream: ReadableStream): Promise<unknown>;
86
+ input(stream: ReadableStream): unknown;
87
+ }
88
+
89
+ /** Minimal Fetcher surface (Service binding / Browser Rendering). */
90
+ export interface FetcherLike {
91
+ fetch(input: string, init?: unknown): Promise<unknown>;
92
+ }
93
+
94
+ /** Minimal Durable Object namespace surface. */
95
+ export interface DurableObjectNamespaceLike {
96
+ idFromName(name: string): unknown;
97
+ idFromString(id: string): unknown;
98
+ newUniqueId(): unknown;
99
+ get(id: unknown): { fetch(input: string, init?: unknown): Promise<unknown> };
100
+ }
101
+
102
+ /**
103
+ * Cloudflare runtime surface as exposed via `this.cf`.
104
+ *
105
+ * Each subnamespace returns either an upstream Cloudflare type or a
106
+ * runtime-supplied proxy with the same shape. Backends:
107
+ * - Local: miniflare instance configured from `protected cfBindings`.
108
+ * - Deployed Worker: real `env.<binding>` passed through.
109
+ */
110
+ export interface CFRuntime {
111
+ r2(name: string): R2BucketLike;
112
+ kv(name: string): KVNamespaceLike;
113
+ d1(name: string): D1DatabaseLike;
114
+ queue<Body = unknown>(name: string): QueueLike<Body>;
115
+ vectorize(name: string): VectorizeIndexLike;
116
+ ai: AiLike;
117
+ images: ImagesBindingLike;
118
+ browser: FetcherLike;
119
+ fetch(input: string, init?: unknown): Promise<unknown>;
120
+ }
121
+
122
+ const HINT =
123
+ 'No Cloudflare runtime is configured. Either run this photon under a ' +
124
+ 'CF-aware host (local Beam with miniflare, or deployed to Cloudflare ' +
125
+ 'Workers), declare `protected cfBindings = { ... }` on the class, or ' +
126
+ 'remove the `this.cf` usage.';
127
+
128
+ function throwingFn(category: string): (...args: unknown[]) => never {
129
+ return (..._args: unknown[]) => {
130
+ throw new Error(`this.cf.${category}() called but ${HINT}`);
131
+ };
132
+ }
133
+
134
+ function throwingProperty(category: string): never {
135
+ return new Proxy(Object.create(null), {
136
+ get(_target, prop) {
137
+ if (prop === Symbol.toPrimitive || prop === 'toString') {
138
+ return () => `[unconfigured this.cf.${category}]`;
139
+ }
140
+ throw new Error(`this.cf.${category}.${String(prop)} accessed but ${HINT}`);
141
+ },
142
+ }) as never;
143
+ }
144
+
145
+ /**
146
+ * Returns a stub `CFRuntime` whose subnamespaces throw a helpful error
147
+ * when used. Callers should hold one instance per photon (mirrors the
148
+ * `_memory` and `_schedule` lazy-init pattern) so identity-sensitive
149
+ * checks remain consistent across reads of `this.cf`.
150
+ */
151
+ export function notConfiguredCF(): CFRuntime {
152
+ return {
153
+ r2: throwingFn('r2') as CFRuntime['r2'],
154
+ kv: throwingFn('kv') as CFRuntime['kv'],
155
+ d1: throwingFn('d1') as CFRuntime['d1'],
156
+ queue: throwingFn('queue') as CFRuntime['queue'],
157
+ vectorize: throwingFn('vectorize') as CFRuntime['vectorize'],
158
+ ai: throwingProperty('ai') as unknown as AiLike,
159
+ images: throwingProperty('images') as unknown as ImagesBindingLike,
160
+ browser: throwingProperty('browser') as unknown as FetcherLike,
161
+ fetch: ((_input: string, _init?: unknown) => {
162
+ throw new Error(`this.cf.fetch() called but ${HINT}`);
163
+ }) as CFRuntime['fetch'],
164
+ };
165
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Cloudflare — wrapped, auto-named CF surface injected via constructor.
3
+ *
4
+ * Where `CloudflareEnv<T>` exposes the raw Worker `env` (escape hatch for
5
+ * service bindings, exotic features, anything we don't wrap), `Cloudflare`
6
+ * is the ergonomic 95% path: photons call `cf.kv()` / `cf.r2()` / etc.
7
+ * with optional qualifier strings, and the runtime resolves bindings by
8
+ * the **photon name + suffix** convention — so authors never pick names
9
+ * that could collide across photons.
10
+ *
11
+ * Importing `Cloudflare` (or `CloudflareEnv`) into a photon's source is
12
+ * the explicit signal that the photon depends on a Cloudflare deploy
13
+ * target. Outside of CF (or its local miniflare mirror), the loader
14
+ * injects a throwing Proxy and the message names the imported symbol so
15
+ * the diagnostic is unambiguous.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import type { Photon, Cloudflare } from "@portel/photon";
20
+ *
21
+ * export class Gallery {
22
+ * constructor(
23
+ * private photon: Photon,
24
+ * private cf: Cloudflare,
25
+ * ) {}
26
+ * async upload(name: string, blob: Blob) {
27
+ * await this.cf.r2().put(name, blob); // gallery_r2
28
+ * await this.cf.d1().prepare("...").bind(name).run(); // gallery_d1
29
+ * await this.photon.memory.set(name, Date.now());
30
+ * }
31
+ * }
32
+ * ```
33
+ *
34
+ * Multi-resource case — qualifier namespaces under the photon:
35
+ * ```ts
36
+ * this.cf.kv() // gallery_kv (default for this photon)
37
+ * this.cf.kv("cache") // gallery_cache_kv
38
+ * this.cf.kv("sessions") // gallery_sessions_kv
39
+ * ```
40
+ *
41
+ * Override path stays in `protected cfBindings = { ... }` — present only
42
+ * when an author needs to point a named binding at a pre-existing CF
43
+ * resource owned outside the photon.
44
+ */
45
+
46
+ import {
47
+ type R2BucketLike,
48
+ type KVNamespaceLike,
49
+ type D1DatabaseLike,
50
+ type QueueLike,
51
+ type VectorizeIndexLike,
52
+ type AiLike,
53
+ type ImagesBindingLike,
54
+ type FetcherLike,
55
+ } from './cf.js';
56
+
57
+ /**
58
+ * The wrapped Cloudflare surface. Methods that take a resource type
59
+ * (kv, r2, d1, queue, vectorize) accept an **optional** qualifier — when
60
+ * omitted, the runtime resolves the photon's default binding for that
61
+ * category. AI / images / browser are single shared bindings per Worker
62
+ * and have no qualifier.
63
+ */
64
+ export interface Cloudflare {
65
+ /** Resolves to `<photonName>_kv` (default) or `<photonName>_<qualifier>_kv`. */
66
+ kv(qualifier?: string): KVNamespaceLike;
67
+ /** Resolves to `<photonName>_r2` (default) or `<photonName>_<qualifier>_r2`. */
68
+ r2(qualifier?: string): R2BucketLike;
69
+ /** Resolves to `<photonName>_d1` (default) or `<photonName>_<qualifier>_d1`. */
70
+ d1(qualifier?: string): D1DatabaseLike;
71
+ /** Resolves to `<photonName>_queue` (default) or `<photonName>_<qualifier>_queue`. */
72
+ queue<Body = unknown>(qualifier?: string): QueueLike<Body>;
73
+ /** Resolves to `<photonName>_vectorize` (default) or `<photonName>_<qualifier>_vectorize`. */
74
+ vectorize(qualifier?: string): VectorizeIndexLike;
75
+ /** Workers AI — shared `AI` binding. */
76
+ readonly ai: AiLike;
77
+ /** Cloudflare Images — shared `IMAGES` binding. */
78
+ readonly images: ImagesBindingLike;
79
+ /** Browser Rendering — shared `BROWSER` binding. */
80
+ readonly browser: FetcherLike;
81
+ /** Top-level fetch (with optional service-binding routing). */
82
+ fetch(input: string, init?: unknown): Promise<unknown>;
83
+ }
84
+
85
+ /**
86
+ * Categories that are scoped per-photon and use the auto-naming
87
+ * convention. Shared categories (ai, images, browser) live outside this
88
+ * list and resolve to fixed binding names.
89
+ */
90
+ export type ScopedBindingCategory = 'kv' | 'r2' | 'd1' | 'queue' | 'vectorize';
91
+
92
+ /**
93
+ * Build the env binding name for a scoped category on a given photon.
94
+ * Default (no qualifier): `<photon>_<category>` (e.g. `gallery_kv`).
95
+ * With qualifier: `<photon>_<qualifier>_<category>` (e.g. `gallery_cache_kv`).
96
+ *
97
+ * The helper is exported so deploy-side codegen (wrangler.toml emission)
98
+ * uses the same naming rule the runtime resolves with.
99
+ */
100
+ export function bindingNameFor(
101
+ photonName: string,
102
+ category: ScopedBindingCategory,
103
+ qualifier?: string,
104
+ ): string {
105
+ const safePhoton = photonName.toLowerCase().replace(/-/g, '_');
106
+ if (qualifier && qualifier.length > 0) {
107
+ const safeQualifier = qualifier.toLowerCase().replace(/-/g, '_');
108
+ return `${safePhoton}_${safeQualifier}_${category}`;
109
+ }
110
+ return `${safePhoton}_${category}`;
111
+ }
112
+
113
+ /** Fixed binding name for a shared category. */
114
+ export const SHARED_AI_BINDING = 'AI';
115
+ export const SHARED_IMAGES_BINDING = 'IMAGES';
116
+ export const SHARED_BROWSER_BINDING = 'BROWSER';
117
+
118
+ const CF_HINT =
119
+ 'This photon imports `Cloudflare` from "@portel/photon" but no CF ' +
120
+ 'runtime is attached. Run via `photon host run` (miniflare-backed) ' +
121
+ 'or deploy with `photon host deploy cloudflare`. Outside CF, this ' +
122
+ 'photon\'s CF-dependent methods cannot execute.';
123
+
124
+ /**
125
+ * Throwing-Proxy fallback. Used by the loader when a photon's
126
+ * constructor types a `Cloudflare` parameter but no CF runtime was
127
+ * provided (e.g. unit tests that didn't pass a stub, or local hosts
128
+ * without miniflare configured).
129
+ */
130
+ export function notConfiguredCloudflare(): Cloudflare {
131
+ const throwing = (label: string) => (..._args: unknown[]) => {
132
+ throw new Error(`Cloudflare.${label} called but ${CF_HINT}`);
133
+ };
134
+ const throwingProp = (label: string) =>
135
+ new Proxy(Object.create(null), {
136
+ get(_t, prop) {
137
+ if (prop === Symbol.toPrimitive || prop === 'toString') {
138
+ return () => `[unconfigured Cloudflare.${label}]`;
139
+ }
140
+ throw new Error(`Cloudflare.${label}.${String(prop)} accessed but ${CF_HINT}`);
141
+ },
142
+ });
143
+ return {
144
+ kv: throwing('kv') as Cloudflare['kv'],
145
+ r2: throwing('r2') as Cloudflare['r2'],
146
+ d1: throwing('d1') as Cloudflare['d1'],
147
+ queue: throwing('queue') as Cloudflare['queue'],
148
+ vectorize: throwing('vectorize') as Cloudflare['vectorize'],
149
+ ai: throwingProp('ai') as unknown as AiLike,
150
+ images: throwingProp('images') as unknown as ImagesBindingLike,
151
+ browser: throwingProp('browser') as unknown as FetcherLike,
152
+ fetch: throwing('fetch') as Cloudflare['fetch'],
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Build a `Cloudflare` surface backed directly by a Worker `env` (the
158
+ * deployed-CF case). Resolves auto-named bindings via the convention in
159
+ * `bindingNameFor`. Local hosts use the miniflare-backed factory in the
160
+ * photon repo's `runtime/cf-local.ts` instead.
161
+ *
162
+ * `env` is typed loosely so this module stays free of
163
+ * `@cloudflare/workers-types`. The deployed Worker template passes the
164
+ * real `env` straight through.
165
+ */
166
+ export function createCloudflareFromEnv(
167
+ env: Record<string, unknown>,
168
+ photonName: string,
169
+ ): Cloudflare {
170
+ const scoped = (category: ScopedBindingCategory) => (qualifier?: string) => {
171
+ const name = bindingNameFor(photonName, category, qualifier);
172
+ const binding = env[name];
173
+ if (binding === undefined) {
174
+ throw new Error(
175
+ `Cloudflare.${category}(${qualifier ? JSON.stringify(qualifier) : ''}) ` +
176
+ `requires binding "${name}" on the Worker env, but it is not defined. ` +
177
+ `Add it to wrangler.toml (or run \`photon host deploy cloudflare\` to ` +
178
+ `regenerate bindings from the photon's declarations).`,
179
+ );
180
+ }
181
+ return binding;
182
+ };
183
+ return {
184
+ kv: scoped('kv') as Cloudflare['kv'],
185
+ r2: scoped('r2') as Cloudflare['r2'],
186
+ d1: scoped('d1') as Cloudflare['d1'],
187
+ queue: scoped('queue') as Cloudflare['queue'],
188
+ vectorize: scoped('vectorize') as Cloudflare['vectorize'],
189
+ ai: (env[SHARED_AI_BINDING] ?? notConfiguredCloudflare().ai) as AiLike,
190
+ images: (env[SHARED_IMAGES_BINDING] ?? notConfiguredCloudflare().images) as ImagesBindingLike,
191
+ browser: (env[SHARED_BROWSER_BINDING] ?? notConfiguredCloudflare().browser) as FetcherLike,
192
+ fetch: (input: string, init?: unknown) => {
193
+ const fetcher = env['fetch'] as ((i: string, n?: unknown) => Promise<unknown>) | undefined;
194
+ if (typeof fetcher === 'function') return fetcher(input, init);
195
+ return fetch(input, init as RequestInit | undefined) as Promise<unknown>;
196
+ },
197
+ };
198
+ }
@@ -330,9 +330,28 @@ export class Collection<T> extends ReactiveArray<T> {
330
330
  /**
331
331
  * Attach a rendering hint for auto-UI.
332
332
  * Returns `this` for chaining at the end of a query.
333
+ *
334
+ * Pins `toJSON` as an own property here. Bun's `JSON.stringify`
335
+ * resolves the array branch before consulting the prototype's
336
+ * `toJSON`, so a prototype-only definition is silently bypassed
337
+ * for Array subclasses (Node 22 calls it correctly; Bun 1.2 does
338
+ * not). Installing the bound function as an own enumerable: false
339
+ * property makes both engines route through our serialization.
340
+ * Without `.as()` the photon shouldn't produce the metadata
341
+ * envelope at all, so the prototype-level `toJSON` (returns plain
342
+ * array) is fine — and JSON.stringify's array path produces the
343
+ * same plain array anyway, so the Bun divergence is invisible
344
+ * for that case.
333
345
  */
334
346
  as(format: RenderFormat, options?: Record<string, unknown>): this {
335
347
  this._renderHint = { format, options };
348
+ if (!Object.prototype.hasOwnProperty.call(this, 'toJSON')) {
349
+ Object.defineProperty(this, 'toJSON', {
350
+ value: this.toJSON.bind(this),
351
+ enumerable: false,
352
+ configurable: true,
353
+ });
354
+ }
336
355
  return this;
337
356
  }
338
357
 
package/src/env.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * CloudflareEnv — raw Cloudflare Worker `env`, surfaced as an explicit
3
+ * constructor injection so the import line itself signals that the
4
+ * importing photon depends on a Cloudflare deploy target.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import type { CloudflareEnv } from "@portel/photon";
9
+ *
10
+ * interface MyBindings {
11
+ * WEATHER_KV: KVNamespace;
12
+ * PHOTOS: R2Bucket;
13
+ * }
14
+ *
15
+ * export class Weather {
16
+ * constructor(private env: CloudflareEnv<MyBindings>) {}
17
+ * async forecast() {
18
+ * await this.env.WEATHER_KV.put("k", "v");
19
+ * }
20
+ * }
21
+ * ```
22
+ *
23
+ * The default loose type (`Record<string, unknown>`) keeps the no-types
24
+ * path working. Authors who want strict binding shapes pass their own
25
+ * type parameter; future codegen from `protected cfBindings` can fill
26
+ * this in automatically.
27
+ */
28
+ export type CloudflareEnv<T = Record<string, unknown>> = T;
package/src/index.ts CHANGED
@@ -154,6 +154,33 @@ export {
154
154
  // Core base class with lifecycle hooks
155
155
  export { Photon, Photon as PhotonMCP } from './base.js';
156
156
 
157
+ // Cloudflare deploy-target injections (constructor-injected, not on Photon)
158
+ export {
159
+ type Cloudflare,
160
+ type ScopedBindingCategory,
161
+ bindingNameFor,
162
+ notConfiguredCloudflare,
163
+ createCloudflareFromEnv,
164
+ SHARED_AI_BINDING,
165
+ SHARED_IMAGES_BINDING,
166
+ SHARED_BROWSER_BINDING,
167
+ } from './cloudflare.js';
168
+ export { type CloudflareEnv } from './env.js';
169
+ // Structural CF binding types — published so hosts can type their own
170
+ // CF adapters against the same shape `Cloudflare` exposes (e.g. unit
171
+ // tests that mock `cf.kv()` returns).
172
+ export {
173
+ type R2BucketLike,
174
+ type KVNamespaceLike,
175
+ type D1DatabaseLike,
176
+ type D1PreparedStatementLike,
177
+ type QueueLike,
178
+ type VectorizeIndexLike,
179
+ type AiLike,
180
+ type ImagesBindingLike,
181
+ type FetcherLike,
182
+ } from './cf.js';
183
+
157
184
  // Mixin for capability injection without requiring inheritance
158
185
  export { withPhotonCapabilities } from './mixins.js';
159
186
 
package/src/mixins.ts CHANGED
@@ -84,6 +84,18 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
84
84
  */
85
85
  private _schedule?: ScheduleProvider;
86
86
 
87
+ /**
88
+ * Cloudflare Worker env when deployed; undefined on local hosts.
89
+ * See Photon.env for the full contract.
90
+ */
91
+ readonly env?: Record<string, unknown>;
92
+
93
+ /**
94
+ * True when the active /mcp request passed the PHOTON_MCP_BEARER
95
+ * gate. See Photon.mcpAuthed for the full contract.
96
+ */
97
+ readonly mcpAuthed?: boolean;
98
+
87
99
  /**
88
100
  * Cross-photon call handler - injected by runtime
89
101
  * @internal