@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,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;
|