@stratal/feature-flags 0.0.0-canary-e5681b8

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,131 @@
1
+ # @stratal/feature-flags
2
+
3
+ ## 0.0.0-canary-e5681b8
4
+
5
+ ### Patch Changes
6
+
7
+ - e5681b8: 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 [e5681b8]
12
+ - Updated dependencies [e5681b8]
13
+ - stratal@0.0.0-canary-e5681b8
14
+ - @stratal/inertia@0.0.0-canary-e5681b8
15
+
16
+ ## 0.1.0
17
+
18
+ ### Patch Changes
19
+
20
+ - a753e55: Release alongside the rest of the packages; nothing changed in this one.
21
+
22
+ 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.
23
+
24
+ - Updated dependencies [a753e55]
25
+ - Updated dependencies [a753e55]
26
+ - stratal@0.1.0
27
+ - @stratal/inertia@0.1.0
28
+
29
+ ## 0.0.27
30
+
31
+ ### Patch Changes
32
+
33
+ - 41a9140: Make feature flag evaluation resilient to runtime failures
34
+
35
+ 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.
36
+
37
+ - Updated dependencies [41a9140]
38
+ - stratal@0.0.27
39
+ - @stratal/inertia@0.0.27
40
+
41
+ ## 0.0.26
42
+
43
+ ### Patch Changes
44
+
45
+ - Updated dependencies [ab95f52]
46
+ - Updated dependencies [ab95f52]
47
+ - Updated dependencies [bb6d3b9]
48
+ - stratal@0.0.26
49
+ - @stratal/inertia@0.0.26
50
+
51
+ ## 0.0.25
52
+
53
+ ### Patch Changes
54
+
55
+ - Updated dependencies [e93db60]
56
+ - Updated dependencies [e93db60]
57
+ - stratal@0.0.25
58
+ - @stratal/inertia@0.0.25
59
+
60
+ ## 0.0.24
61
+
62
+ ### Patch Changes
63
+
64
+ - Updated dependencies [10cf223]
65
+ - @stratal/inertia@0.0.24
66
+ - stratal@0.0.24
67
+
68
+ ## 0.0.23
69
+
70
+ ### Patch Changes
71
+
72
+ - 13b0e8d: Add `@stratal/feature-flags` — Cloudflare Flagship feature flags via the native Worker binding API.
73
+ - `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)`.
74
+ - `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`.
75
+ - `@stratal/inertia`: expose a generic `ctx.share(key, value)` macro on `RouterContext` so middleware and packages can contribute per-request shared props.
76
+ - `@stratal/framework`: add a `ctx.user()` macro on `RouterContext` (shorthand for `AuthContext.requireUser()`).
77
+
78
+ - 13b0e8d: Fix correctness and security issues found in review.
79
+
80
+ Queue:
81
+ - 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.
82
+ - Honor the documented retry budget: `maxRetries` now counts retries correctly against Cloudflare's 1-based `message.attempts` (previously gave one fewer retry than configured).
83
+ - Derive idempotency keys from an order-stable serialization of `type` + `payload`, so payloads that differ only in key order dedupe correctly.
84
+ - `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.
85
+ - 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.
86
+
87
+ Email (SMTP):
88
+ - 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).
89
+ - 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.
90
+ - AUTH is gated on the server's advertised mechanisms and supports both `PLAIN` and `LOGIN`; usernames are percent-decoded like passwords.
91
+ - 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.
92
+ - 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).
93
+
94
+ Inertia SEO:
95
+ - `titleTemplate` substitutes every `%s` and treats `$`-sequences in the title literally.
96
+ - 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.
97
+ - 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).
98
+
99
+ Feature flags:
100
+ - `FeatureFlagService.use()` binds the target app exactly once.
101
+
102
+ Database (framework):
103
+ - The reentrant `$transaction` proxy forwards the receiver for non-transaction property access.
104
+
105
+ Testing:
106
+ - `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.
107
+
108
+ DI:
109
+ - 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.
110
+ - Detect circular dependencies and throw a clear error naming the cycle instead of overflowing the stack.
111
+ - `tryResolve` only swallows "no provider"; a registered provider that throws while constructing now surfaces the real error instead of injecting `undefined`.
112
+ - Request-cache invalidation tracks transitive constructor dependencies, so re-registering a value rebuilds cached services that depend on it through a transient intermediary.
113
+
114
+ Quarry dev runtime:
115
+ - 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.
116
+ - The `cloudflare:sockets` STARTTLS shim re-attaches the stream error handler to the upgraded socket, so post-upgrade connection errors still surface.
117
+
118
+ - Updated dependencies [13b0e8d]
119
+ - Updated dependencies [13b0e8d]
120
+ - Updated dependencies [13b0e8d]
121
+ - Updated dependencies [13b0e8d]
122
+ - Updated dependencies [13b0e8d]
123
+ - Updated dependencies [13b0e8d]
124
+ - Updated dependencies [13b0e8d]
125
+ - Updated dependencies [13b0e8d]
126
+ - Updated dependencies [13b0e8d]
127
+ - Updated dependencies [13b0e8d]
128
+ - Updated dependencies [13b0e8d]
129
+ - Updated dependencies [be813bc]
130
+ - stratal@0.0.23
131
+ - @stratal/inertia@0.0.23
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @stratal/feature-flags
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 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)
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm i @stratal/feature-flags
17
+ ```
18
+
19
+ Add the Flagship binding to your Wrangler config and run `npx wrangler types`:
20
+
21
+ ```jsonc
22
+ // wrangler.jsonc
23
+ { "flagship": [{ "binding": "FLAGS", "app_id": "<APP_ID>" }] }
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { FeatureFlagModule } from '@stratal/feature-flags'
30
+
31
+ @Module({
32
+ imports: [
33
+ FeatureFlagModule.forRoot({
34
+ apps: [{ binding: 'FLAGS', flags: { 'new-checkout': false } }],
35
+ context: (ctx) => ({ userId: ctx.user().id }), // ctx.user() from @stratal/framework
36
+ }),
37
+ ],
38
+ })
39
+ export class AppModule {}
40
+ ```
41
+
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(...)`).
45
+
46
+ Evaluate on the server:
47
+
48
+ ```ts
49
+ const enabled = await this.flags.getBooleanValue('new-checkout') // uses manifest default
50
+ ```
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
+
54
+ Read on the client:
55
+
56
+ ```tsx
57
+ import { useFeatureFlags, useFlag } from '@stratal/feature-flags/react'
58
+
59
+ const showNewCheckout = useFlag('new-checkout')
60
+ const flags = useFeatureFlags()
61
+ ```
62
+
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_).
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1,185 @@
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
+ import { AsyncModuleOptions, DynamicModule } from "stratal/module";
3
+ import { LoggerService } from "stratal/logger";
4
+ import { Middleware, Next, RouterContext } from "stratal/router";
5
+ import { ApplicationError } from "stratal/errors";
6
+ import { StratalEnv } from "stratal";
7
+ //#region src/feature-flags.module.d.ts
8
+ /**
9
+ * Feature Flag Module
10
+ *
11
+ * Evaluates Cloudflare Flagship feature flags through the native Worker binding.
12
+ * Declare your apps (and the flags you use) once; inject {@link FeatureFlagService}
13
+ * to evaluate them.
14
+ *
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
+ *
20
+ * @example
21
+ * ```typescript
22
+ * FeatureFlagModule.forRoot({
23
+ * apps: [{ binding: 'FLAGS', flags: { 'new-checkout': false } }],
24
+ * context: (ctx) => ({ userId: ctx.user().id }), // ctx.user() from @stratal/framework
25
+ * })
26
+ *
27
+ * // Or async, from config namespaces:
28
+ * FeatureFlagModule.forRootAsync({
29
+ * inject: [flagsConfig.KEY],
30
+ * useFactory: (cfg) => ({ apps: cfg.apps, default: cfg.default }),
31
+ * })
32
+ * ```
33
+ */
34
+ export declare class FeatureFlagModule {
35
+ /** Configure with static options. */
36
+ static forRoot(options: FeatureFlagModuleOptions): DynamicModule;
37
+ /** Configure with an async factory (when options depend on other services). */
38
+ static forRootAsync(options: AsyncModuleOptions<FeatureFlagModuleOptions>): DynamicModule;
39
+ }
40
+ //#endregion
41
+ //#region src/services/feature-flag.service.d.ts
42
+ /**
43
+ * Feature Flag Service
44
+ *
45
+ * Type-safe wrapper around a Cloudflare Flagship binding (`env.FLAGS`). Mirrors
46
+ * the binding's evaluation methods 1:1, with two ergonomic additions:
47
+ *
48
+ * - **Manifest defaults** — when you omit a default, the value declared in the
49
+ * app's `flags` manifest is used (an explicit argument always wins).
50
+ * - **Default context** — the module's `context` resolver is merged into every
51
+ * evaluation (per-call context overrides it). Resolved from the current
52
+ * request; skipped automatically outside request scope.
53
+ *
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
+ *
59
+ * @example
60
+ * ```typescript
61
+ * @inject(FEATURE_FLAG_TOKENS.FeatureFlagService)
62
+ * private readonly flags: FeatureFlagService
63
+ *
64
+ * const enabled = await this.flags.getBooleanValue('new-checkout') // manifest default
65
+ * const layout = await this.flags.use('EXPERIMENT_FLAGS').getStringValue('layout', 'v1')
66
+ * ```
67
+ *
68
+ * @see https://developers.cloudflare.com/flagship/binding/
69
+ */
70
+ export declare class FeatureFlagService {
71
+ private readonly options;
72
+ private readonly env;
73
+ private readonly routerContext;
74
+ private readonly logger;
75
+ private readonly apps;
76
+ private bindingName;
77
+ private binding;
78
+ private manifest;
79
+ constructor(options: FeatureFlagModuleOptions, env: StratalEnv, routerContext: RouterContext | undefined, logger: LoggerService | undefined, initialBinding?: string);
80
+ /**
81
+ * Switch to a different configured Flagship app.
82
+ *
83
+ * Returns a new immutable instance bound to `binding`; the original is
84
+ * unchanged. The binding must be declared in the module's `apps`.
85
+ */
86
+ use(binding: FlagshipBindingName): FeatureFlagService;
87
+ /** The binding name this instance currently targets. */
88
+ get app(): string;
89
+ /** Returns the raw flag value without type checking. */
90
+ get(flagKey: string, defaultValue?: unknown, context?: FlagshipEvaluationContext): Promise<unknown>;
91
+ /** Returns the flag value as a `boolean`. */
92
+ getBooleanValue(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<boolean>;
93
+ /** Returns the flag value as a `string`. */
94
+ getStringValue(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<string>;
95
+ /** Returns the flag value as a `number`. */
96
+ getNumberValue(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<number>;
97
+ /** Returns the flag value as a typed object. */
98
+ getObjectValue<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<T>;
99
+ /** Returns the `boolean` flag value with evaluation metadata. */
100
+ getBooleanDetails(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<boolean>>;
101
+ /** Returns the `string` flag value with evaluation metadata. */
102
+ getStringDetails(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<string>>;
103
+ /** Returns the `number` flag value with evaluation metadata. */
104
+ getNumberDetails(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<number>>;
105
+ /** Returns the typed object flag value with evaluation metadata. */
106
+ getObjectDetails<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<T>>;
107
+ /**
108
+ * Evaluates every flag declared in the current app's manifest and returns a
109
+ * `{ key: value }` map. The evaluation method is chosen from each declared
110
+ * default's type. Powers `FeatureFlagShareMiddleware`.
111
+ */
112
+ all(context?: FlagshipEvaluationContext): Promise<Record<string, FlagValue>>;
113
+ private bindTo;
114
+ /** Resolves the merged evaluation context (default context + per-call override). */
115
+ private context;
116
+ /** Picks the default: explicit arg, then manifest, then the type's zero value. */
117
+ private fallback;
118
+ /** Evaluates a single flag, choosing the method from the declared default's type. */
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;
131
+ }
132
+ //#endregion
133
+ //#region src/feature-flag-share.middleware.d.ts
134
+ /**
135
+ * Evaluates the declared flag manifest for the default app and shares it as the
136
+ * `featureFlags` prop on every Inertia page rendered during the request.
137
+ *
138
+ * Only runs on `GET` requests — page renders (full visits and partial reloads)
139
+ * are always `GET`, so mutating API calls don't trigger evaluation. No-ops when
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
+ * ```
152
+ */
153
+ export declare class FeatureFlagShareMiddleware implements Middleware {
154
+ private readonly flags;
155
+ constructor(flags: FeatureFlagService);
156
+ handle(ctx: RouterContext, next: Next): Promise<void>;
157
+ }
158
+ //#endregion
159
+ //#region src/feature-flags.tokens.d.ts
160
+ /**
161
+ * DI tokens for the feature-flags module.
162
+ *
163
+ * Use `Symbol.for(...)` so the tokens resolve to the same symbol across module
164
+ * boundaries (the global symbol registry).
165
+ */
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. */
170
+ readonly FeatureFlagService: symbol;
171
+ };
172
+ type FeatureFlagToken = (typeof FEATURE_FLAG_TOKENS)[keyof typeof FEATURE_FLAG_TOKENS];
173
+ //#endregion
174
+ //#region src/feature-flags.error.d.ts
175
+ /**
176
+ * Thrown for feature-flag misconfiguration — an unknown app or a Flagship
177
+ * binding that is not present on the Worker environment.
178
+ *
179
+ * Note: flag *evaluation* never throws; the binding returns the supplied
180
+ * default value on error.
181
+ */
182
+ export declare class FeatureFlagError extends ApplicationError {}
183
+ //#endregion
184
+ export type { FeatureFlagApp, FeatureFlagModuleOptions, FeatureFlagRegistry, FeatureFlagToken, FlagManifest, FlagValue, FlagshipBindingName };
185
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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 ADDED
@@ -0,0 +1,275 @@
1
+ import { Module } from "stratal/module";
2
+ import { DI_TOKENS, Transient, inject } from "stratal/di";
3
+ import { LOGGER_TOKENS } from "stratal/logger";
4
+ import { ROUTER_TOKENS } from "stratal/router";
5
+ import { ApplicationError } from "stratal/errors";
6
+ //#region src/feature-flags.tokens.ts
7
+ /**
8
+ * DI tokens for the feature-flags module.
9
+ *
10
+ * Use `Symbol.for(...)` so the tokens resolve to the same symbol across module
11
+ * boundaries (the global symbol registry).
12
+ */
13
+ const FEATURE_FLAG_TOKENS = {
14
+ /** The resolved {@link FeatureFlagModuleOptions}. */
15
+ Options: Symbol.for("stratal:feature-flags:options"),
16
+ /** The request-scoped {@link FeatureFlagService} bound to the default app. */
17
+ FeatureFlagService: Symbol.for("stratal:feature-flags:service")
18
+ };
19
+ //#endregion
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
31
+ function __decorateMetadata(k, v) {
32
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
33
+ }
34
+ //#endregion
35
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorateParam.js
36
+ function __decorateParam(paramIndex, decorator) {
37
+ return function(target, key) {
38
+ decorator(target, key, paramIndex);
39
+ };
40
+ }
41
+ //#endregion
42
+ //#region \0@oxc-project+runtime@0.150.0/helpers/esm/decorate.js
43
+ function __decorate(decorators, target, key, desc) {
44
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
45
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
46
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
47
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
48
+ }
49
+ //#endregion
50
+ //#region src/services/feature-flag.service.ts
51
+ var _FeatureFlagService;
52
+ let FeatureFlagService = _FeatureFlagService = class FeatureFlagService {
53
+ options;
54
+ env;
55
+ routerContext;
56
+ logger;
57
+ apps = /* @__PURE__ */ new Map();
58
+ bindingName;
59
+ binding;
60
+ manifest;
61
+ constructor(options, env, routerContext, logger, initialBinding) {
62
+ this.options = options;
63
+ this.env = env;
64
+ this.routerContext = routerContext;
65
+ this.logger = logger;
66
+ for (const app of options.apps) this.apps.set(app.binding, app);
67
+ this.bindTo(initialBinding ?? options.default ?? options.apps[0]?.binding);
68
+ }
69
+ /**
70
+ * Switch to a different configured Flagship app.
71
+ *
72
+ * Returns a new immutable instance bound to `binding`; the original is
73
+ * unchanged. The binding must be declared in the module's `apps`.
74
+ */
75
+ use(binding) {
76
+ if (binding === this.bindingName) return this;
77
+ return new _FeatureFlagService(this.options, this.env, this.routerContext, this.logger, binding);
78
+ }
79
+ /** The binding name this instance currently targets. */
80
+ get app() {
81
+ return this.bindingName;
82
+ }
83
+ /** Returns the raw flag value without type checking. */
84
+ async get(flagKey, defaultValue, context) {
85
+ const fallback = this.fallback(flagKey, defaultValue);
86
+ return this.safe(flagKey, async () => this.binding.get(flagKey, fallback, await this.context(context)), () => fallback);
87
+ }
88
+ /** Returns the flag value as a `boolean`. */
89
+ async getBooleanValue(flagKey, defaultValue, 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);
92
+ }
93
+ /** Returns the flag value as a `string`. */
94
+ async getStringValue(flagKey, defaultValue, context) {
95
+ const fallback = this.fallback(flagKey, defaultValue, "");
96
+ return this.safe(flagKey, async () => this.binding.getStringValue(flagKey, fallback, await this.context(context)), () => fallback);
97
+ }
98
+ /** Returns the flag value as a `number`. */
99
+ async getNumberValue(flagKey, defaultValue, 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);
102
+ }
103
+ /** Returns the flag value as a typed object. */
104
+ async getObjectValue(flagKey, defaultValue, context) {
105
+ const fallback = this.fallback(flagKey, defaultValue, {});
106
+ return this.safe(flagKey, async () => this.binding.getObjectValue(flagKey, fallback, await this.context(context)), () => fallback);
107
+ }
108
+ /** Returns the `boolean` flag value with evaluation metadata. */
109
+ async getBooleanDetails(flagKey, defaultValue, 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));
112
+ }
113
+ /** Returns the `string` flag value with evaluation metadata. */
114
+ async getStringDetails(flagKey, defaultValue, 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));
117
+ }
118
+ /** Returns the `number` flag value with evaluation metadata. */
119
+ async getNumberDetails(flagKey, defaultValue, 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));
122
+ }
123
+ /** Returns the typed object flag value with evaluation metadata. */
124
+ async getObjectDetails(flagKey, defaultValue, 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));
127
+ }
128
+ /**
129
+ * Evaluates every flag declared in the current app's manifest and returns a
130
+ * `{ key: value }` map. The evaluation method is chosen from each declared
131
+ * default's type. Powers `FeatureFlagShareMiddleware`.
132
+ */
133
+ async all(context) {
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
+ }
142
+ const values = await Promise.all(keys.map((key) => this.evaluate(key, this.manifest[key], merged)));
143
+ const result = {};
144
+ keys.forEach((key, i) => {
145
+ result[key] = values[i];
146
+ });
147
+ return result;
148
+ }
149
+ bindTo(name) {
150
+ if (!name) throw new FeatureFlagError("No feature flag apps configured. Provide at least one app in FeatureFlagModule.forRoot({ apps: [...] }).");
151
+ const app = this.apps.get(name);
152
+ if (!app) throw new FeatureFlagError(`Feature flag app "${name}" is not configured.`);
153
+ const binding = this.env[name];
154
+ if (!binding) throw new FeatureFlagError(`Flagship binding "${name}" was not found in the environment.`);
155
+ this.bindingName = name;
156
+ this.binding = binding;
157
+ this.manifest = app.flags ?? {};
158
+ }
159
+ /** Resolves the merged evaluation context (default context + per-call override). */
160
+ async context(callContext) {
161
+ if (!this.options.context || !this.routerContext) return callContext;
162
+ const base = await this.options.context(this.routerContext);
163
+ return callContext ? {
164
+ ...base,
165
+ ...callContext
166
+ } : base;
167
+ }
168
+ /** Picks the default: explicit arg, then manifest, then the type's zero value. */
169
+ fallback(flagKey, provided, zero) {
170
+ if (provided !== void 0) return provided;
171
+ if (flagKey in this.manifest) return this.manifest[flagKey];
172
+ return zero;
173
+ }
174
+ /** Evaluates a single flag, choosing the method from the declared default's type. */
175
+ evaluate(flagKey, declared, context) {
176
+ switch (typeof declared) {
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);
195
+ }
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
+ }
210
+ };
211
+ FeatureFlagService = _FeatureFlagService = __decorate([
212
+ Transient(FEATURE_FLAG_TOKENS.FeatureFlagService),
213
+ __decorateParam(0, inject(FEATURE_FLAG_TOKENS.Options)),
214
+ __decorateParam(1, inject(DI_TOKENS.CloudflareEnv)),
215
+ __decorateParam(2, inject(ROUTER_TOKENS.RouterContext, { isOptional: true })),
216
+ __decorateParam(3, inject(LOGGER_TOKENS.LoggerService, { isOptional: true })),
217
+ __decorateMetadata("design:paramtypes", [
218
+ Object,
219
+ Object,
220
+ Object,
221
+ Object,
222
+ String
223
+ ])
224
+ ], FeatureFlagService);
225
+ //#endregion
226
+ //#region src/feature-flags.module.ts
227
+ var _FeatureFlagModule;
228
+ let FeatureFlagModule = _FeatureFlagModule = class FeatureFlagModule {
229
+ /** Configure with static options. */
230
+ static forRoot(options) {
231
+ return {
232
+ module: _FeatureFlagModule,
233
+ providers: [{
234
+ provide: FEATURE_FLAG_TOKENS.Options,
235
+ useValue: options
236
+ }]
237
+ };
238
+ }
239
+ /** Configure with an async factory (when options depend on other services). */
240
+ static forRootAsync(options) {
241
+ return {
242
+ module: _FeatureFlagModule,
243
+ providers: [{
244
+ provide: FEATURE_FLAG_TOKENS.Options,
245
+ useFactory: options.useFactory,
246
+ inject: options.inject
247
+ }]
248
+ };
249
+ }
250
+ };
251
+ FeatureFlagModule = _FeatureFlagModule = __decorate([Module({ providers: [{
252
+ provide: FEATURE_FLAG_TOKENS.FeatureFlagService,
253
+ useClass: FeatureFlagService
254
+ }] })], FeatureFlagModule);
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
273
+ export { FEATURE_FLAG_TOKENS, FeatureFlagError, FeatureFlagModule, FeatureFlagService, FeatureFlagShareMiddleware };
274
+
275
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
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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"}
@@ -0,0 +1,22 @@
1
+ import { r as FeatureFlagRegistry } from "./types-D-uV0zlC.mjs";
2
+ //#region src/react/use-feature-flags.d.ts
3
+ /**
4
+ * Returns the full map of feature flags shared by `FeatureFlagShareMiddleware`.
5
+ */
6
+ export declare function useFeatureFlags(): Record<string, unknown>;
7
+ /**
8
+ * Returns a single shared feature flag value.
9
+ *
10
+ * When you augment {@link FeatureFlagRegistry}, the key and return type are
11
+ * checked against your declared flags. Otherwise pass an explicit default.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * const showNewCheckout = useFlag('new-checkout') // typed via FeatureFlagRegistry
16
+ * const layout = useFlag('layout', 'v1') // loose fallback
17
+ * ```
18
+ */
19
+ export declare function useFlag<K extends keyof FeatureFlagRegistry>(key: K): FeatureFlagRegistry[K];
20
+ export declare function useFlag<T>(key: string, defaultValue: T): T;
21
+ //#endregion
22
+ //# sourceMappingURL=react.d.mts.map
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1,16 @@
1
+ import { usePage } from "@inertiajs/react";
2
+ //#region src/react/use-feature-flags.ts
3
+ /**
4
+ * Returns the full map of feature flags shared by `FeatureFlagShareMiddleware`.
5
+ */
6
+ function useFeatureFlags() {
7
+ return usePage().props.featureFlags ?? {};
8
+ }
9
+ function useFlag(key, defaultValue) {
10
+ const flags = useFeatureFlags();
11
+ return key in flags ? flags[key] : defaultValue;
12
+ }
13
+ //#endregion
14
+ export { useFeatureFlags, useFlag };
15
+
16
+ //# sourceMappingURL=react.mjs.map
@@ -0,0 +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 `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"}
@@ -0,0 +1,70 @@
1
+ import { RouterContext } from "stratal/router";
2
+ import { StratalEnv } from "stratal";
3
+ //#region src/types.d.ts
4
+ /**
5
+ * A value a feature flag can resolve to.
6
+ *
7
+ * @see https://developers.cloudflare.com/flagship/binding/methods/
8
+ */
9
+ type FlagValue = boolean | string | number | object;
10
+ /**
11
+ * String keys of the augmented `StratalEnv` whose value is a Flagship binding.
12
+ */
13
+ type FlagshipBindingFromEnv = Extract<{ [K in keyof StratalEnv]: StratalEnv[K] extends Flagship ? K : never; }[keyof StratalEnv], string>;
14
+ /**
15
+ * Type-safe Flagship binding name.
16
+ *
17
+ * Resolves to the union of `Flagship`-typed binding keys on the augmented
18
+ * `StratalEnv`. Falls back to `string` when no Flagship bindings are visible
19
+ * (for example library code compiled outside an app's env context).
20
+ */
21
+ type FlagshipBindingName = [FlagshipBindingFromEnv] extends [never] ? string : FlagshipBindingFromEnv;
22
+ /**
23
+ * Augment this interface to get typed flag keys for `useFlag()` and the service.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * declare module '@stratal/feature-flags' {
28
+ * interface FeatureFlagRegistry {
29
+ * 'new-checkout': boolean
30
+ * 'checkout-flow': string
31
+ * }
32
+ * }
33
+ * ```
34
+ */
35
+ interface FeatureFlagRegistry {}
36
+ /**
37
+ * A declared set of flags and their default values.
38
+ *
39
+ * Flagship has no enumeration API, so the flags you intend to evaluate (and
40
+ * share to the frontend) must be declared once here. The default also
41
+ * doubles as the type hint used to pick the evaluation method in `all()`.
42
+ */
43
+ type FlagManifest = Record<string, FlagValue>;
44
+ /**
45
+ * A single Flagship app bound to the Worker.
46
+ */
47
+ interface FeatureFlagApp {
48
+ /** Flagship binding name from your Wrangler config (type-checked against `StratalEnv`). */
49
+ binding: FlagshipBindingName;
50
+ /** Declared flags + defaults for this app. Used for manifest defaults and Inertia sharing. */
51
+ flags?: FlagManifest;
52
+ }
53
+ /**
54
+ * Feature-flags module configuration.
55
+ */
56
+ interface FeatureFlagModuleOptions {
57
+ /** One or more Flagship apps. A Worker may bind to multiple apps. */
58
+ apps: FeatureFlagApp[];
59
+ /** Default app binding used by the injected `FeatureFlagService`. Defaults to `apps[0].binding`. */
60
+ default?: FlagshipBindingName;
61
+ /**
62
+ * Resolves a per-request evaluation context (for example `{ userId }`) merged
63
+ * into every evaluation. Per-call context passed to a method overrides these.
64
+ * Receives the current request context; skipped outside request scope.
65
+ */
66
+ context?: (ctx: RouterContext) => FlagshipEvaluationContext | Promise<FlagshipEvaluationContext>;
67
+ }
68
+ //#endregion
69
+ export { FlagValue as a, FlagManifest as i, FeatureFlagModuleOptions as n, FlagshipBindingName as o, FeatureFlagRegistry as r, FeatureFlagApp as t };
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 ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@stratal/feature-flags",
3
+ "version": "0.0.0-canary-e5681b8",
4
+ "description": "Cloudflare Flagship feature flags for the Stratal framework — binding API wrapper with Inertia.js sharing",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Temitayo Fadojutimi",
9
+ "url": "https://x.com/adesege_"
10
+ },
11
+ "homepage": "https://stratal.dev",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/strataljs/stratal.git",
15
+ "directory": "packages/feature-flags"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/strataljs/stratal/issues"
19
+ },
20
+ "keywords": [
21
+ "stratal",
22
+ "feature-flags",
23
+ "flagship",
24
+ "cloudflare-workers",
25
+ "inertia",
26
+ "react"
27
+ ],
28
+ "engines": {
29
+ "node": ">=22.0.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "provenance": true
34
+ },
35
+ "sideEffects": false,
36
+ "files": [
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.mts",
43
+ "import": "./dist/index.mjs"
44
+ },
45
+ "./react": {
46
+ "types": "./dist/react.d.mts",
47
+ "import": "./dist/react.mjs"
48
+ },
49
+ "./package.json": "./package.json"
50
+ },
51
+ "scripts": {
52
+ "build": "tsdown",
53
+ "typecheck": "tsc --noEmit",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "lint": "npx oxlint .",
57
+ "lint:fix": "npx oxlint --fix ."
58
+ },
59
+ "peerDependencies": {
60
+ "@inertiajs/core": ">=3",
61
+ "@inertiajs/react": ">=3",
62
+ "@stratal/inertia": "0.0.0-canary-e5681b8",
63
+ "hono": ">=4",
64
+ "react": ">=19",
65
+ "react-dom": ">=19",
66
+ "stratal": "0.0.0-canary-e5681b8"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@inertiajs/core": {
70
+ "optional": true
71
+ },
72
+ "@inertiajs/react": {
73
+ "optional": true
74
+ },
75
+ "@stratal/inertia": {
76
+ "optional": true
77
+ },
78
+ "react": {
79
+ "optional": true
80
+ },
81
+ "react-dom": {
82
+ "optional": true
83
+ }
84
+ },
85
+ "devDependencies": {
86
+ "@cloudflare/workers-types": "5.20260919.1",
87
+ "@inertiajs/core": "^3.7.1",
88
+ "@inertiajs/react": "^3.7.1",
89
+ "@stratal/inertia": "0.0.0-canary-e5681b8",
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.0.0-canary-e5681b8",
97
+ "tsdown": "^0.23.0",
98
+ "typescript": "^7.0.2",
99
+ "vitest": "~4.1.11"
100
+ }
101
+ }