@fireweaveai/server-sdk-cjs 0.0.0-reserve.0 → 0.0.0-stage
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -54
- package/package.json +4 -56
- package/LICENSE +0 -21
- package/dist/application/client.d.ts +0 -77
- package/dist/application/client.js +0 -144
- package/dist/application/mode.d.ts +0 -70
- package/dist/application/mode.js +0 -74
- package/dist/application/ports.d.ts +0 -94
- package/dist/application/ports.js +0 -2
- package/dist/application/runtime.d.ts +0 -101
- package/dist/application/runtime.js +0 -346
- package/dist/domain/context.d.ts +0 -67
- package/dist/domain/context.js +0 -90
- package/dist/domain/errors.d.ts +0 -45
- package/dist/domain/errors.js +0 -99
- package/dist/domain/target.d.ts +0 -2
- package/dist/domain/target.js +0 -2
- package/dist/domain/types.d.ts +0 -107
- package/dist/domain/types.js +0 -2
- package/dist/domain/validation.d.ts +0 -124
- package/dist/domain/validation.js +0 -355
- package/dist/index.d.ts +0 -34
- package/dist/index.js +0 -52
- package/dist/infrastructure/adapters/inmemory.d.ts +0 -77
- package/dist/infrastructure/adapters/inmemory.js +0 -151
- package/dist/infrastructure/adapters/local.d.ts +0 -108
- package/dist/infrastructure/adapters/local.js +0 -105
- package/dist/infrastructure/adapters/remote.d.ts +0 -74
- package/dist/infrastructure/adapters/remote.js +0 -373
- package/dist/infrastructure/hosts.d.ts +0 -18
- package/dist/infrastructure/hosts.js +0 -72
- package/dist/register-browser.d.ts +0 -1
- package/dist/register-browser.js +0 -7
- package/dist/register.d.ts +0 -1
- package/dist/register.js +0 -13
- package/dist/start/browser.d.ts +0 -12
- package/dist/start/browser.js +0 -36
- package/dist/start/build-info.d.ts +0 -2
- package/dist/start/build-info.js +0 -9
- package/dist/start/control-points.d.ts +0 -20
- package/dist/start/control-points.js +0 -59
- package/dist/start/env.d.ts +0 -11
- package/dist/start/env.js +0 -64
- package/dist/start/fw.d.ts +0 -29
- package/dist/start/fw.js +0 -76
- package/dist/start/index.d.ts +0 -23
- package/dist/start/index.js +0 -28
- package/dist/start/instance.d.ts +0 -17
- package/dist/start/instance.js +0 -56
- package/dist/start/names.d.ts +0 -22
- package/dist/start/names.js +0 -25
- package/dist/start/resolve.d.ts +0 -40
- package/dist/start/resolve.js +0 -139
- package/dist/start/state.d.ts +0 -96
- package/dist/start/state.js +0 -303
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { type ContextInput, type ContextLimits } from '../domain/context.js';
|
|
2
|
-
import { type ExpectedFlagType } from '../domain/validation.js';
|
|
3
|
-
import type { BackendAdapter, RegisterTargetOptions, RegisterTargetResult } from './ports.js';
|
|
4
|
-
import type { CanonicalContext, Decision, JsonValue, LifecycleState } from '../domain/types.js';
|
|
5
|
-
export type { ExpectedFlagType } from '../domain/validation.js';
|
|
6
|
-
export interface FireweaveRuntimeConfig {
|
|
7
|
-
/**
|
|
8
|
-
* Fireweave project/runtime key (`project-api-key_…`). Adapters may also read
|
|
9
|
-
* it from their own options.
|
|
10
|
-
*/
|
|
11
|
-
projectApiKey?: string;
|
|
12
|
-
/** Backend host (must be http(s) and pass the allowlist). */
|
|
13
|
-
host?: string;
|
|
14
|
-
/**
|
|
15
|
-
* SSRF allowlist: hostnames the SDK may talk to. Empty/undefined ⇒ the
|
|
16
|
-
* canonical Fireweave default allowlist + loopback (DEFAULT_ALLOWED_HOSTS).
|
|
17
|
-
* Custom/self-hosted endpoints must be listed explicitly; ['*'] opts out.
|
|
18
|
-
*/
|
|
19
|
-
allowedHosts?: readonly string[];
|
|
20
|
-
/** Require targetingKey on every evaluation. */
|
|
21
|
-
requireTargetingKey?: boolean;
|
|
22
|
-
/** Context bound overrides (defaults are the ratified spec bounds). */
|
|
23
|
-
limits?: Partial<ContextLimits>;
|
|
24
|
-
/** Additional reserved attribute keys. */
|
|
25
|
-
reservedAttributeKeys?: readonly string[];
|
|
26
|
-
/** Shutdown flush deadline (ms). Default DEFAULT_SHUTDOWN_TIMEOUT_MS. */
|
|
27
|
-
shutdownTimeoutMs?: number;
|
|
28
|
-
}
|
|
29
|
-
/** Default bound on shutdown/flush (matches capabilities.get()). */
|
|
30
|
-
export declare const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10000;
|
|
31
|
-
export interface EvaluateOptions {
|
|
32
|
-
/** Attach flag payload as fireweave.payload metadata (sorted-key JSON string). */
|
|
33
|
-
includePayload?: boolean;
|
|
34
|
-
/**
|
|
35
|
-
* When true, emit a Fireweave-owned exposure for a successful evaluation
|
|
36
|
-
* (H-4 / ADR-0001 §23 errata / ruling 20). Default `false` — phase-one
|
|
37
|
-
* evaluate is side-effect-free. Vendor `$feature_flag_called` stays
|
|
38
|
-
* suppressed on the local snapshot path (RB-2); Fireweave owns emission
|
|
39
|
-
* and dedup when opted in.
|
|
40
|
-
*/
|
|
41
|
-
sendExposure?: boolean;
|
|
42
|
-
signal?: AbortSignal;
|
|
43
|
-
}
|
|
44
|
-
/** JSON stringify with deterministic (sorted) key order at every level. */
|
|
45
|
-
export declare function stableStringify(value: JsonValue): string;
|
|
46
|
-
export declare class FireweaveRuntime {
|
|
47
|
-
readonly adapter: BackendAdapter;
|
|
48
|
-
readonly config: FireweaveRuntimeConfig;
|
|
49
|
-
private readonly contextPolicy;
|
|
50
|
-
private state;
|
|
51
|
-
private globalContext;
|
|
52
|
-
private clientContext;
|
|
53
|
-
private initPromise;
|
|
54
|
-
private readonly stateListeners;
|
|
55
|
-
/** Dedup window for evaluate/OF-path exposures (cleared on flush / shutdown). */
|
|
56
|
-
private readonly evaluateExposureSeen;
|
|
57
|
-
constructor(adapter: BackendAdapter, config?: FireweaveRuntimeConfig);
|
|
58
|
-
/** Clear evaluate-path exposure dedup (M-2 / clear-on-flush lifecycle). */
|
|
59
|
-
clearEvaluateExposureDedup(): void;
|
|
60
|
-
getState(): LifecycleState;
|
|
61
|
-
onStateChange(listener: (state: LifecycleState) => void): void;
|
|
62
|
-
private setState;
|
|
63
|
-
/** Mark definitions/cache staleness (adapter-driven). */
|
|
64
|
-
markStale(): void;
|
|
65
|
-
setGlobalContext(context: ContextInput | undefined): void;
|
|
66
|
-
setClientContext(context: ContextInput | undefined): void;
|
|
67
|
-
/**
|
|
68
|
-
* Initialize the runtime: validate config, then bring the adapter up.
|
|
69
|
-
* Rejects with FireweaveError; Configuration errors put the runtime in FATAL.
|
|
70
|
-
*/
|
|
71
|
-
initialize(signal?: AbortSignal): Promise<void>;
|
|
72
|
-
shutdown(): Promise<void>;
|
|
73
|
-
/** Canonicalize + validate the merged context (throws FireweaveError). */
|
|
74
|
-
resolveContext(invocationContext: ContextInput | undefined): CanonicalContext;
|
|
75
|
-
/**
|
|
76
|
-
* Evaluate a flag to a canonical Decision. Never throws.
|
|
77
|
-
*
|
|
78
|
-
* Validates in the fixed order spec/control-points.md "Validation, before
|
|
79
|
-
* any I/O" names, stopping at the first failure: (1) key, (2) default vs
|
|
80
|
-
* type, (3) context, (4) lifecycle. Only once all four pass does this
|
|
81
|
-
* reach the adapter (the one I/O call in this method).
|
|
82
|
-
*/
|
|
83
|
-
evaluate(controlPointKey: string, expectedType: ExpectedFlagType, defaultValue: JsonValue, invocationContext?: ContextInput, options?: EvaluateOptions): Promise<Decision>;
|
|
84
|
-
/**
|
|
85
|
-
* Register a user or device so flag rules can target its DURABLE properties.
|
|
86
|
-
*
|
|
87
|
-
* Call once per login / device provisioning with the facts that outlive a
|
|
88
|
-
* request (plan, beta membership, region, device model), then send the same
|
|
89
|
-
* `targetingKey` on evaluation. Per-request context attributes still override
|
|
90
|
-
* the registered properties for a single evaluation — the two identity paths
|
|
91
|
-
* compose (spec/remote-protocol.md § Two identity paths).
|
|
92
|
-
*
|
|
93
|
-
* Resolves with `ok: false` instead of throwing: this runs in sign-in paths.
|
|
94
|
-
* Adapters without the capability (in-memory, local dev) report
|
|
95
|
-
* `Unsupported` so a dev harness does not silently look registered.
|
|
96
|
-
*/
|
|
97
|
-
registerTarget(targetingKey: string, options?: RegisterTargetOptions): Promise<RegisterTargetResult>;
|
|
98
|
-
private emitEvaluateExposure;
|
|
99
|
-
private lifecycleError;
|
|
100
|
-
private errorDecision;
|
|
101
|
-
}
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FireweaveRuntime = exports.DEFAULT_SHUTDOWN_TIMEOUT_MS = void 0;
|
|
4
|
-
exports.stableStringify = stableStringify;
|
|
5
|
-
/**
|
|
6
|
-
* FireweaveRuntime: shared engine behind the OpenFeature provider and the
|
|
7
|
-
* FireweaveClient extensions. Owns lifecycle state machine, config validation,
|
|
8
|
-
* context policy, and decision construction. Defaults never throw: evaluation
|
|
9
|
-
* always returns a Decision.
|
|
10
|
-
*/
|
|
11
|
-
const errors_js_1 = require("../domain/errors.js");
|
|
12
|
-
const hosts_js_1 = require("../infrastructure/hosts.js");
|
|
13
|
-
const context_js_1 = require("../domain/context.js");
|
|
14
|
-
const validation_js_1 = require("../domain/validation.js");
|
|
15
|
-
/** Default bound on shutdown/flush (matches capabilities.get()). */
|
|
16
|
-
exports.DEFAULT_SHUTDOWN_TIMEOUT_MS = 10000;
|
|
17
|
-
function validateConfig(config) {
|
|
18
|
-
if (config.host !== undefined) {
|
|
19
|
-
// Allowlist is ON by default: undefined/empty
|
|
20
|
-
// allowedHosts falls back to the canonical Fireweave + loopback list.
|
|
21
|
-
(0, hosts_js_1.assertHostAllowed)(config.host, config.allowedHosts);
|
|
22
|
-
}
|
|
23
|
-
if (config.limits !== undefined) {
|
|
24
|
-
for (const value of Object.values(config.limits)) {
|
|
25
|
-
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
|
|
26
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
/** JSON stringify with deterministic (sorted) key order at every level. */
|
|
32
|
-
function stableStringify(value) {
|
|
33
|
-
if (value === null || typeof value !== 'object')
|
|
34
|
-
return JSON.stringify(value);
|
|
35
|
-
if (Array.isArray(value))
|
|
36
|
-
return `[${value.map((v) => stableStringify(v)).join(',')}]`;
|
|
37
|
-
const keys = Object.keys(value).sort();
|
|
38
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
|
|
39
|
-
}
|
|
40
|
-
class FireweaveRuntime {
|
|
41
|
-
adapter;
|
|
42
|
-
config;
|
|
43
|
-
contextPolicy;
|
|
44
|
-
state = 'UNINITIALIZED';
|
|
45
|
-
globalContext;
|
|
46
|
-
clientContext;
|
|
47
|
-
initPromise;
|
|
48
|
-
stateListeners = new Set();
|
|
49
|
-
/** Dedup window for evaluate/OF-path exposures (cleared on flush / shutdown). */
|
|
50
|
-
evaluateExposureSeen = new Set();
|
|
51
|
-
constructor(adapter, config = {}) {
|
|
52
|
-
this.adapter = adapter;
|
|
53
|
-
this.config = { ...config };
|
|
54
|
-
const limits = { ...context_js_1.DEFAULT_CONTEXT_LIMITS, ...(config.limits ?? {}) };
|
|
55
|
-
this.contextPolicy = {
|
|
56
|
-
limits,
|
|
57
|
-
reservedAttributeKeys: [
|
|
58
|
-
...context_js_1.DEFAULT_RESERVED_ATTRIBUTE_KEYS,
|
|
59
|
-
...(config.reservedAttributeKeys ?? []),
|
|
60
|
-
],
|
|
61
|
-
requireTargetingKey: config.requireTargetingKey ?? false,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/** Clear evaluate-path exposure dedup (M-2 / clear-on-flush lifecycle). */
|
|
65
|
-
clearEvaluateExposureDedup() {
|
|
66
|
-
this.evaluateExposureSeen.clear();
|
|
67
|
-
}
|
|
68
|
-
getState() {
|
|
69
|
-
return this.state;
|
|
70
|
-
}
|
|
71
|
-
onStateChange(listener) {
|
|
72
|
-
this.stateListeners.add(listener);
|
|
73
|
-
}
|
|
74
|
-
setState(next) {
|
|
75
|
-
this.state = next;
|
|
76
|
-
for (const listener of this.stateListeners)
|
|
77
|
-
listener(next);
|
|
78
|
-
}
|
|
79
|
-
/** Mark definitions/cache staleness (adapter-driven). */
|
|
80
|
-
markStale() {
|
|
81
|
-
if (this.state === 'READY')
|
|
82
|
-
this.setState('STALE');
|
|
83
|
-
}
|
|
84
|
-
setGlobalContext(context) {
|
|
85
|
-
this.globalContext = context;
|
|
86
|
-
}
|
|
87
|
-
setClientContext(context) {
|
|
88
|
-
this.clientContext = context;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Initialize the runtime: validate config, then bring the adapter up.
|
|
92
|
-
* Rejects with FireweaveError; Configuration errors put the runtime in FATAL.
|
|
93
|
-
*/
|
|
94
|
-
async initialize(signal) {
|
|
95
|
-
if (this.state === 'SHUTDOWN') {
|
|
96
|
-
throw new errors_js_1.FireweaveError('AlreadyClosed');
|
|
97
|
-
}
|
|
98
|
-
if (this.state === 'READY')
|
|
99
|
-
return;
|
|
100
|
-
if (this.initPromise !== undefined)
|
|
101
|
-
return this.initPromise;
|
|
102
|
-
this.initPromise = (async () => {
|
|
103
|
-
this.setState('INITIALIZING');
|
|
104
|
-
try {
|
|
105
|
-
validateConfig(this.config);
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
this.setState('FATAL');
|
|
109
|
-
throw err;
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
await this.adapter.initialize(signal);
|
|
113
|
-
this.setState('READY');
|
|
114
|
-
}
|
|
115
|
-
catch (err) {
|
|
116
|
-
if ((0, errors_js_1.isFireweaveError)(err) && err.kind === 'Configuration') {
|
|
117
|
-
this.setState('FATAL');
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
this.setState('ERROR');
|
|
121
|
-
}
|
|
122
|
-
throw (0, errors_js_1.isFireweaveError)(err) ? err : new errors_js_1.FireweaveError('Internal', { cause: err });
|
|
123
|
-
}
|
|
124
|
-
finally {
|
|
125
|
-
this.initPromise = undefined;
|
|
126
|
-
}
|
|
127
|
-
})();
|
|
128
|
-
return this.initPromise;
|
|
129
|
-
}
|
|
130
|
-
async shutdown() {
|
|
131
|
-
if (this.state === 'SHUTDOWN')
|
|
132
|
-
return; // idempotent
|
|
133
|
-
// The configured deadline bounds the whole flush+close sequence (M-1/L-4):
|
|
134
|
-
// a wedged vendor client must not hang process exit.
|
|
135
|
-
const timeoutMs = this.config.shutdownTimeoutMs ?? exports.DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
136
|
-
let timer;
|
|
137
|
-
const deadline = new Promise((resolve) => {
|
|
138
|
-
timer = setTimeout(resolve, timeoutMs);
|
|
139
|
-
});
|
|
140
|
-
const close = (async () => {
|
|
141
|
-
try {
|
|
142
|
-
await this.adapter.flush?.();
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
// best-effort flush; never throw from shutdown
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
await this.adapter.shutdown();
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// swallow: shutdown must not throw
|
|
152
|
-
}
|
|
153
|
-
})();
|
|
154
|
-
try {
|
|
155
|
-
await Promise.race([close, deadline]);
|
|
156
|
-
}
|
|
157
|
-
finally {
|
|
158
|
-
this.evaluateExposureSeen.clear();
|
|
159
|
-
if (timer !== undefined)
|
|
160
|
-
clearTimeout(timer);
|
|
161
|
-
this.setState('SHUTDOWN');
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/** Canonicalize + validate the merged context (throws FireweaveError). */
|
|
165
|
-
resolveContext(invocationContext) {
|
|
166
|
-
const merged = (0, context_js_1.mergeContexts)(this.globalContext, this.clientContext, invocationContext);
|
|
167
|
-
const result = (0, validation_js_1.validateContext)(merged, this.contextPolicy);
|
|
168
|
-
if (!result.ok)
|
|
169
|
-
throw result.error;
|
|
170
|
-
return result.value;
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Evaluate a flag to a canonical Decision. Never throws.
|
|
174
|
-
*
|
|
175
|
-
* Validates in the fixed order spec/control-points.md "Validation, before
|
|
176
|
-
* any I/O" names, stopping at the first failure: (1) key, (2) default vs
|
|
177
|
-
* type, (3) context, (4) lifecycle. Only once all four pass does this
|
|
178
|
-
* reach the adapter (the one I/O call in this method).
|
|
179
|
-
*/
|
|
180
|
-
async evaluate(controlPointKey, expectedType, defaultValue, invocationContext, options = {}) {
|
|
181
|
-
const keyResult = (0, validation_js_1.validateControlPointKey)(controlPointKey);
|
|
182
|
-
if (!keyResult.ok) {
|
|
183
|
-
return this.errorDecision(controlPointKey, defaultValue, keyResult.error);
|
|
184
|
-
}
|
|
185
|
-
const defaultResult = (0, validation_js_1.validateDefaultValue)(expectedType, defaultValue);
|
|
186
|
-
if (!defaultResult.ok) {
|
|
187
|
-
return this.errorDecision(controlPointKey, defaultValue, defaultResult.error);
|
|
188
|
-
}
|
|
189
|
-
const merged = (0, context_js_1.mergeContexts)(this.globalContext, this.clientContext, invocationContext);
|
|
190
|
-
const contextResult = (0, validation_js_1.validateContext)(merged, this.contextPolicy);
|
|
191
|
-
if (!contextResult.ok) {
|
|
192
|
-
return this.errorDecision(controlPointKey, defaultValue, contextResult.error);
|
|
193
|
-
}
|
|
194
|
-
const context = contextResult.value;
|
|
195
|
-
const lifecycleError = this.lifecycleError();
|
|
196
|
-
if (lifecycleError !== undefined) {
|
|
197
|
-
return this.errorDecision(controlPointKey, defaultValue, lifecycleError);
|
|
198
|
-
}
|
|
199
|
-
let resolution;
|
|
200
|
-
try {
|
|
201
|
-
const resolveOpts = options.signal !== undefined ? { signal: options.signal } : {};
|
|
202
|
-
resolution = await this.adapter.resolve(controlPointKey, context, resolveOpts);
|
|
203
|
-
}
|
|
204
|
-
catch (err) {
|
|
205
|
-
// H-2: non-Fireweave (vendor/internal) exception text never reaches the
|
|
206
|
-
// outward errorMessage — the fixed taxonomy message is used and the
|
|
207
|
-
// original error is preserved on `cause` only.
|
|
208
|
-
const fw = (0, errors_js_1.isFireweaveError)(err) ? err : new errors_js_1.FireweaveError('Internal', { cause: err });
|
|
209
|
-
return this.errorDecision(controlPointKey, defaultValue, fw);
|
|
210
|
-
}
|
|
211
|
-
if (!resolution.found) {
|
|
212
|
-
// spec/modes.md "Behaviour per mode": local's unknown-key row is
|
|
213
|
-
// `default` / `reason: DEFAULT` — deliberately not an error, unlike
|
|
214
|
-
// remote's `default` / `ERROR` / `ControlPointNotFound`. An adapter signals the
|
|
215
|
-
// former by carrying `reason: 'DEFAULT'` on its miss (FireweaveLocalAdapter);
|
|
216
|
-
// any adapter that leaves `reason` unset (InMemoryAdapter,
|
|
217
|
-
// FireweaveRemoteAdapter) keeps the ControlPointNotFound/ERROR path below.
|
|
218
|
-
if (resolution.reason === 'DEFAULT') {
|
|
219
|
-
return { controlPointKey, value: defaultValue, reason: 'DEFAULT', metadata: {} };
|
|
220
|
-
}
|
|
221
|
-
const meta = {};
|
|
222
|
-
if (resolution.quotaLimited === true)
|
|
223
|
-
meta['fireweave.quotaLimited'] = true;
|
|
224
|
-
return this.errorDecision(controlPointKey, defaultValue, new errors_js_1.FireweaveError('ControlPointNotFound', { metadata: meta }));
|
|
225
|
-
}
|
|
226
|
-
const value = resolution.value ?? null;
|
|
227
|
-
if (!(0, validation_js_1.matchesExpectedType)(value, expectedType)) {
|
|
228
|
-
return this.errorDecision(controlPointKey, defaultValue, new errors_js_1.FireweaveError('TypeMismatch'));
|
|
229
|
-
}
|
|
230
|
-
const metadata = {};
|
|
231
|
-
if (resolution.version !== undefined)
|
|
232
|
-
metadata['fireweave.controlPointVersion'] = resolution.version;
|
|
233
|
-
// Detailed vendor fields travel together: they surface only when the
|
|
234
|
-
// backend reported BOTH a vendor flag id and a matched condition index
|
|
235
|
-
// (fixtures: eval-detailed-fields exposes them; eval-multivariate-string
|
|
236
|
-
// [index, no id] and eval-payload-attached [id, no index] do not).
|
|
237
|
-
if (resolution.vendorControlPointId !== undefined && resolution.conditionIndex !== undefined) {
|
|
238
|
-
metadata['fireweave.vendorControlPointId'] = resolution.vendorControlPointId;
|
|
239
|
-
if (resolution.reasonCode !== undefined)
|
|
240
|
-
metadata['fireweave.reasonCode'] = resolution.reasonCode;
|
|
241
|
-
}
|
|
242
|
-
if (options.includePayload === true && resolution.payload !== undefined) {
|
|
243
|
-
metadata['fireweave.payload'] =
|
|
244
|
-
typeof resolution.payload === 'string' ? resolution.payload : stableStringify(resolution.payload);
|
|
245
|
-
}
|
|
246
|
-
if (resolution.quotaLimited === true)
|
|
247
|
-
metadata['fireweave.quotaLimited'] = true;
|
|
248
|
-
if (resolution.fromCache === true)
|
|
249
|
-
metadata['fireweave.fromCache'] = true;
|
|
250
|
-
let reason;
|
|
251
|
-
if (resolution.enabled === false) {
|
|
252
|
-
reason = 'DISABLED';
|
|
253
|
-
}
|
|
254
|
-
else if (resolution.reason !== undefined) {
|
|
255
|
-
reason = resolution.reason;
|
|
256
|
-
}
|
|
257
|
-
else if (resolution.fromCache === true || this.state === 'STALE') {
|
|
258
|
-
reason = 'STALE';
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
reason = 'TARGETING_MATCH';
|
|
262
|
-
}
|
|
263
|
-
const decision = { controlPointKey, value, reason, metadata };
|
|
264
|
-
if (resolution.variant !== undefined)
|
|
265
|
-
decision.variant = resolution.variant;
|
|
266
|
-
// H-4 / ruling 20: evaluate is side-effect-free by default; opt in via sendExposure: true.
|
|
267
|
-
if (options.sendExposure === true) {
|
|
268
|
-
this.emitEvaluateExposure({
|
|
269
|
-
targetingKey: context.targetingKey ?? '',
|
|
270
|
-
controlPointKey,
|
|
271
|
-
value,
|
|
272
|
-
...(resolution.variant !== undefined ? { variant: resolution.variant } : {}),
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
return decision;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Register a user or device so flag rules can target its DURABLE properties.
|
|
279
|
-
*
|
|
280
|
-
* Call once per login / device provisioning with the facts that outlive a
|
|
281
|
-
* request (plan, beta membership, region, device model), then send the same
|
|
282
|
-
* `targetingKey` on evaluation. Per-request context attributes still override
|
|
283
|
-
* the registered properties for a single evaluation — the two identity paths
|
|
284
|
-
* compose (spec/remote-protocol.md § Two identity paths).
|
|
285
|
-
*
|
|
286
|
-
* Resolves with `ok: false` instead of throwing: this runs in sign-in paths.
|
|
287
|
-
* Adapters without the capability (in-memory, local dev) report
|
|
288
|
-
* `Unsupported` so a dev harness does not silently look registered.
|
|
289
|
-
*/
|
|
290
|
-
async registerTarget(targetingKey, options = {}) {
|
|
291
|
-
const lifecycle = this.lifecycleError();
|
|
292
|
-
if (lifecycle !== undefined)
|
|
293
|
-
return { ok: false, error: lifecycle };
|
|
294
|
-
if (this.adapter.registerTarget === undefined) {
|
|
295
|
-
return { ok: false, error: new errors_js_1.FireweaveError('UnsupportedCapability') };
|
|
296
|
-
}
|
|
297
|
-
return this.adapter.registerTarget(targetingKey, options);
|
|
298
|
-
}
|
|
299
|
-
emitEvaluateExposure(exposure) {
|
|
300
|
-
if (exposure.targetingKey.length === 0)
|
|
301
|
-
return;
|
|
302
|
-
if (this.adapter.recordExposure === undefined)
|
|
303
|
-
return;
|
|
304
|
-
const key = `${exposure.targetingKey}\u0000${exposure.controlPointKey}\u0000${exposure.variant ?? ''}\u0000${stableStringify(exposure.value ?? null)}`;
|
|
305
|
-
if (this.evaluateExposureSeen.has(key))
|
|
306
|
-
return;
|
|
307
|
-
this.evaluateExposureSeen.add(key);
|
|
308
|
-
this.adapter.recordExposure({
|
|
309
|
-
targetingKey: exposure.targetingKey,
|
|
310
|
-
controlPointKey: exposure.controlPointKey,
|
|
311
|
-
value: exposure.value,
|
|
312
|
-
...(exposure.variant !== undefined ? { variant: exposure.variant } : {}),
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
lifecycleError() {
|
|
316
|
-
switch (this.state) {
|
|
317
|
-
case 'UNINITIALIZED':
|
|
318
|
-
case 'INITIALIZING':
|
|
319
|
-
return new errors_js_1.FireweaveError('NotReady');
|
|
320
|
-
case 'ERROR':
|
|
321
|
-
return new errors_js_1.FireweaveError('NotReady');
|
|
322
|
-
case 'FATAL':
|
|
323
|
-
return new errors_js_1.FireweaveError('Configuration');
|
|
324
|
-
case 'SHUTDOWN':
|
|
325
|
-
return new errors_js_1.FireweaveError('AlreadyClosed');
|
|
326
|
-
default:
|
|
327
|
-
return undefined;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
errorDecision(controlPointKey, defaultValue, err) {
|
|
331
|
-
const metadata = {
|
|
332
|
-
'fireweave.errorKind': err.kind,
|
|
333
|
-
...err.metadata,
|
|
334
|
-
};
|
|
335
|
-
return {
|
|
336
|
-
controlPointKey,
|
|
337
|
-
value: defaultValue,
|
|
338
|
-
reason: 'ERROR',
|
|
339
|
-
errorCode: err.openFeatureErrorCode,
|
|
340
|
-
errorKind: err.kind,
|
|
341
|
-
errorMessage: err.message,
|
|
342
|
-
metadata,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
exports.FireweaveRuntime = FireweaveRuntime;
|
package/dist/domain/context.d.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Evaluation-context handling: merge order (global → client → invocation)
|
|
3
|
-
* and normalization of raw caller input into the shape validation.ts checks,
|
|
4
|
-
* plus the post-canonicalization reporting view.
|
|
5
|
-
* Bounds per spec/evaluation-context.schema.json (ratified 2026-07-27):
|
|
6
|
-
* 128 attrs / 256 B keys / 4 KiB values / depth 6 / 64 KiB serialized.
|
|
7
|
-
*
|
|
8
|
-
* Bound/reserved-key ENFORCEMENT (the pure, Validated<T>-returning checks)
|
|
9
|
-
* lives in validation.ts (`validateContext`) — spec/control-points.md
|
|
10
|
-
* "Validation, before any I/O" rule 3. This module has no throwing/failing
|
|
11
|
-
* surface of its own.
|
|
12
|
-
*/
|
|
13
|
-
import type { CanonicalContext, JsonValue } from './types.js';
|
|
14
|
-
export interface ContextLimits {
|
|
15
|
-
maxAttributeCount: number;
|
|
16
|
-
maxKeyBytes: number;
|
|
17
|
-
maxValueBytes: number;
|
|
18
|
-
maxNestingDepth: number;
|
|
19
|
-
maxSerializedContextBytes: number;
|
|
20
|
-
}
|
|
21
|
-
export declare const DEFAULT_CONTEXT_LIMITS: Readonly<ContextLimits>;
|
|
22
|
-
/** Attribute keys callers may never set (in addition to the fireweave.* namespace). */
|
|
23
|
-
export declare const DEFAULT_RESERVED_ATTRIBUTE_KEYS: readonly string[];
|
|
24
|
-
/**
|
|
25
|
-
* Sanctioned fireweave.* carriers (orchestrator rulings 12–14): the ONLY
|
|
26
|
-
* `fireweave.*` context keys callers may set. They are the canonical spelling
|
|
27
|
-
* for group memberships / group properties; plain `groups`/`groupProperties`
|
|
28
|
-
* remain accepted as a documented alias.
|
|
29
|
-
*/
|
|
30
|
-
export declare const ALLOWED_FIREWEAVE_CONTEXT_KEYS: readonly string[];
|
|
31
|
-
export interface ContextPolicy {
|
|
32
|
-
limits: ContextLimits;
|
|
33
|
-
reservedAttributeKeys: readonly string[];
|
|
34
|
-
requireTargetingKey: boolean;
|
|
35
|
-
}
|
|
36
|
-
/** Loose input shape accepted from callers (OpenFeature EvaluationContext-compatible). */
|
|
37
|
-
export interface ContextInput {
|
|
38
|
-
targetingKey?: string;
|
|
39
|
-
attributes?: Record<string, unknown>;
|
|
40
|
-
/** OpenFeature flat contexts: extra top-level keys are treated as attributes. */
|
|
41
|
-
[key: string]: unknown;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Extract {targetingKey, attributes} from a caller context. Supports both the
|
|
45
|
-
* canonical shape ({targetingKey, attributes:{...}}) and OpenFeature flat shape
|
|
46
|
-
* ({targetingKey, plan:"pro", ...}).
|
|
47
|
-
*/
|
|
48
|
-
export declare function normalizeContextInput(input: ContextInput | undefined): {
|
|
49
|
-
targetingKey?: string;
|
|
50
|
-
attributes: Record<string, unknown>;
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* Merge context layers with later layers winning at the attribute-key level.
|
|
54
|
-
* Layers are raw inputs; output is a deep copy (caller objects never retained).
|
|
55
|
-
*/
|
|
56
|
-
export declare function mergeContexts(...layers: Array<ContextInput | undefined>): {
|
|
57
|
-
targetingKey?: string;
|
|
58
|
-
attributes: Record<string, unknown>;
|
|
59
|
-
};
|
|
60
|
-
/**
|
|
61
|
-
* Resolved context for reporting/telemetry: `$`-prefixed system directives are
|
|
62
|
-
* filtered out; empty attributes omitted.
|
|
63
|
-
*/
|
|
64
|
-
export declare function resolvedContextView(ctx: CanonicalContext): {
|
|
65
|
-
targetingKey?: string;
|
|
66
|
-
attributes?: Record<string, JsonValue>;
|
|
67
|
-
};
|
package/dist/domain/context.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ALLOWED_FIREWEAVE_CONTEXT_KEYS = exports.DEFAULT_RESERVED_ATTRIBUTE_KEYS = exports.DEFAULT_CONTEXT_LIMITS = void 0;
|
|
4
|
-
exports.normalizeContextInput = normalizeContextInput;
|
|
5
|
-
exports.mergeContexts = mergeContexts;
|
|
6
|
-
exports.resolvedContextView = resolvedContextView;
|
|
7
|
-
exports.DEFAULT_CONTEXT_LIMITS = Object.freeze({
|
|
8
|
-
maxAttributeCount: 128,
|
|
9
|
-
maxKeyBytes: 256,
|
|
10
|
-
maxValueBytes: 4096,
|
|
11
|
-
maxNestingDepth: 6,
|
|
12
|
-
maxSerializedContextBytes: 65536,
|
|
13
|
-
});
|
|
14
|
-
/** Attribute keys callers may never set (in addition to the fireweave.* namespace). */
|
|
15
|
-
exports.DEFAULT_RESERVED_ATTRIBUTE_KEYS = Object.freeze([
|
|
16
|
-
'targetingKey',
|
|
17
|
-
'kind',
|
|
18
|
-
]);
|
|
19
|
-
/**
|
|
20
|
-
* Sanctioned fireweave.* carriers (orchestrator rulings 12–14): the ONLY
|
|
21
|
-
* `fireweave.*` context keys callers may set. They are the canonical spelling
|
|
22
|
-
* for group memberships / group properties; plain `groups`/`groupProperties`
|
|
23
|
-
* remain accepted as a documented alias.
|
|
24
|
-
*/
|
|
25
|
-
exports.ALLOWED_FIREWEAVE_CONTEXT_KEYS = Object.freeze([
|
|
26
|
-
'fireweave.groups',
|
|
27
|
-
'fireweave.groupProperties',
|
|
28
|
-
]);
|
|
29
|
-
/**
|
|
30
|
-
* Extract {targetingKey, attributes} from a caller context. Supports both the
|
|
31
|
-
* canonical shape ({targetingKey, attributes:{...}}) and OpenFeature flat shape
|
|
32
|
-
* ({targetingKey, plan:"pro", ...}).
|
|
33
|
-
*/
|
|
34
|
-
function normalizeContextInput(input) {
|
|
35
|
-
if (input === undefined || input === null)
|
|
36
|
-
return { attributes: {} };
|
|
37
|
-
const attributes = {};
|
|
38
|
-
let targetingKey;
|
|
39
|
-
if (typeof input.targetingKey === 'string' && input.targetingKey.length > 0) {
|
|
40
|
-
targetingKey = input.targetingKey;
|
|
41
|
-
}
|
|
42
|
-
if (input.attributes !== undefined && typeof input.attributes === 'object' && input.attributes !== null && !Array.isArray(input.attributes)) {
|
|
43
|
-
for (const [k, v] of Object.entries(input.attributes)) {
|
|
44
|
-
if (v !== undefined)
|
|
45
|
-
attributes[k] = v;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
for (const [k, v] of Object.entries(input)) {
|
|
49
|
-
if (k === 'targetingKey' || k === 'attributes')
|
|
50
|
-
continue;
|
|
51
|
-
if (v !== undefined)
|
|
52
|
-
attributes[k] = v;
|
|
53
|
-
}
|
|
54
|
-
return targetingKey !== undefined ? { targetingKey, attributes } : { attributes };
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Merge context layers with later layers winning at the attribute-key level.
|
|
58
|
-
* Layers are raw inputs; output is a deep copy (caller objects never retained).
|
|
59
|
-
*/
|
|
60
|
-
function mergeContexts(...layers) {
|
|
61
|
-
let targetingKey;
|
|
62
|
-
const attributes = {};
|
|
63
|
-
for (const layer of layers) {
|
|
64
|
-
const { targetingKey: tk, attributes: attrs } = normalizeContextInput(layer);
|
|
65
|
-
if (tk !== undefined)
|
|
66
|
-
targetingKey = tk;
|
|
67
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
68
|
-
attributes[k] = v;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return targetingKey !== undefined ? { targetingKey, attributes } : { attributes };
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Resolved context for reporting/telemetry: `$`-prefixed system directives are
|
|
75
|
-
* filtered out; empty attributes omitted.
|
|
76
|
-
*/
|
|
77
|
-
function resolvedContextView(ctx) {
|
|
78
|
-
const attributes = {};
|
|
79
|
-
for (const [k, v] of Object.entries(ctx.attributes)) {
|
|
80
|
-
if (k.startsWith('$'))
|
|
81
|
-
continue;
|
|
82
|
-
attributes[k] = v;
|
|
83
|
-
}
|
|
84
|
-
const out = {};
|
|
85
|
-
if (ctx.targetingKey !== undefined)
|
|
86
|
-
out.targetingKey = ctx.targetingKey;
|
|
87
|
-
if (Object.keys(attributes).length > 0)
|
|
88
|
-
out.attributes = attributes;
|
|
89
|
-
return out;
|
|
90
|
-
}
|
package/dist/domain/errors.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fireweave canonical error taxonomy (spec/errors.schema.json, contracts/errors.json).
|
|
3
|
-
*
|
|
4
|
-
* 15 PascalCase kinds mapped to OpenFeature error codes. Rules:
|
|
5
|
-
* - defaults never throw: evaluation paths surface errors via Decision, not exceptions;
|
|
6
|
-
* - no secrets in messages: all outward-facing messages are redacted;
|
|
7
|
-
* - cause preserved internally (`FireweaveError.cause`), never serialized outward.
|
|
8
|
-
*/
|
|
9
|
-
export type FireweaveErrorKind = 'NotReady' | 'ControlPointNotFound' | 'TypeMismatch' | 'InvalidContext' | 'Authentication' | 'Authorization' | 'RateLimited' | 'Timeout' | 'Network' | 'BackendUnavailable' | 'MalformedResponse' | 'UnsupportedCapability' | 'Configuration' | 'AlreadyClosed' | 'Internal';
|
|
10
|
-
/** OpenFeature error code strings (mirrors @openfeature/core ErrorCode values). */
|
|
11
|
-
export type OpenFeatureErrorCode = 'PROVIDER_NOT_READY' | 'PROVIDER_FATAL' | 'FLAG_NOT_FOUND' | 'PARSE_ERROR' | 'TYPE_MISMATCH' | 'TARGETING_KEY_MISSING' | 'INVALID_CONTEXT' | 'GENERAL';
|
|
12
|
-
export interface ErrorKindSpec {
|
|
13
|
-
readonly kind: FireweaveErrorKind;
|
|
14
|
-
readonly openFeatureErrorCode: OpenFeatureErrorCode;
|
|
15
|
-
readonly retryable: boolean;
|
|
16
|
-
readonly errorClass: 'transient' | 'permanent';
|
|
17
|
-
readonly defaultMessage: string;
|
|
18
|
-
}
|
|
19
|
-
export declare const ERROR_TAXONOMY: Readonly<Record<FireweaveErrorKind, ErrorKindSpec>>;
|
|
20
|
-
/**
|
|
21
|
-
* Scrub secrets per the redaction contract, then collapse whitespace runs and
|
|
22
|
-
* trim (so a message stays on one log line).
|
|
23
|
-
*/
|
|
24
|
-
export declare function redactSecrets(text: string): string;
|
|
25
|
-
export interface FireweaveErrorOptions {
|
|
26
|
-
/** Custom message; will be redacted. Defaults to the taxonomy default message. */
|
|
27
|
-
message?: string;
|
|
28
|
-
/** Underlying cause, preserved internally only. */
|
|
29
|
-
cause?: unknown;
|
|
30
|
-
/** Override the OF code (e.g. TARGETING_KEY_MISSING for InvalidContext). */
|
|
31
|
-
openFeatureErrorCode?: OpenFeatureErrorCode;
|
|
32
|
-
/** Extra fireweave.* flag metadata to surface with error decisions. */
|
|
33
|
-
metadata?: Record<string, string | number | boolean>;
|
|
34
|
-
}
|
|
35
|
-
export declare class FireweaveError extends Error {
|
|
36
|
-
readonly kind: FireweaveErrorKind;
|
|
37
|
-
readonly openFeatureErrorCode: OpenFeatureErrorCode;
|
|
38
|
-
readonly retryable: boolean;
|
|
39
|
-
readonly errorClass: 'transient' | 'permanent';
|
|
40
|
-
/** Deterministic outward-facing message from the taxonomy. */
|
|
41
|
-
readonly safeMessage: string;
|
|
42
|
-
readonly metadata: Readonly<Record<string, string | number | boolean>>;
|
|
43
|
-
constructor(kind: FireweaveErrorKind, options?: FireweaveErrorOptions);
|
|
44
|
-
}
|
|
45
|
-
export declare function isFireweaveError(err: unknown): err is FireweaveError;
|