@stratal/feature-flags 0.0.21 → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,118 @@
1
+ # @stratal/feature-flags
2
+
3
+ ## 0.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - a753e55: Release alongside the rest of the packages; nothing changed in this one.
8
+
9
+ Every Stratal package is versioned as one fixed group, so `@stratal/feature-flags` is republished at the same version as the packages it builds on rather than being left behind. Its peer ranges are open-ended, so an existing install keeps resolving — upgrade only to keep one aligned set of versions across the framework.
10
+
11
+ - Updated dependencies [a753e55]
12
+ - Updated dependencies [a753e55]
13
+ - stratal@0.1.0
14
+ - @stratal/inertia@0.1.0
15
+
16
+ ## 0.0.27
17
+
18
+ ### Patch Changes
19
+
20
+ - 41a9140: Make feature flag evaluation resilient to runtime failures
21
+
22
+ Flag evaluation now returns the fallback value and logs a warning if a lookup throws — for example when a remote-binding dev tunnel drops — rather than failing the request. Detail methods return an `ERROR` reason with the fallback in these cases.
23
+
24
+ - Updated dependencies [41a9140]
25
+ - stratal@0.0.27
26
+ - @stratal/inertia@0.0.27
27
+
28
+ ## 0.0.26
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies [ab95f52]
33
+ - Updated dependencies [ab95f52]
34
+ - Updated dependencies [bb6d3b9]
35
+ - stratal@0.0.26
36
+ - @stratal/inertia@0.0.26
37
+
38
+ ## 0.0.25
39
+
40
+ ### Patch Changes
41
+
42
+ - Updated dependencies [e93db60]
43
+ - Updated dependencies [e93db60]
44
+ - stratal@0.0.25
45
+ - @stratal/inertia@0.0.25
46
+
47
+ ## 0.0.24
48
+
49
+ ### Patch Changes
50
+
51
+ - Updated dependencies [10cf223]
52
+ - @stratal/inertia@0.0.24
53
+ - stratal@0.0.24
54
+
55
+ ## 0.0.23
56
+
57
+ ### Patch Changes
58
+
59
+ - 13b0e8d: Add `@stratal/feature-flags` — Cloudflare Flagship feature flags via the native Worker binding API.
60
+ - `FeatureFlagModule.forRoot({ apps: [{ binding, flags }], default, context })` with a declare-once flag manifest, manifest defaults, a per-request evaluation-context resolver, and multi-app support via `FeatureFlagService.use(binding)`.
61
+ - `FeatureFlagShareMiddleware` shares evaluated flags to Inertia pages as the `featureFlags` prop; register it yourself (scoped to page controllers via `router.middleware(...)` or app-wide via `router.use(...)`) so a stalled Flagship binding can't block unrelated routes. Typed `useFlag` / `useFeatureFlags` hooks on `@stratal/feature-flags/react`. No runtime dependency on `@stratal/inertia`.
62
+ - `@stratal/inertia`: expose a generic `ctx.share(key, value)` macro on `RouterContext` so middleware and packages can contribute per-request shared props.
63
+ - `@stratal/framework`: add a `ctx.user()` macro on `RouterContext` (shorthand for `AuthContext.requireUser()`).
64
+
65
+ - 13b0e8d: Fix correctness and security issues found in review.
66
+
67
+ Queue:
68
+ - Retry the correct binding: dispatch stamps the producer binding into message metadata and failed jobs record it, so `queue:retry` re-enqueues through the Cloudflare binding instead of the queue name (which is not a valid binding key and broke retry whenever the two differed). A message with no binding metadata is logged and acked rather than stored as an unretryable job.
69
+ - Honor the documented retry budget: `maxRetries` now counts retries correctly against Cloudflare's 1-based `message.attempts` (previously gave one fewer retry than configured).
70
+ - Derive idempotency keys from an order-stable serialization of `type` + `payload`, so payloads that differ only in key order dedupe correctly.
71
+ - `queue:retry --all` / `queue:purge --all --queue` collect matching keys before deleting, so cursor pagination no longer skips jobs; `queue:failed --queue --limit` now counts matching jobs rather than scanned keys.
72
+ - Documented that delivery is at-least-once with best-effort de-duplication (not exactly-once), since the processed marker is written only after a handler succeeds and KV is eventually consistent — handlers must be idempotent.
73
+
74
+ Email (SMTP):
75
+ - Upgrade STARTTLS onto the socket `startTls()` returns: the original socket is closed by the runtime, so the post-upgrade reader/writer are re-derived from the new secure socket and any pre-handshake bytes are discarded (fixes a broken `smtp://` STARTTLS path on real Workers and closes the STARTTLS plaintext-injection vector).
76
+ - Refuse to send credentials over an unencrypted connection: an `smtp://` server that doesn't offer STARTTLS now fails loudly instead of leaking the password (blocks STARTTLS-stripping downgrades). Credential-free connections (e.g. local Mailpit) are unaffected.
77
+ - AUTH is gated on the server's advertised mechanisms and supports both `PLAIN` and `LOGIN`; usernames are percent-decoded like passwords.
78
+ - Add a response timeout so a hung SMTP server can't wedge the worker; QUIT/socket close are now best-effort and never mask a successful send.
79
+ - MIME builder strips CR/LF from headers, escapes/RFC 2231-encodes attachment filenames (prevents header injection), base64-encodes message bodies (fixes long-line corruption), and rejects envelope addresses containing whitespace or angle brackets (prevents `MAIL FROM`/`RCPT TO` desync).
80
+
81
+ Inertia SEO:
82
+ - `titleTemplate` substitutes every `%s` and treats `$`-sequences in the title literally.
83
+ - Inject head/body content via function replacements, so SEO/page content containing `$`-sequences (`$$`, `$&`, `` $` ``, `$'`) is no longer corrupted or able to splice a template placeholder back into the output.
84
+ - Drop unsafe attribute names — including inline event handlers (`on*`) — from custom `meta`/`link` entries (prevents tag breakout server-side, `setAttribute` errors during client head-sync, and developer-supplied event-handler attributes).
85
+
86
+ Feature flags:
87
+ - `FeatureFlagService.use()` binds the target app exactly once.
88
+
89
+ Database (framework):
90
+ - The reentrant `$transaction` proxy forwards the receiver for non-transaction property access.
91
+
92
+ Testing:
93
+ - `TestingModule.close()` drops the isolated per-file database even if shutdown throws; the stale-database sweep escapes LIKE metacharacters so a prefix containing `_` can't over-match.
94
+
95
+ DI:
96
+ - Construct singletons against the root container so they can never capture a request-scoped dependency (which would leak one request's state across every later request); an illegal singleton→request dependency now throws loudly.
97
+ - Detect circular dependencies and throw a clear error naming the cycle instead of overflowing the stack.
98
+ - `tryResolve` only swallows "no provider"; a registered provider that throws while constructing now surfaces the real error instead of injecting `undefined`.
99
+ - Request-cache invalidation tracks transitive constructor dependencies, so re-registering a value rebuilds cached services that depend on it through a transient intermediary.
100
+
101
+ Quarry dev runtime:
102
+ - Persist every durable plugin (KV, D1, R2, Durable Objects, cache) under `.wrangler/state/v3`, matching `wrangler dev` (previously only R2 was persisted); load `.env.local` / `.env.<env>.local` into `process.env` for full parity.
103
+ - The `cloudflare:sockets` STARTTLS shim re-attaches the stream error handler to the upgraded socket, so post-upgrade connection errors still surface.
104
+
105
+ - Updated dependencies [13b0e8d]
106
+ - Updated dependencies [13b0e8d]
107
+ - Updated dependencies [13b0e8d]
108
+ - Updated dependencies [13b0e8d]
109
+ - Updated dependencies [13b0e8d]
110
+ - Updated dependencies [13b0e8d]
111
+ - Updated dependencies [13b0e8d]
112
+ - Updated dependencies [13b0e8d]
113
+ - Updated dependencies [13b0e8d]
114
+ - Updated dependencies [13b0e8d]
115
+ - Updated dependencies [13b0e8d]
116
+ - Updated dependencies [be813bc]
117
+ - stratal@0.0.23
118
+ - @stratal/inertia@0.0.23
package/README.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # @stratal/feature-flags
2
2
 
3
- [Cloudflare Flagship](https://developers.cloudflare.com/flagship/) feature flags for the [Stratal](https://stratal.dev) framework, using the native Worker **binding API** — with zero-config [Inertia.js](https://inertiajs.com) auto-sharing and typed React hooks.
3
+ [Cloudflare Flagship](https://developers.cloudflare.com/flagship/) feature flags for the [Stratal](https://stratal.dev) framework, using the native Worker **binding API** — with opt-in [Inertia.js](https://inertiajs.com) sharing and typed React hooks.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@stratal/feature-flags)](https://www.npmjs.com/package/@stratal/feature-flags)
6
+ [![CI](https://github.com/strataljs/stratal/actions/workflows/ci.yml/badge.svg)](https://github.com/strataljs/stratal/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![npm downloads](https://img.shields.io/npm/dm/@stratal/feature-flags)](https://www.npmjs.com/package/@stratal/feature-flags)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
10
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/strataljs/stratal/pulls)
11
+ [![GitHub stars](https://img.shields.io/github/stars/strataljs/stratal?style=social)](https://github.com/strataljs/stratal)
4
12
 
5
13
  ## Install
6
14
 
@@ -31,7 +39,9 @@ import { FeatureFlagModule } from '@stratal/feature-flags'
31
39
  export class AppModule {}
32
40
  ```
33
41
 
34
- When `@stratal/inertia` is installed, declared flags are auto-shared to every Inertia page no extra wiring.
42
+ A Worker can bind to several Flagship apps — list each one under `apps`, and set `default` to pick the binding the injected `FeatureFlagService` uses. Flagship has no enumeration API, so the `flags` manifest declares what you intend to evaluate and share, and its values double as defaults.
43
+
44
+ To expose flags to an Inertia frontend, register `FeatureFlagShareMiddleware` where you want them — scoped to your page-rendering controllers (`router.middleware(...)`) or app-wide (`router.use(...)`).
35
45
 
36
46
  Evaluate on the server:
37
47
 
@@ -39,15 +49,41 @@ Evaluate on the server:
39
49
  const enabled = await this.flags.getBooleanValue('new-checkout') // uses manifest default
40
50
  ```
41
51
 
52
+ `getStringValue()`, `getNumberValue()` and `getObjectValue()` cover the other flag types, each with a `*Details()` variant that returns the full evaluation details. `use(binding)` switches to another bound app, and `all()` evaluates the whole manifest at once.
53
+
42
54
  Read on the client:
43
55
 
44
56
  ```tsx
45
- import { useFlag } from '@stratal/feature-flags/react'
57
+ import { useFeatureFlags, useFlag } from '@stratal/feature-flags/react'
46
58
 
47
59
  const showNewCheckout = useFlag('new-checkout')
60
+ const flags = useFeatureFlags()
48
61
  ```
49
62
 
50
- See the framework docs for the full API.
63
+ ## Typed flag keys
64
+
65
+ Augment `FeatureFlagRegistry` once and both the service and the hooks get typed keys:
66
+
67
+ ```ts
68
+ declare module '@stratal/feature-flags' {
69
+ interface FeatureFlagRegistry {
70
+ 'new-checkout': boolean
71
+ 'checkout-flow': string
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Documentation
77
+
78
+ See the framework docs at **[stratal.dev](https://stratal.dev)** for the full API.
79
+
80
+ ## Support the project
81
+
82
+ If Stratal is useful to you, **[star the repository](https://github.com/strataljs/stratal)** — it is the simplest way to help others find it.
83
+
84
+ ## Maintainer
85
+
86
+ Built and maintained by **Temitayo Fadojutimi** — [@adesege_](https://x.com/adesege_).
51
87
 
52
88
  ## License
53
89
 
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
- import { a as FlagValue, i as FlagManifest, n as FeatureFlagModuleOptions, o as FlagshipBindingName, r as FeatureFlagRegistry, t as FeatureFlagApp } from "./types-Dxuc-7SJ.mjs";
1
+ import { a as FlagValue, i as FlagManifest, n as FeatureFlagModuleOptions, o as FlagshipBindingName, r as FeatureFlagRegistry, t as FeatureFlagApp } from "./types-D-uV0zlC.mjs";
2
2
  import { AsyncModuleOptions, DynamicModule } from "stratal/module";
3
- import { Middleware, Next, RouteConfigurable, Router, RouterContext } from "stratal/router";
3
+ import { LoggerService } from "stratal/logger";
4
+ import { Middleware, Next, RouterContext } from "stratal/router";
4
5
  import { ApplicationError } from "stratal/errors";
5
6
  import { StratalEnv } from "stratal";
6
-
7
7
  //#region src/feature-flags.module.d.ts
8
8
  /**
9
9
  * Feature Flag Module
@@ -12,10 +12,10 @@ import { StratalEnv } from "stratal";
12
12
  * Declare your apps (and the flags you use) once; inject {@link FeatureFlagService}
13
13
  * to evaluate them.
14
14
  *
15
- * When `@stratal/inertia` is also present, declared flags are auto-shared to
16
- * every Inertia page as the `featureFlags` prop (read them with `useFlag` /
17
- * `useFeatureFlags` from `@stratal/feature-flags/react`) no extra wiring. In
18
- * a pure-API worker the auto-share middleware is a no-op.
15
+ * To expose flags to an Inertia frontend, register {@link FeatureFlagShareMiddleware}
16
+ * yourself scope it to the controllers that render pages (`router.middleware(...)`)
17
+ * or app-wide (`router.use(...)`) from a module's `configureRoutes`. It is not
18
+ * registered for you, so a stalled Flagship binding never blocks unrelated routes.
19
19
  *
20
20
  * @example
21
21
  * ```typescript
@@ -31,9 +31,7 @@ import { StratalEnv } from "stratal";
31
31
  * })
32
32
  * ```
33
33
  */
34
- declare class FeatureFlagModule implements RouteConfigurable {
35
- /** Auto-shares declared flags to every Inertia page (no-op without Inertia). */
36
- configureRoutes(router: Router): void;
34
+ export declare class FeatureFlagModule {
37
35
  /** Configure with static options. */
38
36
  static forRoot(options: FeatureFlagModuleOptions): DynamicModule;
39
37
  /** Configure with an async factory (when options depend on other services). */
@@ -53,8 +51,10 @@ declare class FeatureFlagModule implements RouteConfigurable {
53
51
  * evaluation (per-call context overrides it). Resolved from the current
54
52
  * request; skipped automatically outside request scope.
55
53
  *
56
- * Switch to another Flagship app with {@link use}. Evaluation never throws — the
57
- * binding returns the default value on error.
54
+ * Switch to another Flagship app with {@link use}. Evaluation never throws —
55
+ * the binding returns the default value on evaluation errors, and the service
56
+ * catches everything else (e.g. a dropped remote-binding tunnel in local dev)
57
+ * and returns the same fallback, logging a warning.
58
58
  *
59
59
  * @example
60
60
  * ```typescript
@@ -67,15 +67,16 @@ declare class FeatureFlagModule implements RouteConfigurable {
67
67
  *
68
68
  * @see https://developers.cloudflare.com/flagship/binding/
69
69
  */
70
- declare class FeatureFlagService {
70
+ export declare class FeatureFlagService {
71
71
  private readonly options;
72
72
  private readonly env;
73
73
  private readonly routerContext;
74
+ private readonly logger;
74
75
  private readonly apps;
75
76
  private bindingName;
76
77
  private binding;
77
78
  private manifest;
78
- constructor(options: FeatureFlagModuleOptions, env: StratalEnv, routerContext: RouterContext | null);
79
+ constructor(options: FeatureFlagModuleOptions, env: StratalEnv, routerContext: RouterContext | undefined, logger: LoggerService | undefined, initialBinding?: string);
79
80
  /**
80
81
  * Switch to a different configured Flagship app.
81
82
  *
@@ -106,7 +107,7 @@ declare class FeatureFlagService {
106
107
  /**
107
108
  * Evaluates every flag declared in the current app's manifest and returns a
108
109
  * `{ key: value }` map. The evaluation method is chosen from each declared
109
- * default's type. Powers the Inertia auto-share.
110
+ * default's type. Powers `FeatureFlagShareMiddleware`.
110
111
  */
111
112
  all(context?: FlagshipEvaluationContext): Promise<Record<string, FlagValue>>;
112
113
  private bindTo;
@@ -116,24 +117,40 @@ declare class FeatureFlagService {
116
117
  private fallback;
117
118
  /** Evaluates a single flag, choosing the method from the declared default's type. */
118
119
  private evaluate;
120
+ /**
121
+ * Runs an evaluation and absorbs any failure into the fallback. The binding
122
+ * already returns the default on evaluation errors, but the call itself can
123
+ * still reject — e.g. when a `remote: true` binding's dev-proxy WebSocket
124
+ * tunnel drops. A flag lookup must never take the request down with it.
125
+ */
126
+ private safe;
127
+ /** Synthesizes the details shape the binding would return for a failed evaluation. */
128
+ private errorDetails;
129
+ /** Extracts a human-readable message from an unknown thrown value. */
130
+ private message;
119
131
  }
120
132
  //#endregion
121
133
  //#region src/feature-flag-share.middleware.d.ts
122
- declare module 'stratal/router' {
123
- interface RouterContext {
124
- share(key: string, value: unknown): void;
125
- }
126
- }
127
134
  /**
128
135
  * Evaluates the declared flag manifest for the default app and shares it as the
129
136
  * `featureFlags` prop on every Inertia page rendered during the request.
130
137
  *
131
138
  * Only runs on `GET` requests — page renders (full visits and partial reloads)
132
139
  * are always `GET`, so mutating API calls don't trigger evaluation. No-ops when
133
- * Inertia is not installed (`ctx.share` absent), so `FeatureFlagModule` is safe
134
- * in pure-API workers. Registered by `FeatureFlagModule`.
140
+ * Inertia is not installed (`ctx.share` absent).
141
+ *
142
+ * Register it yourself, scoped to where flags are actually needed — a Flagship
143
+ * stall then only affects those routes, not the whole app:
144
+ *
145
+ * ```typescript
146
+ * configureRoutes(router: Router): void {
147
+ * // only the controllers that render flag-aware pages
148
+ * router.group([DashboardController], (r) => r.middleware(FeatureFlagShareMiddleware))
149
+ * // ...or app-wide: router.use(FeatureFlagShareMiddleware)
150
+ * }
151
+ * ```
135
152
  */
136
- declare class FeatureFlagShareMiddleware implements Middleware {
153
+ export declare class FeatureFlagShareMiddleware implements Middleware {
137
154
  private readonly flags;
138
155
  constructor(flags: FeatureFlagService);
139
156
  handle(ctx: RouterContext, next: Next): Promise<void>;
@@ -146,8 +163,10 @@ declare class FeatureFlagShareMiddleware implements Middleware {
146
163
  * Use `Symbol.for(...)` so the tokens resolve to the same symbol across module
147
164
  * boundaries (the global symbol registry).
148
165
  */
149
- declare const FEATURE_FLAG_TOKENS: {
150
- /** The resolved {@link FeatureFlagModuleOptions}. */readonly Options: symbol; /** The request-scoped {@link FeatureFlagService} bound to the default app. */
166
+ export declare const FEATURE_FLAG_TOKENS: {
167
+ /** The resolved {@link FeatureFlagModuleOptions}. */
168
+ readonly Options: symbol;
169
+ /** The request-scoped {@link FeatureFlagService} bound to the default app. */
151
170
  readonly FeatureFlagService: symbol;
152
171
  };
153
172
  type FeatureFlagToken = (typeof FEATURE_FLAG_TOKENS)[keyof typeof FEATURE_FLAG_TOKENS];
@@ -160,7 +179,7 @@ type FeatureFlagToken = (typeof FEATURE_FLAG_TOKENS)[keyof typeof FEATURE_FLAG_T
160
179
  * Note: flag *evaluation* never throws; the binding returns the supplied
161
180
  * default value on error.
162
181
  */
163
- declare class FeatureFlagError extends ApplicationError {}
182
+ export declare class FeatureFlagError extends ApplicationError {}
164
183
  //#endregion
165
- export { FEATURE_FLAG_TOKENS, type FeatureFlagApp, FeatureFlagError, FeatureFlagModule, type FeatureFlagModuleOptions, type FeatureFlagRegistry, FeatureFlagService, FeatureFlagShareMiddleware, type FeatureFlagToken, type FlagManifest, type FlagValue, type FlagshipBindingName };
184
+ export type { FeatureFlagApp, FeatureFlagModuleOptions, FeatureFlagRegistry, FeatureFlagToken, FlagManifest, FlagValue, FlagshipBindingName };
166
185
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/feature-flags.module.ts","../src/services/feature-flag.service.ts","../src/feature-flag-share.middleware.ts","../src/feature-flags.tokens.ts","../src/feature-flags.error.ts"],"mappings":";;;;;;;;;;;AAkCA;;;;;;;;;;;;;;;;;;;;;;cAKa,iBAAA,YAA6B,iBAAA;EAiBpB;EAfpB,eAAA,CAAgB,MAAA,EAAQ,MAAA;EAeiE;EAAA,OAVlF,OAAA,CAAQ,OAAA,EAAS,wBAAA,GAA2B,aAAA;;SAU5C,YAAA,CAAa,OAAA,EAAS,kBAAA,CAAmB,wBAAA,IAA4B,aAAA;AAAA;;;;;;;AAtB9E;;;;;;;;;;;;;;;;;;;;;;cCMa,kBAAA;EAAA,iBAO6C,OAAA;EAAA,iBACJ,GAAA;EAAA,iBACI,aAAA;EAAA,iBARvC,IAAA;EAAA,QACT,WAAA;EAAA,QACA,OAAA;EAAA,QACA,QAAA;cAGgD,OAAA,EAAS,wBAAA,EACb,GAAA,EAAK,UAAA,EACD,aAAA,EAAe,aAAA;EAT1C;;;;;;EAuB7B,GAAA,CAAI,OAAA,EAAS,mBAAA,GAAsB,kBAAA;EAe0B;EAAA,IAPzD,GAAA,CAAA;EAYqE;EALnE,GAAA,CAAI,OAAA,UAAiB,YAAA,YAAwB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAUlB;EALjE,eAAA,CAAgB,OAAA,UAAiB,YAAA,YAAwB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAU9B;EALjE,cAAA,CAAe,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAU5B;EALjE,cAAA,CAAe,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAKqB;EAAlH,cAAA,kBAAA,CAAiC,OAAA,UAAiB,YAAA,GAAe,CAAA,EAAG,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,CAAA;EAK7C;EAArE,iBAAA,CAAkB,OAAA,UAAiB,YAAA,YAAwB,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;EAAR;EAKjG,gBAAA,CAAiB,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;EAAA;EAKvG,gBAAA,CAAiB,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;EAApC;EAKnE,gBAAA,kBAAA,CAAmC,OAAA,UAAiB,YAAA,GAAe,CAAA,EAAG,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA,CAA0B,CAAA;EAL/C;;;;;EAc/F,GAAA,CAAI,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,MAAA,SAAe,SAAA;EAAA,QAa/D,MAAA;EAb+D;EAAA,QA+BzD,OAAA;EA/BkC;EAAA,QAsCxC,QAAA;EAtC+C;EAAA,QA6C/C,QAAA;AAAA;;;;YCnKE,aAAA;IACR,KAAA,CAAM,GAAA,UAAa,KAAA;EAAA;AAAA;AFwBvB;;;;;;;;;AAAA,cEVa,0BAAA,YAAsC,UAAA;EAAA,iBAEkB,KAAA;cAAA,KAAA,EAAO,kBAAA;EAGpE,MAAA,CAAO,GAAA,EAAK,aAAA,EAAe,IAAA,EAAM,IAAA,GAAO,OAAA;AAAA;;;;;;;;;cCvBnC,mBAAA;EHiCA,8DG5BH,OAAA;WAAA,kBAAA;AAAA;AAAA,KAEE,gBAAA,WAA2B,mBAAA,eAAkC,mBAAA;;;;;;;;;AHqBzE;cIzBa,gBAAA,SAAyB,gBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/feature-flags.module.ts","../src/services/feature-flag.service.ts","../src/feature-flag-share.middleware.ts","../src/feature-flags.tokens.ts","../src/feature-flags.error.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAqCa;;SAEJ,QAAQ,SAAS,2BAA2B;;SAU5C,aAAa,SAAS,mBAAmB,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCNjE;mBAO6C;mBACJ;mBAC0B;mBACA;mBAT7D;UACT;UACA;UACA;EAER,YACwD,SAAS,0BACb,KAAK,YACqB,eAAe,2BACf,QAAQ,2BAGpF;;;;;;;EAcF,IAAI,SAAS,sBAAsB;;MAM/B;;EAOE,IAAI,iBAAiB,wBAAwB,UAAU,4BAA4B;;EAMnF,gBAAgB,iBAAiB,wBAAwB,UAAU,4BAA4B;;EAM/F,eAAe,iBAAiB,uBAAuB,UAAU,4BAA4B;;EAM7F,eAAe,iBAAiB,uBAAuB,UAAU,4BAA4B;;EAM7F,eAAe,kBAAkB,iBAAiB,eAAe,GAAG,UAAU,4BAA4B,QAAQ;;EAMlH,kBAAkB,iBAAiB,wBAAwB,UAAU,4BAA4B,QAAQ;;EAMzG,iBAAiB,iBAAiB,uBAAuB,UAAU,4BAA4B,QAAQ;;EAMvG,iBAAiB,iBAAiB,uBAAuB,UAAU,4BAA4B,QAAQ;;EAMvG,iBAAiB,kBAAkB,iBAAiB,eAAe,GAAG,UAAU,4BAA4B,QAAQ,0BAA0B;;;;;;EAU9I,IAAI,UAAU,4BAA4B,QAAQ,eAAe;UAsB/D;;UAkBM;;UAON;;UAOA;;;;;;;UAmBM;;UAYN;;UAKA;;;;;;;;;;;;;;;;;;;;;;;qBC5MG,sCAAsC;mBAEkB;EADnE,YACmE,OAAO;EAGpE,OAAO,KAAK,eAAe,MAAM,OAAO;;;;;;;;;;qBC1BnC;;WAEX;;WAEA;;KAGU,2BAA2B,kCAAkC;;;;;;;;;;qBCJ5D,yBAAyB"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Module } from "stratal/module";
2
- import { DI_TOKENS, Request, Transient, inject } from "stratal/di";
2
+ import { DI_TOKENS, Transient, inject } from "stratal/di";
3
+ import { LOGGER_TOKENS } from "stratal/logger";
3
4
  import { ROUTER_TOKENS } from "stratal/router";
4
5
  import { ApplicationError } from "stratal/errors";
5
6
  //#region src/feature-flags.tokens.ts
@@ -16,19 +17,29 @@ const FEATURE_FLAG_TOKENS = {
16
17
  FeatureFlagService: Symbol.for("stratal:feature-flags:service")
17
18
  };
18
19
  //#endregion
19
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorateMetadata.js
20
+ //#region src/feature-flags.error.ts
21
+ /**
22
+ * Thrown for feature-flag misconfiguration — an unknown app or a Flagship
23
+ * binding that is not present on the Worker environment.
24
+ *
25
+ * Note: flag *evaluation* never throws; the binding returns the supplied
26
+ * default value on error.
27
+ */
28
+ var FeatureFlagError = class extends ApplicationError {};
29
+ //#endregion
30
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateMetadata.js
20
31
  function __decorateMetadata(k, v) {
21
32
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
33
  }
23
34
  //#endregion
24
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorateParam.js
35
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
25
36
  function __decorateParam(paramIndex, decorator) {
26
37
  return function(target, key) {
27
38
  decorator(target, key, paramIndex);
28
39
  };
29
40
  }
30
41
  //#endregion
31
- //#region \0@oxc-project+runtime@0.129.0/helpers/decorate.js
42
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
32
43
  function __decorate(decorators, target, key, desc) {
33
44
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
34
45
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -36,49 +47,24 @@ function __decorate(decorators, target, key, desc) {
36
47
  return c > 3 && r && Object.defineProperty(target, key, r), r;
37
48
  }
38
49
  //#endregion
39
- //#region src/feature-flag-share.middleware.ts
40
- let FeatureFlagShareMiddleware = class FeatureFlagShareMiddleware {
41
- flags;
42
- constructor(flags) {
43
- this.flags = flags;
44
- }
45
- async handle(ctx, next) {
46
- if (ctx.c.req.method === "GET" && typeof ctx.share === "function") ctx.share("featureFlags", await this.flags.all());
47
- await next();
48
- }
49
- };
50
- FeatureFlagShareMiddleware = __decorate([
51
- Transient(),
52
- __decorateParam(0, inject(FEATURE_FLAG_TOKENS.FeatureFlagService)),
53
- __decorateMetadata("design:paramtypes", [Object])
54
- ], FeatureFlagShareMiddleware);
55
- //#endregion
56
- //#region src/feature-flags.error.ts
57
- /**
58
- * Thrown for feature-flag misconfiguration — an unknown app or a Flagship
59
- * binding that is not present on the Worker environment.
60
- *
61
- * Note: flag *evaluation* never throws; the binding returns the supplied
62
- * default value on error.
63
- */
64
- var FeatureFlagError = class extends ApplicationError {};
65
- //#endregion
66
50
  //#region src/services/feature-flag.service.ts
67
51
  var _FeatureFlagService;
68
52
  let FeatureFlagService = _FeatureFlagService = class FeatureFlagService {
69
53
  options;
70
54
  env;
71
55
  routerContext;
56
+ logger;
72
57
  apps = /* @__PURE__ */ new Map();
73
58
  bindingName;
74
59
  binding;
75
60
  manifest;
76
- constructor(options, env, routerContext) {
61
+ constructor(options, env, routerContext, logger, initialBinding) {
77
62
  this.options = options;
78
63
  this.env = env;
79
64
  this.routerContext = routerContext;
65
+ this.logger = logger;
80
66
  for (const app of options.apps) this.apps.set(app.binding, app);
81
- this.bindTo(options.default ?? options.apps[0]?.binding);
67
+ this.bindTo(initialBinding ?? options.default ?? options.apps[0]?.binding);
82
68
  }
83
69
  /**
84
70
  * Switch to a different configured Flagship app.
@@ -88,9 +74,7 @@ let FeatureFlagService = _FeatureFlagService = class FeatureFlagService {
88
74
  */
89
75
  use(binding) {
90
76
  if (binding === this.bindingName) return this;
91
- const instance = new _FeatureFlagService(this.options, this.env, this.routerContext);
92
- instance.bindTo(binding);
93
- return instance;
77
+ return new _FeatureFlagService(this.options, this.env, this.routerContext, this.logger, binding);
94
78
  }
95
79
  /** The binding name this instance currently targets. */
96
80
  get app() {
@@ -98,48 +82,63 @@ let FeatureFlagService = _FeatureFlagService = class FeatureFlagService {
98
82
  }
99
83
  /** Returns the raw flag value without type checking. */
100
84
  async get(flagKey, defaultValue, context) {
101
- return this.binding.get(flagKey, this.fallback(flagKey, defaultValue), await this.context(context));
85
+ const fallback = this.fallback(flagKey, defaultValue);
86
+ return this.safe(flagKey, async () => this.binding.get(flagKey, fallback, await this.context(context)), () => fallback);
102
87
  }
103
88
  /** Returns the flag value as a `boolean`. */
104
89
  async getBooleanValue(flagKey, defaultValue, context) {
105
- return this.binding.getBooleanValue(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context));
90
+ const fallback = this.fallback(flagKey, defaultValue, false);
91
+ return this.safe(flagKey, async () => this.binding.getBooleanValue(flagKey, fallback, await this.context(context)), () => fallback);
106
92
  }
107
93
  /** Returns the flag value as a `string`. */
108
94
  async getStringValue(flagKey, defaultValue, context) {
109
- return this.binding.getStringValue(flagKey, this.fallback(flagKey, defaultValue, ""), await this.context(context));
95
+ const fallback = this.fallback(flagKey, defaultValue, "");
96
+ return this.safe(flagKey, async () => this.binding.getStringValue(flagKey, fallback, await this.context(context)), () => fallback);
110
97
  }
111
98
  /** Returns the flag value as a `number`. */
112
99
  async getNumberValue(flagKey, defaultValue, context) {
113
- return this.binding.getNumberValue(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context));
100
+ const fallback = this.fallback(flagKey, defaultValue, 0);
101
+ return this.safe(flagKey, async () => this.binding.getNumberValue(flagKey, fallback, await this.context(context)), () => fallback);
114
102
  }
115
103
  /** Returns the flag value as a typed object. */
116
104
  async getObjectValue(flagKey, defaultValue, context) {
117
- return this.binding.getObjectValue(flagKey, this.fallback(flagKey, defaultValue, {}), await this.context(context));
105
+ const fallback = this.fallback(flagKey, defaultValue, {});
106
+ return this.safe(flagKey, async () => this.binding.getObjectValue(flagKey, fallback, await this.context(context)), () => fallback);
118
107
  }
119
108
  /** Returns the `boolean` flag value with evaluation metadata. */
120
109
  async getBooleanDetails(flagKey, defaultValue, context) {
121
- return this.binding.getBooleanDetails(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context));
110
+ const fallback = this.fallback(flagKey, defaultValue, false);
111
+ return this.safe(flagKey, async () => this.binding.getBooleanDetails(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error));
122
112
  }
123
113
  /** Returns the `string` flag value with evaluation metadata. */
124
114
  async getStringDetails(flagKey, defaultValue, context) {
125
- return this.binding.getStringDetails(flagKey, this.fallback(flagKey, defaultValue, ""), await this.context(context));
115
+ const fallback = this.fallback(flagKey, defaultValue, "");
116
+ return this.safe(flagKey, async () => this.binding.getStringDetails(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error));
126
117
  }
127
118
  /** Returns the `number` flag value with evaluation metadata. */
128
119
  async getNumberDetails(flagKey, defaultValue, context) {
129
- return this.binding.getNumberDetails(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context));
120
+ const fallback = this.fallback(flagKey, defaultValue, 0);
121
+ return this.safe(flagKey, async () => this.binding.getNumberDetails(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error));
130
122
  }
131
123
  /** Returns the typed object flag value with evaluation metadata. */
132
124
  async getObjectDetails(flagKey, defaultValue, context) {
133
- return this.binding.getObjectDetails(flagKey, this.fallback(flagKey, defaultValue, {}), await this.context(context));
125
+ const fallback = this.fallback(flagKey, defaultValue, {});
126
+ return this.safe(flagKey, async () => this.binding.getObjectDetails(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error));
134
127
  }
135
128
  /**
136
129
  * Evaluates every flag declared in the current app's manifest and returns a
137
130
  * `{ key: value }` map. The evaluation method is chosen from each declared
138
- * default's type. Powers the Inertia auto-share.
131
+ * default's type. Powers `FeatureFlagShareMiddleware`.
139
132
  */
140
133
  async all(context) {
141
- const merged = await this.context(context);
142
134
  const keys = Object.keys(this.manifest);
135
+ let merged;
136
+ try {
137
+ merged = await this.context(context);
138
+ } catch (error) {
139
+ this.logger?.warn(`Feature flag context resolution failed on app "${this.bindingName}"; returning manifest defaults.`, { error: this.message(error) });
140
+ return { ...this.manifest };
141
+ }
143
142
  const values = await Promise.all(keys.map((key) => this.evaluate(key, this.manifest[key], merged)));
144
143
  const result = {};
145
144
  keys.forEach((key, i) => {
@@ -175,32 +174,58 @@ let FeatureFlagService = _FeatureFlagService = class FeatureFlagService {
175
174
  /** Evaluates a single flag, choosing the method from the declared default's type. */
176
175
  evaluate(flagKey, declared, context) {
177
176
  switch (typeof declared) {
178
- case "boolean": return this.binding.getBooleanValue(flagKey, declared, context);
179
- case "number": return this.binding.getNumberValue(flagKey, declared, context);
180
- case "string": return this.binding.getStringValue(flagKey, declared, context);
181
- default: return this.binding.getObjectValue(flagKey, declared, context);
177
+ case "boolean": return this.safe(flagKey, () => this.binding.getBooleanValue(flagKey, declared, context), () => declared);
178
+ case "number": return this.safe(flagKey, () => this.binding.getNumberValue(flagKey, declared, context), () => declared);
179
+ case "string": return this.safe(flagKey, () => this.binding.getStringValue(flagKey, declared, context), () => declared);
180
+ default: return this.safe(flagKey, () => this.binding.getObjectValue(flagKey, declared, context), () => declared);
181
+ }
182
+ }
183
+ /**
184
+ * Runs an evaluation and absorbs any failure into the fallback. The binding
185
+ * already returns the default on evaluation errors, but the call itself can
186
+ * still reject — e.g. when a `remote: true` binding's dev-proxy WebSocket
187
+ * tunnel drops. A flag lookup must never take the request down with it.
188
+ */
189
+ async safe(flagKey, evaluate, onError) {
190
+ try {
191
+ return await evaluate();
192
+ } catch (error) {
193
+ this.logger?.warn(`Feature flag evaluation failed for "${flagKey}" on app "${this.bindingName}"; returning the fallback value.`, { error: this.message(error) });
194
+ return onError(error);
182
195
  }
183
196
  }
197
+ /** Synthesizes the details shape the binding would return for a failed evaluation. */
198
+ errorDetails(flagKey, value, error) {
199
+ return {
200
+ flagKey,
201
+ value,
202
+ reason: "ERROR",
203
+ errorMessage: this.message(error)
204
+ };
205
+ }
206
+ /** Extracts a human-readable message from an unknown thrown value. */
207
+ message(error) {
208
+ return error instanceof Error ? error.message : String(error);
209
+ }
184
210
  };
185
211
  FeatureFlagService = _FeatureFlagService = __decorate([
186
- Request(FEATURE_FLAG_TOKENS.FeatureFlagService),
212
+ Transient(FEATURE_FLAG_TOKENS.FeatureFlagService),
187
213
  __decorateParam(0, inject(FEATURE_FLAG_TOKENS.Options)),
188
214
  __decorateParam(1, inject(DI_TOKENS.CloudflareEnv)),
189
- __decorateParam(2, inject(ROUTER_TOKENS.RouterContext)),
215
+ __decorateParam(2, inject(ROUTER_TOKENS.RouterContext, { isOptional: true })),
216
+ __decorateParam(3, inject(LOGGER_TOKENS.LoggerService, { isOptional: true })),
190
217
  __decorateMetadata("design:paramtypes", [
191
218
  Object,
192
219
  Object,
193
- Object
220
+ Object,
221
+ Object,
222
+ String
194
223
  ])
195
224
  ], FeatureFlagService);
196
225
  //#endregion
197
226
  //#region src/feature-flags.module.ts
198
227
  var _FeatureFlagModule;
199
228
  let FeatureFlagModule = _FeatureFlagModule = class FeatureFlagModule {
200
- /** Auto-shares declared flags to every Inertia page (no-op without Inertia). */
201
- configureRoutes(router) {
202
- router.use(FeatureFlagShareMiddleware);
203
- }
204
229
  /** Configure with static options. */
205
230
  static forRoot(options) {
206
231
  return {
@@ -228,6 +253,23 @@ FeatureFlagModule = _FeatureFlagModule = __decorate([Module({ providers: [{
228
253
  useClass: FeatureFlagService
229
254
  }] })], FeatureFlagModule);
230
255
  //#endregion
256
+ //#region src/feature-flag-share.middleware.ts
257
+ let FeatureFlagShareMiddleware = class FeatureFlagShareMiddleware {
258
+ flags;
259
+ constructor(flags) {
260
+ this.flags = flags;
261
+ }
262
+ async handle(ctx, next) {
263
+ if (ctx.c.req.method === "GET" && typeof ctx.share === "function") ctx.share("featureFlags", await this.flags.all());
264
+ await next();
265
+ }
266
+ };
267
+ FeatureFlagShareMiddleware = __decorate([
268
+ Transient(),
269
+ __decorateParam(0, inject(FEATURE_FLAG_TOKENS.FeatureFlagService)),
270
+ __decorateMetadata("design:paramtypes", [Object])
271
+ ], FeatureFlagShareMiddleware);
272
+ //#endregion
231
273
  export { FEATURE_FLAG_TOKENS, FeatureFlagError, FeatureFlagModule, FeatureFlagService, FeatureFlagShareMiddleware };
232
274
 
233
275
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/feature-flags.tokens.ts","../src/feature-flag-share.middleware.ts","../src/feature-flags.error.ts","../src/services/feature-flag.service.ts","../src/feature-flags.module.ts"],"sourcesContent":["/**\n * DI tokens for the feature-flags module.\n *\n * Use `Symbol.for(...)` so the tokens resolve to the same symbol across module\n * boundaries (the global symbol registry).\n */\nexport const FEATURE_FLAG_TOKENS = {\n /** The resolved {@link FeatureFlagModuleOptions}. */\n Options: Symbol.for('stratal:feature-flags:options'),\n /** The request-scoped {@link FeatureFlagService} bound to the default app. */\n FeatureFlagService: Symbol.for('stratal:feature-flags:service'),\n} as const\n\nexport type FeatureFlagToken = (typeof FEATURE_FLAG_TOKENS)[keyof typeof FEATURE_FLAG_TOKENS]\n","import { Transient, inject } from 'stratal/di'\nimport type { Middleware, Next, RouterContext } from 'stratal/router'\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens'\nimport type { FeatureFlagService } from './services/feature-flag.service'\n\n// `ctx.share` is contributed at runtime by `@stratal/inertia` (an optional peer).\n// Declared here so this package types the call without importing Inertia; the\n// signature matches Inertia's, so the declarations merge when both are present.\ndeclare module 'stratal/router' {\n interface RouterContext {\n share(key: string, value: unknown): void\n }\n}\n\n/**\n * Evaluates the declared flag manifest for the default app and shares it as the\n * `featureFlags` prop on every Inertia page rendered during the request.\n *\n * Only runs on `GET` requests — page renders (full visits and partial reloads)\n * are always `GET`, so mutating API calls don't trigger evaluation. No-ops when\n * Inertia is not installed (`ctx.share` absent), so `FeatureFlagModule` is safe\n * in pure-API workers. Registered by `FeatureFlagModule`.\n */\n@Transient()\nexport class FeatureFlagShareMiddleware implements Middleware {\n constructor(\n @inject(FEATURE_FLAG_TOKENS.FeatureFlagService) private readonly flags: FeatureFlagService,\n ) {}\n\n async handle(ctx: RouterContext, next: Next): Promise<void> {\n if (ctx.c.req.method === 'GET' && typeof ctx.share === 'function') {\n ctx.share('featureFlags', await this.flags.all())\n }\n await next()\n }\n}\n","import { ApplicationError } from 'stratal/errors'\n\n/**\n * Thrown for feature-flag misconfiguration — an unknown app or a Flagship\n * binding that is not present on the Worker environment.\n *\n * Note: flag *evaluation* never throws; the binding returns the supplied\n * default value on error.\n */\nexport class FeatureFlagError extends ApplicationError {}\n","import type { StratalEnv } from 'stratal'\nimport { DI_TOKENS, Request, inject } from 'stratal/di'\nimport { ROUTER_TOKENS, type RouterContext } from 'stratal/router'\nimport { FeatureFlagError } from '../feature-flags.error'\nimport { FEATURE_FLAG_TOKENS } from '../feature-flags.tokens'\nimport type {\n FeatureFlagApp,\n FeatureFlagModuleOptions,\n FlagManifest,\n FlagValue,\n FlagshipBindingName,\n} from '../types'\n\n/**\n * Feature Flag Service\n *\n * Type-safe wrapper around a Cloudflare Flagship binding (`env.FLAGS`). Mirrors\n * the binding's evaluation methods 1:1, with two ergonomic additions:\n *\n * - **Manifest defaults** — when you omit a default, the value declared in the\n * app's `flags` manifest is used (an explicit argument always wins).\n * - **Default context** — the module's `context` resolver is merged into every\n * evaluation (per-call context overrides it). Resolved from the current\n * request; skipped automatically outside request scope.\n *\n * Switch to another Flagship app with {@link use}. Evaluation never throws — the\n * binding returns the default value on error.\n *\n * @example\n * ```typescript\n * @inject(FEATURE_FLAG_TOKENS.FeatureFlagService)\n * private readonly flags: FeatureFlagService\n *\n * const enabled = await this.flags.getBooleanValue('new-checkout') // manifest default\n * const layout = await this.flags.use('EXPERIMENT_FLAGS').getStringValue('layout', 'v1')\n * ```\n *\n * @see https://developers.cloudflare.com/flagship/binding/\n */\n@Request(FEATURE_FLAG_TOKENS.FeatureFlagService)\nexport class FeatureFlagService {\n private readonly apps = new Map<string, FeatureFlagApp>()\n private bindingName!: string\n private binding!: Flagship\n private manifest!: FlagManifest\n\n constructor(\n @inject(FEATURE_FLAG_TOKENS.Options) private readonly options: FeatureFlagModuleOptions,\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv,\n @inject(ROUTER_TOKENS.RouterContext) private readonly routerContext: RouterContext | null,\n ) {\n for (const app of options.apps) {\n this.apps.set(app.binding, app)\n }\n this.bindTo(options.default ?? options.apps[0]?.binding)\n }\n\n /**\n * Switch to a different configured Flagship app.\n *\n * Returns a new immutable instance bound to `binding`; the original is\n * unchanged. The binding must be declared in the module's `apps`.\n */\n use(binding: FlagshipBindingName): FeatureFlagService {\n if (binding === this.bindingName) return this\n const instance = new FeatureFlagService(this.options, this.env, this.routerContext)\n instance.bindTo(binding)\n return instance\n }\n\n /** The binding name this instance currently targets. */\n get app(): string {\n return this.bindingName\n }\n\n // ==================== EVALUATION ====================\n\n /** Returns the raw flag value without type checking. */\n async get(flagKey: string, defaultValue?: unknown, context?: FlagshipEvaluationContext): Promise<unknown> {\n return this.binding.get(flagKey, this.fallback(flagKey, defaultValue), await this.context(context))\n }\n\n /** Returns the flag value as a `boolean`. */\n async getBooleanValue(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<boolean> {\n return this.binding.getBooleanValue(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context))\n }\n\n /** Returns the flag value as a `string`. */\n async getStringValue(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<string> {\n return this.binding.getStringValue(flagKey, this.fallback(flagKey, defaultValue, ''), await this.context(context))\n }\n\n /** Returns the flag value as a `number`. */\n async getNumberValue(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<number> {\n return this.binding.getNumberValue(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context))\n }\n\n /** Returns the flag value as a typed object. */\n async getObjectValue<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<T> {\n return this.binding.getObjectValue<T>(flagKey, this.fallback(flagKey, defaultValue, {} as T), await this.context(context))\n }\n\n /** Returns the `boolean` flag value with evaluation metadata. */\n async getBooleanDetails(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<boolean>> {\n return this.binding.getBooleanDetails(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context))\n }\n\n /** Returns the `string` flag value with evaluation metadata. */\n async getStringDetails(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<string>> {\n return this.binding.getStringDetails(flagKey, this.fallback(flagKey, defaultValue, ''), await this.context(context))\n }\n\n /** Returns the `number` flag value with evaluation metadata. */\n async getNumberDetails(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<number>> {\n return this.binding.getNumberDetails(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context))\n }\n\n /** Returns the typed object flag value with evaluation metadata. */\n async getObjectDetails<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<T>> {\n return this.binding.getObjectDetails<T>(flagKey, this.fallback(flagKey, defaultValue, {} as T), await this.context(context))\n }\n\n /**\n * Evaluates every flag declared in the current app's manifest and returns a\n * `{ key: value }` map. The evaluation method is chosen from each declared\n * default's type. Powers the Inertia auto-share.\n */\n async all(context?: FlagshipEvaluationContext): Promise<Record<string, FlagValue>> {\n const merged = await this.context(context)\n const keys = Object.keys(this.manifest)\n const values = await Promise.all(keys.map((key) => this.evaluate(key, this.manifest[key], merged)))\n const result: Record<string, FlagValue> = {}\n keys.forEach((key, i) => {\n result[key] = values[i]\n })\n return result\n }\n\n // ==================== INTERNAL ====================\n\n private bindTo(name: string | undefined): void {\n if (!name) {\n throw new FeatureFlagError('No feature flag apps configured. Provide at least one app in FeatureFlagModule.forRoot({ apps: [...] }).')\n }\n const app = this.apps.get(name)\n if (!app) {\n throw new FeatureFlagError(`Feature flag app \"${name}\" is not configured.`)\n }\n const binding = (this.env as unknown as Record<string, unknown>)[name] as Flagship | undefined\n if (!binding) {\n throw new FeatureFlagError(`Flagship binding \"${name}\" was not found in the environment.`)\n }\n this.bindingName = name\n this.binding = binding\n this.manifest = app.flags ?? {}\n }\n\n /** Resolves the merged evaluation context (default context + per-call override). */\n private async context(callContext?: FlagshipEvaluationContext): Promise<FlagshipEvaluationContext | undefined> {\n if (!this.options.context || !this.routerContext) return callContext\n const base = await this.options.context(this.routerContext)\n return callContext ? { ...base, ...callContext } : base\n }\n\n /** Picks the default: explicit arg, then manifest, then the type's zero value. */\n private fallback<T>(flagKey: string, provided: T | undefined, zero?: T): T {\n if (provided !== undefined) return provided\n if (flagKey in this.manifest) return this.manifest[flagKey] as T\n return zero as T\n }\n\n /** Evaluates a single flag, choosing the method from the declared default's type. */\n private evaluate(flagKey: string, declared: FlagValue, context?: FlagshipEvaluationContext): Promise<FlagValue> {\n switch (typeof declared) {\n case 'boolean':\n return this.binding.getBooleanValue(flagKey, declared, context)\n case 'number':\n return this.binding.getNumberValue(flagKey, declared, context)\n case 'string':\n return this.binding.getStringValue(flagKey, declared, context)\n default:\n return this.binding.getObjectValue(flagKey, declared, context)\n }\n }\n}\n","import { Module } from 'stratal/module'\nimport type { AsyncModuleOptions, DynamicModule } from 'stratal/module'\nimport type { RouteConfigurable, Router } from 'stratal/router'\nimport { FeatureFlagShareMiddleware } from './feature-flag-share.middleware'\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens'\nimport { FeatureFlagService } from './services/feature-flag.service'\nimport type { FeatureFlagModuleOptions } from './types'\n\n/**\n * Feature Flag Module\n *\n * Evaluates Cloudflare Flagship feature flags through the native Worker binding.\n * Declare your apps (and the flags you use) once; inject {@link FeatureFlagService}\n * to evaluate them.\n *\n * When `@stratal/inertia` is also present, declared flags are auto-shared to\n * every Inertia page as the `featureFlags` prop (read them with `useFlag` /\n * `useFeatureFlags` from `@stratal/feature-flags/react`) — no extra wiring. In\n * a pure-API worker the auto-share middleware is a no-op.\n *\n * @example\n * ```typescript\n * FeatureFlagModule.forRoot({\n * apps: [{ binding: 'FLAGS', flags: { 'new-checkout': false } }],\n * context: (ctx) => ({ userId: ctx.user().id }), // ctx.user() from @stratal/framework\n * })\n *\n * // Or async, from config namespaces:\n * FeatureFlagModule.forRootAsync({\n * inject: [flagsConfig.KEY],\n * useFactory: (cfg) => ({ apps: cfg.apps, default: cfg.default }),\n * })\n * ```\n */\n@Module({\n providers: [\n { provide: FEATURE_FLAG_TOKENS.FeatureFlagService, useClass: FeatureFlagService },\n ],\n})\nexport class FeatureFlagModule implements RouteConfigurable {\n /** Auto-shares declared flags to every Inertia page (no-op without Inertia). */\n configureRoutes(router: Router): void {\n router.use(FeatureFlagShareMiddleware)\n }\n\n /** Configure with static options. */\n static forRoot(options: FeatureFlagModuleOptions): DynamicModule {\n return {\n module: FeatureFlagModule,\n providers: [\n { provide: FEATURE_FLAG_TOKENS.Options, useValue: options },\n ],\n }\n }\n\n /** Configure with an async factory (when options depend on other services). */\n static forRootAsync(options: AsyncModuleOptions<FeatureFlagModuleOptions>): DynamicModule {\n return {\n module: FeatureFlagModule,\n providers: [\n {\n provide: FEATURE_FLAG_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/feature-flags.tokens.ts","../src/feature-flags.error.ts","../src/services/feature-flag.service.ts","../src/feature-flags.module.ts","../src/feature-flag-share.middleware.ts"],"sourcesContent":["/**\n * DI tokens for the feature-flags module.\n *\n * Use `Symbol.for(...)` so the tokens resolve to the same symbol across module\n * boundaries (the global symbol registry).\n */\nexport const FEATURE_FLAG_TOKENS = {\n /** The resolved {@link FeatureFlagModuleOptions}. */\n Options: Symbol.for('stratal:feature-flags:options'),\n /** The request-scoped {@link FeatureFlagService} bound to the default app. */\n FeatureFlagService: Symbol.for('stratal:feature-flags:service'),\n} as const\n\nexport type FeatureFlagToken = (typeof FEATURE_FLAG_TOKENS)[keyof typeof FEATURE_FLAG_TOKENS]\n","import { ApplicationError } from 'stratal/errors'\n\n/**\n * Thrown for feature-flag misconfiguration — an unknown app or a Flagship\n * binding that is not present on the Worker environment.\n *\n * Note: flag *evaluation* never throws; the binding returns the supplied\n * default value on error.\n */\nexport class FeatureFlagError extends ApplicationError {}\n","import type { StratalEnv } from 'stratal'\nimport { DI_TOKENS, Transient, inject } from 'stratal/di'\nimport { LOGGER_TOKENS, type LoggerService } from 'stratal/logger'\nimport { ROUTER_TOKENS, type RouterContext } from 'stratal/router'\nimport { FeatureFlagError } from '../feature-flags.error'\nimport { FEATURE_FLAG_TOKENS } from '../feature-flags.tokens'\nimport type {\n FeatureFlagApp,\n FeatureFlagModuleOptions,\n FlagManifest,\n FlagValue,\n FlagshipBindingName,\n} from '../types'\n\n/**\n * Feature Flag Service\n *\n * Type-safe wrapper around a Cloudflare Flagship binding (`env.FLAGS`). Mirrors\n * the binding's evaluation methods 1:1, with two ergonomic additions:\n *\n * - **Manifest defaults** — when you omit a default, the value declared in the\n * app's `flags` manifest is used (an explicit argument always wins).\n * - **Default context** — the module's `context` resolver is merged into every\n * evaluation (per-call context overrides it). Resolved from the current\n * request; skipped automatically outside request scope.\n *\n * Switch to another Flagship app with {@link use}. Evaluation never throws —\n * the binding returns the default value on evaluation errors, and the service\n * catches everything else (e.g. a dropped remote-binding tunnel in local dev)\n * and returns the same fallback, logging a warning.\n *\n * @example\n * ```typescript\n * @inject(FEATURE_FLAG_TOKENS.FeatureFlagService)\n * private readonly flags: FeatureFlagService\n *\n * const enabled = await this.flags.getBooleanValue('new-checkout') // manifest default\n * const layout = await this.flags.use('EXPERIMENT_FLAGS').getStringValue('layout', 'v1')\n * ```\n *\n * @see https://developers.cloudflare.com/flagship/binding/\n */\n@Transient(FEATURE_FLAG_TOKENS.FeatureFlagService)\nexport class FeatureFlagService {\n private readonly apps = new Map<string, FeatureFlagApp>()\n private bindingName!: string\n private binding!: Flagship\n private manifest!: FlagManifest\n\n constructor(\n @inject(FEATURE_FLAG_TOKENS.Options) private readonly options: FeatureFlagModuleOptions,\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv,\n @inject(ROUTER_TOKENS.RouterContext, { isOptional: true }) private readonly routerContext: RouterContext | undefined,\n @inject(LOGGER_TOKENS.LoggerService, { isOptional: true }) private readonly logger: LoggerService | undefined,\n // Only passed by `use()`; DI never injects it. Lets `use()` bind exactly once\n // instead of binding to the default in the constructor and re-binding after.\n initialBinding?: string,\n ) {\n for (const app of options.apps) {\n this.apps.set(app.binding, app)\n }\n this.bindTo(initialBinding ?? options.default ?? options.apps[0]?.binding)\n }\n\n /**\n * Switch to a different configured Flagship app.\n *\n * Returns a new immutable instance bound to `binding`; the original is\n * unchanged. The binding must be declared in the module's `apps`.\n */\n use(binding: FlagshipBindingName): FeatureFlagService {\n if (binding === this.bindingName) return this\n return new FeatureFlagService(this.options, this.env, this.routerContext, this.logger, binding)\n }\n\n /** The binding name this instance currently targets. */\n get app(): string {\n return this.bindingName\n }\n\n // ==================== EVALUATION ====================\n\n /** Returns the raw flag value without type checking. */\n async get(flagKey: string, defaultValue?: unknown, context?: FlagshipEvaluationContext): Promise<unknown> {\n const fallback = this.fallback(flagKey, defaultValue)\n return this.safe(flagKey, async () => this.binding.get(flagKey, fallback, await this.context(context)), () => fallback)\n }\n\n /** Returns the flag value as a `boolean`. */\n async getBooleanValue(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<boolean> {\n const fallback = this.fallback(flagKey, defaultValue, false)\n return this.safe(flagKey, async () => this.binding.getBooleanValue(flagKey, fallback, await this.context(context)), () => fallback)\n }\n\n /** Returns the flag value as a `string`. */\n async getStringValue(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<string> {\n const fallback = this.fallback(flagKey, defaultValue, '')\n return this.safe(flagKey, async () => this.binding.getStringValue(flagKey, fallback, await this.context(context)), () => fallback)\n }\n\n /** Returns the flag value as a `number`. */\n async getNumberValue(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<number> {\n const fallback = this.fallback(flagKey, defaultValue, 0)\n return this.safe(flagKey, async () => this.binding.getNumberValue(flagKey, fallback, await this.context(context)), () => fallback)\n }\n\n /** Returns the flag value as a typed object. */\n async getObjectValue<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<T> {\n const fallback = this.fallback(flagKey, defaultValue, {} as T)\n return this.safe(flagKey, async () => this.binding.getObjectValue<T>(flagKey, fallback, await this.context(context)), () => fallback)\n }\n\n /** Returns the `boolean` flag value with evaluation metadata. */\n async getBooleanDetails(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<boolean>> {\n const fallback = this.fallback(flagKey, defaultValue, false)\n return this.safe(flagKey, async () => this.binding.getBooleanDetails(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error))\n }\n\n /** Returns the `string` flag value with evaluation metadata. */\n async getStringDetails(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<string>> {\n const fallback = this.fallback(flagKey, defaultValue, '')\n return this.safe(flagKey, async () => this.binding.getStringDetails(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error))\n }\n\n /** Returns the `number` flag value with evaluation metadata. */\n async getNumberDetails(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<number>> {\n const fallback = this.fallback(flagKey, defaultValue, 0)\n return this.safe(flagKey, async () => this.binding.getNumberDetails(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error))\n }\n\n /** Returns the typed object flag value with evaluation metadata. */\n async getObjectDetails<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<T>> {\n const fallback = this.fallback(flagKey, defaultValue, {} as T)\n return this.safe(flagKey, async () => this.binding.getObjectDetails<T>(flagKey, fallback, await this.context(context)), (error) => this.errorDetails(flagKey, fallback, error))\n }\n\n /**\n * Evaluates every flag declared in the current app's manifest and returns a\n * `{ key: value }` map. The evaluation method is chosen from each declared\n * default's type. Powers `FeatureFlagShareMiddleware`.\n */\n async all(context?: FlagshipEvaluationContext): Promise<Record<string, FlagValue>> {\n const keys = Object.keys(this.manifest)\n // Resolve the shared context once. A throwing resolver must not take the\n // batch down — fall back to the manifest defaults, the same values each\n // per-flag method returns when evaluation can't proceed.\n let merged: FlagshipEvaluationContext | undefined\n try {\n merged = await this.context(context)\n } catch (error) {\n this.logger?.warn(`Feature flag context resolution failed on app \"${this.bindingName}\"; returning manifest defaults.`, { error: this.message(error) })\n return { ...this.manifest }\n }\n const values = await Promise.all(keys.map((key) => this.evaluate(key, this.manifest[key], merged)))\n const result: Record<string, FlagValue> = {}\n keys.forEach((key, i) => {\n result[key] = values[i]\n })\n return result\n }\n\n // ==================== INTERNAL ====================\n\n private bindTo(name: string | undefined): void {\n if (!name) {\n throw new FeatureFlagError('No feature flag apps configured. Provide at least one app in FeatureFlagModule.forRoot({ apps: [...] }).')\n }\n const app = this.apps.get(name)\n if (!app) {\n throw new FeatureFlagError(`Feature flag app \"${name}\" is not configured.`)\n }\n const binding = (this.env as unknown as Record<string, unknown>)[name] as Flagship | undefined\n if (!binding) {\n throw new FeatureFlagError(`Flagship binding \"${name}\" was not found in the environment.`)\n }\n this.bindingName = name\n this.binding = binding\n this.manifest = app.flags ?? {}\n }\n\n /** Resolves the merged evaluation context (default context + per-call override). */\n private async context(callContext?: FlagshipEvaluationContext): Promise<FlagshipEvaluationContext | undefined> {\n if (!this.options.context || !this.routerContext) return callContext\n const base = await this.options.context(this.routerContext)\n return callContext ? { ...base, ...callContext } : base\n }\n\n /** Picks the default: explicit arg, then manifest, then the type's zero value. */\n private fallback<T>(flagKey: string, provided: T | undefined, zero?: T): T {\n if (provided !== undefined) return provided\n if (flagKey in this.manifest) return this.manifest[flagKey] as T\n return zero as T\n }\n\n /** Evaluates a single flag, choosing the method from the declared default's type. */\n private evaluate(flagKey: string, declared: FlagValue, context?: FlagshipEvaluationContext): Promise<FlagValue> {\n switch (typeof declared) {\n case 'boolean':\n return this.safe(flagKey, () => this.binding.getBooleanValue(flagKey, declared, context), () => declared)\n case 'number':\n return this.safe(flagKey, () => this.binding.getNumberValue(flagKey, declared, context), () => declared)\n case 'string':\n return this.safe(flagKey, () => this.binding.getStringValue(flagKey, declared, context), () => declared)\n default:\n return this.safe(flagKey, () => this.binding.getObjectValue(flagKey, declared, context), () => declared)\n }\n }\n\n /**\n * Runs an evaluation and absorbs any failure into the fallback. The binding\n * already returns the default on evaluation errors, but the call itself can\n * still reject — e.g. when a `remote: true` binding's dev-proxy WebSocket\n * tunnel drops. A flag lookup must never take the request down with it.\n */\n private async safe<T>(flagKey: string, evaluate: () => Promise<T>, onError: (error: unknown) => T): Promise<T> {\n try {\n return await evaluate()\n } catch (error) {\n this.logger?.warn(`Feature flag evaluation failed for \"${flagKey}\" on app \"${this.bindingName}\"; returning the fallback value.`, {\n error: this.message(error),\n })\n return onError(error)\n }\n }\n\n /** Synthesizes the details shape the binding would return for a failed evaluation. */\n private errorDetails<T>(flagKey: string, value: T, error: unknown): FlagshipEvaluationDetails<T> {\n return { flagKey, value, reason: 'ERROR', errorMessage: this.message(error) }\n }\n\n /** Extracts a human-readable message from an unknown thrown value. */\n private message(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n }\n}\n","import { Module } from 'stratal/module'\nimport type { AsyncModuleOptions, DynamicModule } from 'stratal/module'\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens'\nimport { FeatureFlagService } from './services/feature-flag.service'\nimport type { FeatureFlagModuleOptions } from './types'\n\n/**\n * Feature Flag Module\n *\n * Evaluates Cloudflare Flagship feature flags through the native Worker binding.\n * Declare your apps (and the flags you use) once; inject {@link FeatureFlagService}\n * to evaluate them.\n *\n * To expose flags to an Inertia frontend, register {@link FeatureFlagShareMiddleware}\n * yourself — scope it to the controllers that render pages (`router.middleware(...)`)\n * or app-wide (`router.use(...)`) from a module's `configureRoutes`. It is not\n * registered for you, so a stalled Flagship binding never blocks unrelated routes.\n *\n * @example\n * ```typescript\n * FeatureFlagModule.forRoot({\n * apps: [{ binding: 'FLAGS', flags: { 'new-checkout': false } }],\n * context: (ctx) => ({ userId: ctx.user().id }), // ctx.user() from @stratal/framework\n * })\n *\n * // Or async, from config namespaces:\n * FeatureFlagModule.forRootAsync({\n * inject: [flagsConfig.KEY],\n * useFactory: (cfg) => ({ apps: cfg.apps, default: cfg.default }),\n * })\n * ```\n */\n@Module({\n providers: [\n { provide: FEATURE_FLAG_TOKENS.FeatureFlagService, useClass: FeatureFlagService },\n ],\n})\nexport class FeatureFlagModule {\n /** Configure with static options. */\n static forRoot(options: FeatureFlagModuleOptions): DynamicModule {\n return {\n module: FeatureFlagModule,\n providers: [\n { provide: FEATURE_FLAG_TOKENS.Options, useValue: options },\n ],\n }\n }\n\n /** Configure with an async factory (when options depend on other services). */\n static forRootAsync(options: AsyncModuleOptions<FeatureFlagModuleOptions>): DynamicModule {\n return {\n module: FeatureFlagModule,\n providers: [\n {\n provide: FEATURE_FLAG_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n}\n","/// <reference types=\"@stratal/inertia\" />\n\nimport { Transient, inject } from 'stratal/di';\nimport type { Middleware, Next, RouterContext } from 'stratal/router';\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens';\nimport type { FeatureFlagService } from './services/feature-flag.service';\n\n/**\n * Evaluates the declared flag manifest for the default app and shares it as the\n * `featureFlags` prop on every Inertia page rendered during the request.\n *\n * Only runs on `GET` requests — page renders (full visits and partial reloads)\n * are always `GET`, so mutating API calls don't trigger evaluation. No-ops when\n * Inertia is not installed (`ctx.share` absent).\n *\n * Register it yourself, scoped to where flags are actually needed — a Flagship\n * stall then only affects those routes, not the whole app:\n *\n * ```typescript\n * configureRoutes(router: Router): void {\n * // only the controllers that render flag-aware pages\n * router.group([DashboardController], (r) => r.middleware(FeatureFlagShareMiddleware))\n * // ...or app-wide: router.use(FeatureFlagShareMiddleware)\n * }\n * ```\n */\n@Transient()\nexport class FeatureFlagShareMiddleware implements Middleware {\n constructor(\n @inject(FEATURE_FLAG_TOKENS.FeatureFlagService) private readonly flags: FeatureFlagService,\n ) {}\n\n async handle(ctx: RouterContext, next: Next): Promise<void> {\n if (ctx.c.req.method === 'GET' && typeof ctx.share === 'function') {\n ctx.share('featureFlags', await this.flags.all())\n }\n await next()\n }\n}\n"],"mappings":"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"}
package/dist/react.d.mts CHANGED
@@ -1,10 +1,9 @@
1
- import { r as FeatureFlagRegistry } from "./types-Dxuc-7SJ.mjs";
2
-
1
+ import { r as FeatureFlagRegistry } from "./types-D-uV0zlC.mjs";
3
2
  //#region src/react/use-feature-flags.d.ts
4
3
  /**
5
- * Returns the full map of feature flags shared by `FeatureFlagInertiaModule`.
4
+ * Returns the full map of feature flags shared by `FeatureFlagShareMiddleware`.
6
5
  */
7
- declare function useFeatureFlags(): Record<string, unknown>;
6
+ export declare function useFeatureFlags(): Record<string, unknown>;
8
7
  /**
9
8
  * Returns a single shared feature flag value.
10
9
  *
@@ -17,8 +16,7 @@ declare function useFeatureFlags(): Record<string, unknown>;
17
16
  * const layout = useFlag('layout', 'v1') // loose fallback
18
17
  * ```
19
18
  */
20
- declare function useFlag<K extends keyof FeatureFlagRegistry>(key: K): FeatureFlagRegistry[K];
21
- declare function useFlag<T>(key: string, defaultValue: T): T;
19
+ export declare function useFlag<K extends keyof FeatureFlagRegistry>(key: K): FeatureFlagRegistry[K];
20
+ export declare function useFlag<T>(key: string, defaultValue: T): T;
22
21
  //#endregion
23
- export { useFeatureFlags, useFlag };
24
22
  //# sourceMappingURL=react.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/use-feature-flags.ts"],"mappings":";;;;;AAWA;iBAAgB,eAAA,CAAA,GAAmB,MAAA;;;;AAgBnC;;;;;;;;;iBAAgB,OAAA,iBAAwB,mBAAA,CAAA,CAAqB,GAAA,EAAK,CAAA,GAAI,mBAAA,CAAoB,CAAA;AAAA,iBAC1E,OAAA,GAAA,CAAW,GAAA,UAAa,YAAA,EAAc,CAAA,GAAI,CAAA"}
1
+ {"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/use-feature-flags.ts"],"mappings":";;;;;wBAWgB,mBAAmB;;;;;;;;;;;;;wBAgBnB,QAAQ,gBAAgB,qBAAqB,KAAK,IAAI,oBAAoB;wBAC1E,QAAQ,GAAG,aAAa,cAAc,IAAI"}
package/dist/react.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { usePage } from "@inertiajs/react";
2
2
  //#region src/react/use-feature-flags.ts
3
3
  /**
4
- * Returns the full map of feature flags shared by `FeatureFlagInertiaModule`.
4
+ * Returns the full map of feature flags shared by `FeatureFlagShareMiddleware`.
5
5
  */
6
6
  function useFeatureFlags() {
7
7
  return usePage().props.featureFlags ?? {};
@@ -1 +1 @@
1
- {"version":3,"file":"react.mjs","names":[],"sources":["../src/react/use-feature-flags.ts"],"sourcesContent":["import type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport type { FeatureFlagRegistry } from '../types'\n\ninterface FeatureFlagsPageProps extends PageProps {\n featureFlags?: Record<string, unknown>\n}\n\n/**\n * Returns the full map of feature flags shared by `FeatureFlagInertiaModule`.\n */\nexport function useFeatureFlags(): Record<string, unknown> {\n return usePage<FeatureFlagsPageProps>().props.featureFlags ?? {}\n}\n\n/**\n * Returns a single shared feature flag value.\n *\n * When you augment {@link FeatureFlagRegistry}, the key and return type are\n * checked against your declared flags. Otherwise pass an explicit default.\n *\n * @example\n * ```tsx\n * const showNewCheckout = useFlag('new-checkout') // typed via FeatureFlagRegistry\n * const layout = useFlag('layout', 'v1') // loose fallback\n * ```\n */\nexport function useFlag<K extends keyof FeatureFlagRegistry>(key: K): FeatureFlagRegistry[K]\nexport function useFlag<T>(key: string, defaultValue: T): T\nexport function useFlag(key: string, defaultValue?: unknown): unknown {\n const flags = useFeatureFlags()\n return key in flags ? flags[key] : defaultValue\n}\n"],"mappings":";;;;;AAWA,SAAgB,kBAA2C;CACzD,OAAO,SAAgC,CAAC,MAAM,gBAAgB,EAAE;;AAiBlE,SAAgB,QAAQ,KAAa,cAAiC;CACpE,MAAM,QAAQ,iBAAiB;CAC/B,OAAO,OAAO,QAAQ,MAAM,OAAO"}
1
+ {"version":3,"file":"react.mjs","names":[],"sources":["../src/react/use-feature-flags.ts"],"sourcesContent":["import type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport type { FeatureFlagRegistry } from '../types'\n\ninterface FeatureFlagsPageProps extends PageProps {\n featureFlags?: Record<string, unknown>\n}\n\n/**\n * Returns the full map of feature flags shared by `FeatureFlagShareMiddleware`.\n */\nexport function useFeatureFlags(): Record<string, unknown> {\n return usePage<FeatureFlagsPageProps>().props.featureFlags ?? {}\n}\n\n/**\n * Returns a single shared feature flag value.\n *\n * When you augment {@link FeatureFlagRegistry}, the key and return type are\n * checked against your declared flags. Otherwise pass an explicit default.\n *\n * @example\n * ```tsx\n * const showNewCheckout = useFlag('new-checkout') // typed via FeatureFlagRegistry\n * const layout = useFlag('layout', 'v1') // loose fallback\n * ```\n */\nexport function useFlag<K extends keyof FeatureFlagRegistry>(key: K): FeatureFlagRegistry[K]\nexport function useFlag<T>(key: string, defaultValue: T): T\nexport function useFlag(key: string, defaultValue?: unknown): unknown {\n const flags = useFeatureFlags()\n return key in flags ? flags[key] : defaultValue\n}\n"],"mappings":";;;;;AAWA,SAAgB,kBAA2C;CACzD,OAAO,QAA+B,CAAC,CAAC,MAAM,gBAAgB,CAAC;AACjE;AAgBA,SAAgB,QAAQ,KAAa,cAAiC;CACpE,MAAM,QAAQ,gBAAgB;CAC9B,OAAO,OAAO,QAAQ,MAAM,OAAO;AACrC"}
@@ -1,6 +1,5 @@
1
1
  import { RouterContext } from "stratal/router";
2
2
  import { StratalEnv } from "stratal";
3
-
4
3
  //#region src/types.d.ts
5
4
  /**
6
5
  * A value a feature flag can resolve to.
@@ -11,7 +10,7 @@ type FlagValue = boolean | string | number | object;
11
10
  /**
12
11
  * String keys of the augmented `StratalEnv` whose value is a Flagship binding.
13
12
  */
14
- type FlagshipBindingFromEnv = Extract<{ [K in keyof StratalEnv]: StratalEnv[K] extends Flagship ? K : never }[keyof StratalEnv], string>;
13
+ type FlagshipBindingFromEnv = Extract<{ [K in keyof StratalEnv]: StratalEnv[K] extends Flagship ? K : never; }[keyof StratalEnv], string>;
15
14
  /**
16
15
  * Type-safe Flagship binding name.
17
16
  *
@@ -38,7 +37,7 @@ interface FeatureFlagRegistry {}
38
37
  * A declared set of flags and their default values.
39
38
  *
40
39
  * Flagship has no enumeration API, so the flags you intend to evaluate (and
41
- * auto-share to the frontend) must be declared once here. The default also
40
+ * share to the frontend) must be declared once here. The default also
42
41
  * doubles as the type hint used to pick the evaluation method in `all()`.
43
42
  */
44
43
  type FlagManifest = Record<string, FlagValue>;
@@ -48,7 +47,7 @@ type FlagManifest = Record<string, FlagValue>;
48
47
  interface FeatureFlagApp {
49
48
  /** Flagship binding name from your Wrangler config (type-checked against `StratalEnv`). */
50
49
  binding: FlagshipBindingName;
51
- /** Declared flags + defaults for this app. Used for manifest defaults and Inertia auto-share. */
50
+ /** Declared flags + defaults for this app. Used for manifest defaults and Inertia sharing. */
52
51
  flags?: FlagManifest;
53
52
  }
54
53
  /**
@@ -68,4 +67,4 @@ interface FeatureFlagModuleOptions {
68
67
  }
69
68
  //#endregion
70
69
  export { FlagValue as a, FlagManifest as i, FeatureFlagModuleOptions as n, FlagshipBindingName as o, FeatureFlagRegistry as r, FeatureFlagApp as t };
71
- //# sourceMappingURL=types-Dxuc-7SJ.d.mts.map
70
+ //# sourceMappingURL=types-D-uV0zlC.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types-D-uV0zlC.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;KAQY;;;;KAKP,yBAAyB,WACzB,WAAW,aAAa,WAAW,WAAW,WAAW,mBAAkB;;;;;;;;KAWpE,uBAAuB,mDAE/B;;;;;;;;;;;;;;UAea;;;;;;;;KASL,eAAe,eAAe;;;;UAKzB;;EAEf,SAAS;;EAET,QAAQ;;;;;UAMO;;EAEf,MAAM;;EAEN,UAAU;;;;;;EAMV,WAAW,KAAK,kBAAkB,4BAA4B,QAAQ"}
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@stratal/feature-flags",
3
- "version": "0.0.21",
4
- "description": "Cloudflare Flagship feature flags for the Stratal framework — binding API wrapper with Inertia.js auto-sharing",
3
+ "version": "0.1.0",
4
+ "description": "Cloudflare Flagship feature flags for the Stratal framework — binding API wrapper with Inertia.js sharing",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
- "author": "Temitayo Fadojutimi",
8
- "homepage": "https://github.com/strataljs/stratal#readme",
7
+ "author": {
8
+ "name": "Temitayo Fadojutimi",
9
+ "url": "https://x.com/adesege_"
10
+ },
11
+ "homepage": "https://stratal.dev",
9
12
  "repository": {
10
13
  "type": "git",
11
14
  "url": "git+https://github.com/strataljs/stratal.git",
@@ -27,7 +30,7 @@
27
30
  },
28
31
  "publishConfig": {
29
32
  "access": "public",
30
- "provenance": false
33
+ "provenance": true
31
34
  },
32
35
  "sideEffects": false,
33
36
  "files": [
@@ -54,14 +57,18 @@
54
57
  "lint:fix": "npx oxlint --fix ."
55
58
  },
56
59
  "peerDependencies": {
60
+ "@inertiajs/core": ">=3",
57
61
  "@inertiajs/react": ">=3",
58
- "@stratal/inertia": ">=0.0.21",
62
+ "@stratal/inertia": ">=0.1.0",
59
63
  "hono": ">=4",
60
64
  "react": ">=19",
61
65
  "react-dom": ">=19",
62
- "stratal": ">=0.0.21"
66
+ "stratal": ">=0.1.0"
63
67
  },
64
68
  "peerDependenciesMeta": {
69
+ "@inertiajs/core": {
70
+ "optional": true
71
+ },
65
72
  "@inertiajs/react": {
66
73
  "optional": true
67
74
  },
@@ -76,18 +83,19 @@
76
83
  }
77
84
  },
78
85
  "devDependencies": {
79
- "@cloudflare/workers-types": "4.20260528.1",
80
- "@inertiajs/core": "^3.1.1",
81
- "@inertiajs/react": "^3.1.1",
82
- "@types/node": "^25.6.2",
83
- "@types/react": "^19.2.14",
84
- "@types/react-dom": "^19.2.3",
85
- "hono": "^4.12.18",
86
- "react": "^19.2.6",
87
- "react-dom": "^19.2.6",
88
- "stratal": "workspace:*",
89
- "tsdown": "^0.22.0",
90
- "typescript": "^6.0.3",
91
- "vitest": "~4.1.5"
86
+ "@cloudflare/workers-types": "5.20260919.1",
87
+ "@inertiajs/core": "^3.7.1",
88
+ "@inertiajs/react": "^3.7.1",
89
+ "@stratal/inertia": "0.1.0",
90
+ "@types/node": "^26.6.2",
91
+ "@types/react": "^19.3.0",
92
+ "@types/react-dom": "^19.3.0",
93
+ "hono": "^4.13.8",
94
+ "react": "^19.3.0",
95
+ "react-dom": "^19.3.0",
96
+ "stratal": "0.1.0",
97
+ "tsdown": "^0.23.0",
98
+ "typescript": "^7.0.2",
99
+ "vitest": "~4.1.11"
92
100
  }
93
- }
101
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"types-Dxuc-7SJ.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;AAQA;;;KAAY,SAAA;;AAA8C;;KAKrD,sBAAA,GAAyB,OAAA,eACd,UAAA,GAAa,UAAA,CAAW,CAAA,UAAW,QAAA,GAAW,CAAA,iBAAkB,UAAA;;;;;;;;KAWpE,mBAAA,IAAuB,sBAAA,6BAE/B,sBAAA;;;;;;;;;;AAFJ;;;;UAiBiB,mBAAA;;;;;AASjB;;;KAAY,YAAA,GAAe,MAAA,SAAe,SAAA;;AAK1C;;UAAiB,cAAA;EAIK;EAFpB,OAAA,EAAS,mBAAA;EAAA;EAET,KAAA,GAAQ,YAAA;AAAA;;;AAMV;UAAiB,wBAAA;;EAEf,IAAA,EAAM,cAAA;EAEI;EAAV,OAAA,GAAU,mBAAA;EAMwB;;;;;EAAlC,OAAA,IAAW,GAAA,EAAK,aAAA,KAAkB,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;AAAA"}