@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.
Files changed (55) hide show
  1. package/README.md +2 -54
  2. package/package.json +4 -56
  3. package/LICENSE +0 -21
  4. package/dist/application/client.d.ts +0 -77
  5. package/dist/application/client.js +0 -144
  6. package/dist/application/mode.d.ts +0 -70
  7. package/dist/application/mode.js +0 -74
  8. package/dist/application/ports.d.ts +0 -94
  9. package/dist/application/ports.js +0 -2
  10. package/dist/application/runtime.d.ts +0 -101
  11. package/dist/application/runtime.js +0 -346
  12. package/dist/domain/context.d.ts +0 -67
  13. package/dist/domain/context.js +0 -90
  14. package/dist/domain/errors.d.ts +0 -45
  15. package/dist/domain/errors.js +0 -99
  16. package/dist/domain/target.d.ts +0 -2
  17. package/dist/domain/target.js +0 -2
  18. package/dist/domain/types.d.ts +0 -107
  19. package/dist/domain/types.js +0 -2
  20. package/dist/domain/validation.d.ts +0 -124
  21. package/dist/domain/validation.js +0 -355
  22. package/dist/index.d.ts +0 -34
  23. package/dist/index.js +0 -52
  24. package/dist/infrastructure/adapters/inmemory.d.ts +0 -77
  25. package/dist/infrastructure/adapters/inmemory.js +0 -151
  26. package/dist/infrastructure/adapters/local.d.ts +0 -108
  27. package/dist/infrastructure/adapters/local.js +0 -105
  28. package/dist/infrastructure/adapters/remote.d.ts +0 -74
  29. package/dist/infrastructure/adapters/remote.js +0 -373
  30. package/dist/infrastructure/hosts.d.ts +0 -18
  31. package/dist/infrastructure/hosts.js +0 -72
  32. package/dist/register-browser.d.ts +0 -1
  33. package/dist/register-browser.js +0 -7
  34. package/dist/register.d.ts +0 -1
  35. package/dist/register.js +0 -13
  36. package/dist/start/browser.d.ts +0 -12
  37. package/dist/start/browser.js +0 -36
  38. package/dist/start/build-info.d.ts +0 -2
  39. package/dist/start/build-info.js +0 -9
  40. package/dist/start/control-points.d.ts +0 -20
  41. package/dist/start/control-points.js +0 -59
  42. package/dist/start/env.d.ts +0 -11
  43. package/dist/start/env.js +0 -64
  44. package/dist/start/fw.d.ts +0 -29
  45. package/dist/start/fw.js +0 -76
  46. package/dist/start/index.d.ts +0 -23
  47. package/dist/start/index.js +0 -28
  48. package/dist/start/instance.d.ts +0 -17
  49. package/dist/start/instance.js +0 -56
  50. package/dist/start/names.d.ts +0 -22
  51. package/dist/start/names.js +0 -25
  52. package/dist/start/resolve.d.ts +0 -40
  53. package/dist/start/resolve.js +0 -139
  54. package/dist/start/state.d.ts +0 -96
  55. package/dist/start/state.js +0 -303
@@ -1,108 +0,0 @@
1
- /**
2
- * Local development adapter — the DEV substrate for a scaffolded harness.
3
- *
4
- * It is the counterpart to {@link FireweaveRemoteAdapter}: prod evaluates
5
- * control points against fw-server, dev evaluates them here, in-process, with
6
- * no network and no credentials. Because it satisfies the same
7
- * {@link BackendAdapter} port, the dev branch of a harness runs through the
8
- * same {@link FireweaveRuntime} as prod — inheriting identical lifecycle
9
- * gating and context canonicalization. That symmetry is the point: dev/prod
10
- * skew in the harness is exactly what the harness exists to prevent.
11
- *
12
- * Resolution policy is deliberately minimal:
13
- *
14
- * - a key present in `devFlags` resolves to its mapped value with reason
15
- * `STATIC` — the only supported way to turn a control point ON (or force it
16
- * OFF) on a laptop;
17
- * - every other key MISSES (`{ found: false, reason: 'DEFAULT' }`), which the
18
- * runtime turns into the caller's own default with reason `DEFAULT` — not
19
- * an error (spec/modes.md "Behaviour per mode": local's unknown-key row is
20
- * deliberately `default`/`DEFAULT`, unlike remote's `default`/`ERROR`/
21
- * `ControlPointNotFound`).
22
- *
23
- * Call-site defaults stay `false` under RAMP-1. Never write
24
- * `fw.flag(key, true)` to dogfood locally — that same `true` is the production
25
- * fallback when the control point is absent from the provider, so it silently
26
- * ships the ON branch. Use `devFlags` instead.
27
- *
28
- * Runtime portability: pure computation, no I/O, no env, no Node globals — so
29
- * it runs unchanged on Node, Bun, and Deno (ADR-0008), and is covered by
30
- * `test/unit/runtime-portability.test.ts`.
31
- */
32
- import type { AdapterResolution, AdapterRuntimeFeatures, BackendAdapter, RegisterTargetOptions, RegisterTargetResult, ResolveOptions } from '../../application/ports.js';
33
- import type { TargetKind } from '../../domain/target.js';
34
- import type { CanonicalContext } from '../../domain/types.js';
35
- export interface FireweaveLocalAdapterOptions {
36
- /**
37
- * Per-key boolean overrides. A present key resolves to its mapped value with
38
- * reason `STATIC`; an absent key misses so the caller's default is used.
39
- */
40
- readonly devFlags?: Record<string, boolean>;
41
- /**
42
- * Sink for the local `registerTarget` trace. Defaults to `console.info`.
43
- * Injectable so tests assert the call without capturing stdout, and so a
44
- * host that owns its logging can route it.
45
- */
46
- readonly log?: (message: string) => void;
47
- }
48
- /** A target recorded by {@link FireweaveLocalAdapter.registerTarget}. */
49
- export interface LocalRegisteredTarget {
50
- readonly targetingKey: string;
51
- readonly kind: TargetKind;
52
- readonly properties: Record<string, unknown>;
53
- readonly environment?: string;
54
- }
55
- export declare class FireweaveLocalAdapter implements BackendAdapter {
56
- /**
57
- * Not `inmemory` — that name belongs to {@link InMemoryAdapter}, the fixture
58
- * adapter conformance runs against. This is a distinct dev substrate, and
59
- * `capabilities.get()` should report it as such rather than impersonate one.
60
- */
61
- readonly name: "other";
62
- private readonly devFlags;
63
- private readonly log;
64
- private readonly targets;
65
- private closed;
66
- constructor(options?: FireweaveLocalAdapterOptions);
67
- initialize(_signal?: AbortSignal): Promise<void>;
68
- /**
69
- * A `devFlags` hit reports `enabled: true` alongside `reason: 'STATIC'`.
70
- * Reporting `enabled: false` for an override of `false` would make the
71
- * runtime label the decision `DISABLED` (runtime.ts, reason resolution),
72
- * which means "the control point exists but is switched off upstream" — not
73
- * what a local override expresses.
74
- *
75
- * `flagType: 'boolean'` is declared honestly. Reading an overridden key as a
76
- * string or number therefore yields TYPE_MISMATCH rather than silently
77
- * handing back the default: `devFlags` is `Record<string, boolean>`, so such
78
- * a read is a genuine call-site mistake and is better surfaced than hidden.
79
- *
80
- * A miss carries `reason: 'DEFAULT'` on the `{ found: false }` resolution —
81
- * the signal `FireweaveRuntime.evaluate` (runtime.ts) reads to return the
82
- * caller's default with reason `DEFAULT` instead of falling through to its
83
- * generic ControlPointNotFound/ERROR mapping (spec/modes.md).
84
- */
85
- resolve(controlPointKey: string, _context: CanonicalContext, _options?: ResolveOptions): Promise<AdapterResolution>;
86
- /**
87
- * Records the target in-process and traces it, rather than reporting
88
- * `UnsupportedCapability`.
89
- *
90
- * The capability previously reported Unsupported so that a dev harness could
91
- * not *silently* look registered — the failure being guarded against is a
92
- * developer believing their targeting works because nothing objected. A
93
- * recorded target plus an explicit `[fireweave:local]` line preserves that
94
- * guarantee by a different route: nothing is silent, and local dev can now
95
- * exercise targeting rules offline instead of only in production.
96
- *
97
- * The trace names the mode, so a line appearing in a production log is
98
- * itself the signal that something booted in local mode by mistake.
99
- *
100
- * No network call is made and nothing reaches fw-server.
101
- */
102
- registerTarget(targetingKey: string, options?: RegisterTargetOptions): Promise<RegisterTargetResult>;
103
- /** Targets recorded this process, for assertions and dev inspection. */
104
- getRegisteredTargets(): readonly LocalRegisteredTarget[];
105
- shutdown(): Promise<void>;
106
- isClosed(): boolean;
107
- features(): AdapterRuntimeFeatures;
108
- }
@@ -1,105 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FireweaveLocalAdapter = void 0;
4
- class FireweaveLocalAdapter {
5
- /**
6
- * Not `inmemory` — that name belongs to {@link InMemoryAdapter}, the fixture
7
- * adapter conformance runs against. This is a distinct dev substrate, and
8
- * `capabilities.get()` should report it as such rather than impersonate one.
9
- */
10
- name = 'other';
11
- devFlags;
12
- log;
13
- targets = new Map();
14
- closed = false;
15
- constructor(options = {}) {
16
- this.devFlags = { ...(options.devFlags ?? {}) };
17
- this.log = options.log ?? ((m) => console.info(m));
18
- }
19
- async initialize(_signal) {
20
- // Nothing to connect to. Kept async to satisfy the port.
21
- }
22
- /**
23
- * A `devFlags` hit reports `enabled: true` alongside `reason: 'STATIC'`.
24
- * Reporting `enabled: false` for an override of `false` would make the
25
- * runtime label the decision `DISABLED` (runtime.ts, reason resolution),
26
- * which means "the control point exists but is switched off upstream" — not
27
- * what a local override expresses.
28
- *
29
- * `flagType: 'boolean'` is declared honestly. Reading an overridden key as a
30
- * string or number therefore yields TYPE_MISMATCH rather than silently
31
- * handing back the default: `devFlags` is `Record<string, boolean>`, so such
32
- * a read is a genuine call-site mistake and is better surfaced than hidden.
33
- *
34
- * A miss carries `reason: 'DEFAULT'` on the `{ found: false }` resolution —
35
- * the signal `FireweaveRuntime.evaluate` (runtime.ts) reads to return the
36
- * caller's default with reason `DEFAULT` instead of falling through to its
37
- * generic ControlPointNotFound/ERROR mapping (spec/modes.md).
38
- */
39
- async resolve(controlPointKey, _context, _options) {
40
- const override = this.devFlags[controlPointKey];
41
- if (override === undefined)
42
- return { found: false, reason: 'DEFAULT' };
43
- return {
44
- found: true,
45
- enabled: true,
46
- value: override,
47
- variant: override ? 'on' : 'off',
48
- reason: 'STATIC',
49
- flagType: 'boolean',
50
- };
51
- }
52
- /**
53
- * Records the target in-process and traces it, rather than reporting
54
- * `UnsupportedCapability`.
55
- *
56
- * The capability previously reported Unsupported so that a dev harness could
57
- * not *silently* look registered — the failure being guarded against is a
58
- * developer believing their targeting works because nothing objected. A
59
- * recorded target plus an explicit `[fireweave:local]` line preserves that
60
- * guarantee by a different route: nothing is silent, and local dev can now
61
- * exercise targeting rules offline instead of only in production.
62
- *
63
- * The trace names the mode, so a line appearing in a production log is
64
- * itself the signal that something booted in local mode by mistake.
65
- *
66
- * No network call is made and nothing reaches fw-server.
67
- */
68
- async registerTarget(targetingKey, options = {}) {
69
- const kind = options.kind ?? 'user';
70
- const properties = { ...(options.properties ?? {}) };
71
- const target = {
72
- targetingKey,
73
- kind,
74
- properties,
75
- ...(options.environment === undefined ? {} : { environment: options.environment }),
76
- };
77
- this.targets.set(targetingKey, target);
78
- this.log(`[fireweave:local] registerTarget ${kind} ${targetingKey} ` +
79
- `${JSON.stringify(properties)} — recorded in-process, NOT sent to fw-server`);
80
- return { ok: true };
81
- }
82
- /** Targets recorded this process, for assertions and dev inspection. */
83
- getRegisteredTargets() {
84
- return [...this.targets.values()];
85
- }
86
- async shutdown() {
87
- this.closed = true;
88
- }
89
- isClosed() {
90
- return this.closed;
91
- }
92
- features() {
93
- return {
94
- remoteEvaluation: false,
95
- localEvaluation: true,
96
- localOnly: true,
97
- // No exposure sink exists locally; claiming otherwise would make
98
- // capabilities.get() advertise emission that silently goes nowhere.
99
- exposureEmission: false,
100
- sideEffectFreeReads: true,
101
- groupAnalytics: false,
102
- };
103
- }
104
- }
105
- exports.FireweaveLocalAdapter = FireweaveLocalAdapter;
@@ -1,74 +0,0 @@
1
- import type { AdapterResolution, AdapterRuntimeFeatures, BackendAdapter, RegisterTargetOptions, RegisterTargetResult, ResolveOptions } from '../../application/ports.js';
2
- import type { CanonicalContext, Exposure, Signal } from '../../domain/types.js';
3
- type FetchLike = (url: string, init?: {
4
- method?: string;
5
- headers?: Record<string, string>;
6
- body?: string;
7
- signal?: AbortSignal;
8
- }) => Promise<{
9
- status: number;
10
- text(): Promise<string>;
11
- json(): Promise<unknown>;
12
- }>;
13
- export interface FireweaveRemoteAdapterOptions {
14
- /**
15
- * fw-server base URL (e.g. https://fw.example.com or http://127.0.0.1:3901).
16
- * Required — never read from the environment (spec/modes.md "The SDK reads
17
- * no environment variables. Credentials arrive as explicit options.").
18
- */
19
- apiUrl?: string;
20
- /**
21
- * Fireweave project/runtime key (project-api-key_…).
22
- * Required — never read from the environment (spec/modes.md "The SDK reads
23
- * no environment variables. Credentials arrive as explicit options.").
24
- */
25
- apiKey?: string;
26
- /**
27
- * SSRF allowlist override. Default: hostname of `apiUrl` + loopback.
28
- * Pass `['*']` to opt out of host pinning.
29
- */
30
- allowedHosts?: readonly string[];
31
- /** Per-request timeout for evaluate/capture (default 3000 ms). */
32
- requestTimeoutMs?: number;
33
- /** Deadline for flush during shutdown (default 10 000 ms). */
34
- shutdownTimeoutMs?: number;
35
- /** Injected fetch (tests). */
36
- fetch?: FetchLike;
37
- }
38
- export declare class FireweaveRemoteAdapter implements BackendAdapter {
39
- readonly name: "fireweave";
40
- private readonly options;
41
- private apiUrl;
42
- private apiKey;
43
- private closed;
44
- private ready;
45
- private readonly pending;
46
- private readonly fetchImpl;
47
- constructor(options?: FireweaveRemoteAdapterOptions);
48
- initialize(_signal?: AbortSignal): Promise<void>;
49
- resolve(controlPointKey: string, context: CanonicalContext, options?: ResolveOptions): Promise<AdapterResolution>;
50
- /**
51
- * Register a user or device so flag rules can target its DURABLE properties.
52
- *
53
- * Call once per login / device provisioning, then send the same
54
- * `targetingKey` on evaluate. Per-request `attributes` still override the
55
- * stored properties for a single evaluation — the two identity paths compose
56
- * (see spec/remote-protocol.md § Two identity paths).
57
- *
58
- * Never throws for transport failures: registration sits in login paths, and
59
- * an analytics call must not break sign-in. Retried ONCE when the error
60
- * taxonomy marks the failure retryable (network / timeout / backend); a
61
- * rejected payload or bad key is not retried, since it would be rejected
62
- * identically. The result object reports what happened for callers that
63
- * want to log it.
64
- */
65
- registerTarget(targetingKey: string, options?: RegisterTargetOptions): Promise<RegisterTargetResult>;
66
- recordExposure(exposure: Exposure): void;
67
- recordSignal(signal: Signal): void;
68
- flush(): Promise<void>;
69
- shutdown(): Promise<void>;
70
- features(): AdapterRuntimeFeatures;
71
- private toResolution;
72
- private requestJson;
73
- }
74
- export {};
@@ -1,373 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FireweaveRemoteAdapter = void 0;
4
- /**
5
- * Fireweave remote backend adapter (ADR-0005) — **default production path**.
6
- *
7
- * Speaks only the vendor-neutral Fireweave remote protocol to fw-server:
8
- * POST /v1/control-points/evaluate
9
- * POST /v1/capture
10
- * POST /v1/targets/register
11
- *
12
- * Auth: Authorization: Bearer <project key> (project-api-key_…).
13
- * Which backend fw-server forwards to is fw-server's concern: no vendor SDK,
14
- * key, or host ever enters the application process.
15
- *
16
- * See spec/remote-protocol.md.
17
- */
18
- const errors_js_1 = require("../../domain/errors.js");
19
- const hosts_js_1 = require("../hosts.js");
20
- const validation_js_1 = require("../../domain/validation.js");
21
- const DEFAULT_ADAPTER_SHUTDOWN_TIMEOUT_MS = 10000;
22
- const DEFAULT_REQUEST_TIMEOUT_MS = 3000;
23
- const EVALUATE_PATH = '/v1/control-points/evaluate';
24
- const CAPTURE_PATH = '/v1/capture';
25
- const REGISTER_TARGET_PATH = '/v1/targets/register';
26
- function mapHttpStatus(status) {
27
- if (status === 401)
28
- return new errors_js_1.FireweaveError('Authentication');
29
- if (status === 403)
30
- return new errors_js_1.FireweaveError('Authorization');
31
- if (status === 429)
32
- return new errors_js_1.FireweaveError('RateLimited');
33
- if (status >= 500)
34
- return new errors_js_1.FireweaveError('BackendUnavailable');
35
- return new errors_js_1.FireweaveError('BackendUnavailable');
36
- }
37
- /**
38
- * `apiUrl`/`apiKey` come from `options` only — never the environment
39
- * (spec/modes.md "The SDK reads no environment variables. Credentials
40
- * arrive as explicit options.", unscoped).
41
- */
42
- function resolveCredentials(options) {
43
- const apiUrl = (options.apiUrl ?? '').replace(/\/+$/, '');
44
- const apiKey = options.apiKey ?? '';
45
- return { apiUrl, apiKey };
46
- }
47
- function defaultAllowedHostsFor(apiUrl) {
48
- try {
49
- const hostname = new URL(apiUrl).hostname;
50
- const hosts = [hostname, 'localhost', '127.0.0.1', '::1'];
51
- if (hostname === '[::1]')
52
- hosts.push('::1');
53
- return hosts;
54
- }
55
- catch {
56
- return ['localhost', '127.0.0.1', '::1'];
57
- }
58
- }
59
- function reasonToAdapter(reason) {
60
- const allowed = new Set([
61
- 'STATIC',
62
- 'DEFAULT',
63
- 'TARGETING_MATCH',
64
- 'SPLIT',
65
- 'CACHED',
66
- 'STALE',
67
- 'DISABLED',
68
- 'UNKNOWN',
69
- 'ERROR',
70
- ]);
71
- return allowed.has(reason) ? reason : undefined;
72
- }
73
- class FireweaveRemoteAdapter {
74
- name = 'fireweave';
75
- options;
76
- apiUrl = '';
77
- apiKey = '';
78
- closed = false;
79
- ready = false;
80
- pending = [];
81
- fetchImpl;
82
- constructor(options = {}) {
83
- this.options = { ...options };
84
- this.fetchImpl = options.fetch ?? globalThis.fetch;
85
- }
86
- async initialize(_signal) {
87
- if (this.closed)
88
- throw new errors_js_1.FireweaveError('AlreadyClosed');
89
- const { apiUrl, apiKey } = resolveCredentials(this.options);
90
- if (!apiUrl || !apiKey) {
91
- throw new errors_js_1.FireweaveError('Configuration');
92
- }
93
- let url;
94
- try {
95
- url = new URL(apiUrl);
96
- }
97
- catch {
98
- throw new errors_js_1.FireweaveError('Configuration');
99
- }
100
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
101
- throw new errors_js_1.FireweaveError('Configuration');
102
- }
103
- if (url.protocol === 'http:' && !(0, hosts_js_1.isLoopbackHostname)(url.hostname)) {
104
- throw new errors_js_1.FireweaveError('Configuration');
105
- }
106
- const allow = this.options.allowedHosts !== undefined && this.options.allowedHosts.length > 0
107
- ? this.options.allowedHosts
108
- : defaultAllowedHostsFor(apiUrl);
109
- (0, hosts_js_1.assertHostAllowed)(apiUrl, allow);
110
- this.apiUrl = apiUrl.replace(/\/+$/, '');
111
- this.apiKey = apiKey;
112
- this.ready = true;
113
- }
114
- async resolve(controlPointKey, context, options) {
115
- if (this.closed)
116
- throw new errors_js_1.FireweaveError('AlreadyClosed');
117
- if (!this.ready)
118
- throw new errors_js_1.FireweaveError('NotReady');
119
- const targetingKeyResult = (0, validation_js_1.validateTargetingKey)(context.targetingKey, true);
120
- if (!targetingKeyResult.ok)
121
- throw targetingKeyResult.error;
122
- const targetingKey = targetingKeyResult.value ?? '';
123
- const attributes = {};
124
- for (const [k, v] of Object.entries(context.attributes)) {
125
- if (k === 'groups' || k === 'groupProperties' || k.startsWith('$') || k.startsWith('fireweave.')) {
126
- continue;
127
- }
128
- attributes[k] = v;
129
- }
130
- const body = {
131
- targetingKey,
132
- controlPointKeys: [controlPointKey],
133
- };
134
- if (Object.keys(attributes).length > 0)
135
- body['attributes'] = attributes;
136
- if (context.groups !== undefined)
137
- body['groups'] = context.groups;
138
- if (context.groupProperties !== undefined)
139
- body['groupProperties'] = context.groupProperties;
140
- const response = await this.requestJson(EVALUATE_PATH, body, options?.signal);
141
- const item = response.decisions?.find((d) => d.controlPointKey === controlPointKey);
142
- if (item === undefined) {
143
- const missing = { found: false };
144
- if (response.quotaLimited === true)
145
- missing.quotaLimited = true;
146
- return missing;
147
- }
148
- return this.toResolution(item, response.quotaLimited === true);
149
- }
150
- /**
151
- * Register a user or device so flag rules can target its DURABLE properties.
152
- *
153
- * Call once per login / device provisioning, then send the same
154
- * `targetingKey` on evaluate. Per-request `attributes` still override the
155
- * stored properties for a single evaluation — the two identity paths compose
156
- * (see spec/remote-protocol.md § Two identity paths).
157
- *
158
- * Never throws for transport failures: registration sits in login paths, and
159
- * an analytics call must not break sign-in. Retried ONCE when the error
160
- * taxonomy marks the failure retryable (network / timeout / backend); a
161
- * rejected payload or bad key is not retried, since it would be rejected
162
- * identically. The result object reports what happened for callers that
163
- * want to log it.
164
- */
165
- async registerTarget(targetingKey, options = {}) {
166
- if (this.closed)
167
- return { ok: false, error: new errors_js_1.FireweaveError('AlreadyClosed') };
168
- if (!this.ready)
169
- return { ok: false, error: new errors_js_1.FireweaveError('NotReady') };
170
- const targetingKeyResult = (0, validation_js_1.validateTargetingKey)(targetingKey, true);
171
- if (!targetingKeyResult.ok)
172
- return { ok: false, error: targetingKeyResult.error };
173
- const body = { targetingKey };
174
- if (options.kind !== undefined)
175
- body['kind'] = options.kind;
176
- if (options.environment !== undefined)
177
- body['environment'] = options.environment;
178
- if (options.properties !== undefined && Object.keys(options.properties).length > 0) {
179
- body['properties'] = options.properties;
180
- }
181
- let lastError;
182
- for (let attempt = 0; attempt < 2; attempt += 1) {
183
- try {
184
- await this.requestJson(REGISTER_TARGET_PATH, body, options.signal);
185
- return { ok: true };
186
- }
187
- catch (err) {
188
- lastError =
189
- err instanceof errors_js_1.FireweaveError ? err : new errors_js_1.FireweaveError('BackendUnavailable');
190
- if (!lastError.retryable)
191
- break;
192
- }
193
- }
194
- return { ok: false, ...(lastError !== undefined ? { error: lastError } : {}) };
195
- }
196
- recordExposure(exposure) {
197
- if (this.closed || !this.ready)
198
- return;
199
- const event = {
200
- type: 'exposure',
201
- targetingKey: exposure.targetingKey,
202
- controlPointKey: exposure.controlPointKey,
203
- value: exposure.value,
204
- };
205
- if (exposure.variant !== undefined)
206
- event.variant = exposure.variant;
207
- if (exposure.rolloutId !== undefined)
208
- event.rolloutId = exposure.rolloutId;
209
- if (exposure.changeId !== undefined)
210
- event.changeId = exposure.changeId;
211
- if (exposure.stampId !== undefined)
212
- event.stampId = exposure.stampId;
213
- this.pending.push(event);
214
- }
215
- recordSignal(signal) {
216
- if (this.closed || !this.ready)
217
- return;
218
- const event = {
219
- type: 'signal',
220
- targetingKey: signal.targetingKey ?? signal.rolloutId ?? 'fireweave-sdk',
221
- name: signal.name,
222
- properties: {
223
- kind: signal.kind,
224
- ...(signal.status !== undefined ? { status: signal.status } : {}),
225
- ...(signal.errorKind !== undefined ? { errorKind: signal.errorKind } : {}),
226
- ...(signal.message !== undefined ? { message: signal.message } : {}),
227
- ...(signal.value !== undefined ? { value: signal.value } : {}),
228
- },
229
- };
230
- if (signal.controlPointKey !== undefined)
231
- event.controlPointKey = signal.controlPointKey;
232
- if (signal.variant !== undefined)
233
- event.variant = signal.variant;
234
- if (signal.rolloutId !== undefined)
235
- event.rolloutId = signal.rolloutId;
236
- if (signal.changeId !== undefined)
237
- event.changeId = signal.changeId;
238
- if (signal.stampId !== undefined)
239
- event.stampId = signal.stampId;
240
- if (signal.timestamp !== undefined)
241
- event.timestamp = signal.timestamp;
242
- this.pending.push(event);
243
- }
244
- async flush() {
245
- if (this.closed || !this.ready || this.pending.length === 0)
246
- return;
247
- const batch = this.pending.splice(0, this.pending.length);
248
- try {
249
- await this.requestJson(CAPTURE_PATH, { events: batch });
250
- }
251
- catch {
252
- // flush failures must not throw into shutdown paths; re-queue best-effort
253
- this.pending.unshift(...batch);
254
- }
255
- }
256
- async shutdown() {
257
- if (this.closed)
258
- return;
259
- this.closed = true;
260
- // The race timer is cleared once the flush wins; left armed, it held the
261
- // process open for up to shutdownTimeoutMs after a clean shutdown.
262
- let timer;
263
- try {
264
- const timeout = this.options.shutdownTimeoutMs ?? DEFAULT_ADAPTER_SHUTDOWN_TIMEOUT_MS;
265
- await Promise.race([
266
- this.flush(),
267
- new Promise((resolve) => {
268
- timer = setTimeout(resolve, timeout);
269
- }),
270
- ]);
271
- }
272
- catch {
273
- // never throw from shutdown
274
- }
275
- finally {
276
- if (timer !== undefined)
277
- clearTimeout(timer);
278
- }
279
- this.ready = false;
280
- }
281
- features() {
282
- return {
283
- remoteEvaluation: true,
284
- localEvaluation: false,
285
- localOnly: false,
286
- exposureEmission: true,
287
- sideEffectFreeReads: true,
288
- groupAnalytics: true,
289
- };
290
- }
291
- toResolution(item, quotaLimited) {
292
- if (item.found === false) {
293
- const missing = { found: false };
294
- if (quotaLimited)
295
- missing.quotaLimited = true;
296
- return missing;
297
- }
298
- const resolution = {
299
- found: true,
300
- value: item.value,
301
- enabled: item.enabled ?? true,
302
- };
303
- if (item.variant != null)
304
- resolution.variant = item.variant;
305
- const reason = reasonToAdapter(item.reason);
306
- if (reason !== undefined)
307
- resolution.reason = reason;
308
- if (item.payload !== undefined)
309
- resolution.payload = item.payload;
310
- const meta = item.controlPointMetadata ?? {};
311
- if (typeof meta['fireweave.controlPointVersion'] === 'number') {
312
- resolution.version = meta['fireweave.controlPointVersion'];
313
- }
314
- if (typeof meta['fireweave.vendorControlPointId'] === 'number') {
315
- resolution.vendorControlPointId = meta['fireweave.vendorControlPointId'];
316
- }
317
- if (typeof meta['fireweave.reasonCode'] === 'string') {
318
- resolution.reasonCode = meta['fireweave.reasonCode'];
319
- }
320
- if (quotaLimited || meta['fireweave.quotaLimited'] === true) {
321
- resolution.quotaLimited = true;
322
- }
323
- return resolution;
324
- }
325
- async requestJson(path, body, outerSignal) {
326
- const timeoutMs = this.options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
327
- const controller = new AbortController();
328
- const timer = setTimeout(() => controller.abort(), timeoutMs);
329
- const onOuterAbort = () => controller.abort();
330
- if (outerSignal !== undefined) {
331
- if (outerSignal.aborted)
332
- controller.abort();
333
- else
334
- outerSignal.addEventListener('abort', onOuterAbort, { once: true });
335
- }
336
- try {
337
- let response;
338
- try {
339
- response = await this.fetchImpl(`${this.apiUrl}${path}`, {
340
- method: 'POST',
341
- headers: {
342
- 'content-type': 'application/json',
343
- authorization: `Bearer ${this.apiKey}`,
344
- },
345
- body: JSON.stringify(body),
346
- signal: controller.signal,
347
- });
348
- }
349
- catch (err) {
350
- const aborted = err instanceof Error && err.name === 'AbortError';
351
- throw aborted ? new errors_js_1.FireweaveError('Timeout') : new errors_js_1.FireweaveError('Network');
352
- }
353
- if (response.status >= 400) {
354
- throw mapHttpStatus(response.status);
355
- }
356
- let parsed;
357
- try {
358
- parsed = await response.json();
359
- }
360
- catch {
361
- throw new errors_js_1.FireweaveError('MalformedResponse');
362
- }
363
- return parsed;
364
- }
365
- finally {
366
- clearTimeout(timer);
367
- if (outerSignal !== undefined) {
368
- outerSignal.removeEventListener('abort', onOuterAbort);
369
- }
370
- }
371
- }
372
- }
373
- exports.FireweaveRemoteAdapter = FireweaveRemoteAdapter;
@@ -1,18 +0,0 @@
1
- /**
2
- * Canonical default host allowlist.
3
- *
4
- * v3 replaced the third-party analytics hostnames this list used to carry
5
- * (ADR-0006) with Fireweave's own fw-server hostnames. The security property is
6
- * unchanged — an unconfigured allowlist still denies everything it does not name
7
- * — but the *contents* changed: code doing
8
- * `allowedHosts: [...DEFAULT_ALLOWED_HOSTS, …]` no longer reaches the former
9
- * vendor endpoints, which is the intent.
10
- */
11
- export declare const DEFAULT_ALLOWED_HOSTS: readonly string[];
12
- /** True for loopback hostnames (http and test stubs are permitted here). */
13
- export declare function isLoopbackHostname(hostname: string): boolean;
14
- /**
15
- * Validate a backend host URL against the allowlist.
16
- * Throws FireweaveError('Configuration') — fixed message, no host echo.
17
- */
18
- export declare function assertHostAllowed(host: string, allowedHosts?: readonly string[]): void;