@fireweaveai/web-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 -51
- package/package.json +4 -66
- package/LICENSE +0 -21
- package/dist/application/client.d.ts +0 -104
- package/dist/application/client.js +0 -147
- package/dist/application/mode.d.ts +0 -99
- package/dist/application/mode.js +0 -101
- package/dist/application/ports.d.ts +0 -101
- package/dist/application/ports.js +0 -2
- package/dist/application/runtime.d.ts +0 -70
- package/dist/application/runtime.js +0 -348
- package/dist/build/node/define.d.ts +0 -14
- package/dist/build/node/define.js +0 -38
- package/dist/build/node/shared.d.ts +0 -19
- package/dist/build/node/shared.js +0 -39
- package/dist/build/node/vite.d.ts +0 -53
- package/dist/build/node/vite.js +0 -106
- package/dist/build/src/start/build-info.d.ts +0 -2
- package/dist/build/src/start/build-info.js +0 -9
- package/dist/build/src/start/names.d.ts +0 -38
- package/dist/build/src/start/names.js +0 -41
- package/dist/build/src/start/policy.d.ts +0 -70
- package/dist/build/src/start/policy.js +0 -128
- package/dist/domain/context.d.ts +0 -51
- package/dist/domain/context.js +0 -72
- package/dist/domain/errors.d.ts +0 -36
- package/dist/domain/errors.js +0 -97
- package/dist/domain/target.d.ts +0 -2
- package/dist/domain/target.js +0 -2
- package/dist/domain/types.d.ts +0 -89
- package/dist/domain/types.js +0 -10
- package/dist/domain/validation.d.ts +0 -128
- package/dist/domain/validation.js +0 -358
- package/dist/index.d.ts +0 -50
- package/dist/index.js +0 -66
- package/dist/infrastructure/adapters/inmemory.d.ts +0 -50
- package/dist/infrastructure/adapters/inmemory.js +0 -94
- package/dist/infrastructure/adapters/local.d.ts +0 -79
- package/dist/infrastructure/adapters/local.js +0 -98
- package/dist/infrastructure/adapters/remote.d.ts +0 -44
- package/dist/infrastructure/adapters/remote.js +0 -329
- package/dist/infrastructure/hosts.d.ts +0 -10
- package/dist/infrastructure/hosts.js +0 -84
- 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 -24
- package/dist/start/control-points.js +0 -61
- package/dist/start/fw.d.ts +0 -42
- package/dist/start/fw.js +0 -137
- package/dist/start/identity.d.ts +0 -19
- package/dist/start/identity.js +0 -93
- package/dist/start/index.d.ts +0 -34
- package/dist/start/index.js +0 -38
- package/dist/start/names.d.ts +0 -38
- package/dist/start/names.js +0 -41
- package/dist/start/policy.d.ts +0 -70
- package/dist/start/policy.js +0 -128
- package/dist/start/state.d.ts +0 -103
- package/dist/start/state.js +0 -375
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local development adapter for the browser — the DEV substrate of a
|
|
3
|
-
* scaffolded web harness, and the exact peer of the server SDK's
|
|
4
|
-
* `FireweaveLocalAdapter`.
|
|
5
|
-
*
|
|
6
|
-
* A key present in `devFlags` resolves with reason `STATIC`; every other key
|
|
7
|
-
* misses, which the runtime turns into the caller's own default. No network,
|
|
8
|
-
* no credentials, nothing to configure.
|
|
9
|
-
*
|
|
10
|
-
* Call-site defaults stay `false` under RAMP-1. Never write `flag(key, true)`
|
|
11
|
-
* to dogfood locally — that same `true` is the production fallback when a
|
|
12
|
-
* control point is absent from the provider, so it silently ships the ON
|
|
13
|
-
* branch. Use `devFlags`.
|
|
14
|
-
*/
|
|
15
|
-
import type { AdapterRuntimeFeatures, PrefetchOptions, PrefetchResult, RegisterTargetOptions, RegisterTargetResult, WebBackendAdapter } from '../../application/ports.js';
|
|
16
|
-
import type { TargetKind } from '../../domain/target.js';
|
|
17
|
-
import type { CanonicalContext } from '../../domain/types.js';
|
|
18
|
-
export interface FireweaveLocalWebAdapterOptions {
|
|
19
|
-
readonly devFlags?: Record<string, boolean>;
|
|
20
|
-
/**
|
|
21
|
-
* Sink for the `[fireweave:local]` registerTarget trace line
|
|
22
|
-
* (spec/modes.md "registerTarget in local mode"). Defaults to
|
|
23
|
-
* `console.info`.
|
|
24
|
-
*/
|
|
25
|
-
readonly log?: (message: string) => void;
|
|
26
|
-
}
|
|
27
|
-
/** A target recorded by {@link FireweaveLocalWebAdapter.registerTarget}. */
|
|
28
|
-
export interface LocalRegisteredTarget {
|
|
29
|
-
readonly targetingKey: string;
|
|
30
|
-
readonly kind: TargetKind;
|
|
31
|
-
readonly properties: Record<string, unknown>;
|
|
32
|
-
readonly environment?: string;
|
|
33
|
-
}
|
|
34
|
-
export declare class FireweaveLocalWebAdapter implements WebBackendAdapter {
|
|
35
|
-
/** Not `inmemory` — that name belongs to the fixture adapter. */
|
|
36
|
-
readonly name: "other";
|
|
37
|
-
/**
|
|
38
|
-
* spec/modes.md "Behaviour per mode": local's unknown-key row is
|
|
39
|
-
* `default`/reason `DEFAULT`, not an error. This is the strict `===` seam
|
|
40
|
-
* `FireweaveWebRuntime.evaluateSync` checks on a cache miss.
|
|
41
|
-
*/
|
|
42
|
-
readonly missReason: "DEFAULT";
|
|
43
|
-
private readonly devFlags;
|
|
44
|
-
private readonly log;
|
|
45
|
-
private readonly targets;
|
|
46
|
-
private closed;
|
|
47
|
-
constructor(options?: FireweaveLocalWebAdapterOptions);
|
|
48
|
-
initialize(_signal?: AbortSignal): Promise<void>;
|
|
49
|
-
/**
|
|
50
|
-
* `enabled: true` alongside `reason: 'STATIC'` is deliberate: reporting
|
|
51
|
-
* `enabled: false` for an override of `false` would make the runtime label
|
|
52
|
-
* the decision `DISABLED`, which means "exists but switched off upstream" —
|
|
53
|
-
* not what a local override expresses.
|
|
54
|
-
*/
|
|
55
|
-
prefetch(_context: CanonicalContext, _options?: PrefetchOptions): Promise<PrefetchResult>;
|
|
56
|
-
/**
|
|
57
|
-
* Records the target in-process and traces it, rather than reporting
|
|
58
|
-
* `UnsupportedCapability`.
|
|
59
|
-
*
|
|
60
|
-
* The capability could instead report Unsupported so a dev harness could
|
|
61
|
-
* not *silently* look registered — but the failure being guarded against
|
|
62
|
-
* is a developer believing their targeting works because nothing objected.
|
|
63
|
-
* A recorded target plus an explicit `[fireweave:local]` line preserves
|
|
64
|
-
* that guarantee by a different route: nothing is silent, and local dev
|
|
65
|
-
* can exercise targeting rules offline instead of only in production
|
|
66
|
-
* (spec/modes.md "registerTarget in local mode").
|
|
67
|
-
*
|
|
68
|
-
* The trace names the mode, so a line appearing in a production log is
|
|
69
|
-
* itself the signal that something booted in local mode by mistake.
|
|
70
|
-
*
|
|
71
|
-
* No network call is made and nothing reaches fw-server.
|
|
72
|
-
*/
|
|
73
|
-
registerTarget(targetingKey: string, options?: RegisterTargetOptions): Promise<RegisterTargetResult>;
|
|
74
|
-
/** Targets recorded this process, for assertions and dev inspection. */
|
|
75
|
-
getRegisteredTargets(): readonly LocalRegisteredTarget[];
|
|
76
|
-
shutdown(): Promise<void>;
|
|
77
|
-
isClosed(): boolean;
|
|
78
|
-
features(): AdapterRuntimeFeatures;
|
|
79
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FireweaveLocalWebAdapter = void 0;
|
|
4
|
-
class FireweaveLocalWebAdapter {
|
|
5
|
-
/** Not `inmemory` — that name belongs to the fixture adapter. */
|
|
6
|
-
name = 'other';
|
|
7
|
-
/**
|
|
8
|
-
* spec/modes.md "Behaviour per mode": local's unknown-key row is
|
|
9
|
-
* `default`/reason `DEFAULT`, not an error. This is the strict `===` seam
|
|
10
|
-
* `FireweaveWebRuntime.evaluateSync` checks on a cache miss.
|
|
11
|
-
*/
|
|
12
|
-
missReason = 'DEFAULT';
|
|
13
|
-
devFlags;
|
|
14
|
-
log;
|
|
15
|
-
targets = new Map();
|
|
16
|
-
closed = false;
|
|
17
|
-
constructor(options = {}) {
|
|
18
|
-
this.devFlags = { ...(options.devFlags ?? {}) };
|
|
19
|
-
this.log = options.log ?? ((m) => console.info(m));
|
|
20
|
-
}
|
|
21
|
-
async initialize(_signal) {
|
|
22
|
-
// Nothing to connect to.
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* `enabled: true` alongside `reason: 'STATIC'` is deliberate: reporting
|
|
26
|
-
* `enabled: false` for an override of `false` would make the runtime label
|
|
27
|
-
* the decision `DISABLED`, which means "exists but switched off upstream" —
|
|
28
|
-
* not what a local override expresses.
|
|
29
|
-
*/
|
|
30
|
-
async prefetch(_context, _options) {
|
|
31
|
-
const out = new Map();
|
|
32
|
-
for (const [key, value] of Object.entries(this.devFlags)) {
|
|
33
|
-
out.set(key, {
|
|
34
|
-
found: true,
|
|
35
|
-
enabled: true,
|
|
36
|
-
value,
|
|
37
|
-
variant: value ? 'on' : 'off',
|
|
38
|
-
reason: 'STATIC',
|
|
39
|
-
flagType: 'boolean',
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
return out;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Records the target in-process and traces it, rather than reporting
|
|
46
|
-
* `UnsupportedCapability`.
|
|
47
|
-
*
|
|
48
|
-
* The capability could instead report Unsupported so a dev harness could
|
|
49
|
-
* not *silently* look registered — but the failure being guarded against
|
|
50
|
-
* is a developer believing their targeting works because nothing objected.
|
|
51
|
-
* A recorded target plus an explicit `[fireweave:local]` line preserves
|
|
52
|
-
* that guarantee by a different route: nothing is silent, and local dev
|
|
53
|
-
* can exercise targeting rules offline instead of only in production
|
|
54
|
-
* (spec/modes.md "registerTarget in local mode").
|
|
55
|
-
*
|
|
56
|
-
* The trace names the mode, so a line appearing in a production log is
|
|
57
|
-
* itself the signal that something booted in local mode by mistake.
|
|
58
|
-
*
|
|
59
|
-
* No network call is made and nothing reaches fw-server.
|
|
60
|
-
*/
|
|
61
|
-
async registerTarget(targetingKey, options = {}) {
|
|
62
|
-
const kind = options.kind ?? 'user';
|
|
63
|
-
const properties = { ...(options.properties ?? {}) };
|
|
64
|
-
const target = {
|
|
65
|
-
targetingKey,
|
|
66
|
-
kind,
|
|
67
|
-
properties,
|
|
68
|
-
...(options.environment === undefined ? {} : { environment: options.environment }),
|
|
69
|
-
};
|
|
70
|
-
this.targets.set(targetingKey, target);
|
|
71
|
-
this.log(`[fireweave:local] registerTarget ${kind} ${targetingKey} ` +
|
|
72
|
-
`${JSON.stringify(properties)} — recorded in-process, NOT sent to fw-server`);
|
|
73
|
-
return { ok: true };
|
|
74
|
-
}
|
|
75
|
-
/** Targets recorded this process, for assertions and dev inspection. */
|
|
76
|
-
getRegisteredTargets() {
|
|
77
|
-
return [...this.targets.values()];
|
|
78
|
-
}
|
|
79
|
-
async shutdown() {
|
|
80
|
-
this.closed = true;
|
|
81
|
-
}
|
|
82
|
-
isClosed() {
|
|
83
|
-
return this.closed;
|
|
84
|
-
}
|
|
85
|
-
features() {
|
|
86
|
-
return {
|
|
87
|
-
remoteEvaluation: false,
|
|
88
|
-
localEvaluation: true,
|
|
89
|
-
localOnly: true,
|
|
90
|
-
// No exposure sink exists locally; claiming otherwise would advertise
|
|
91
|
-
// emission that silently goes nowhere.
|
|
92
|
-
exposureEmission: false,
|
|
93
|
-
sideEffectFreeReads: true,
|
|
94
|
-
groupAnalytics: false,
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
exports.FireweaveLocalWebAdapter = FireweaveLocalWebAdapter;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { AdapterRuntimeFeatures, PrefetchOptions, PrefetchResult, RegisterTargetOptions, RegisterTargetResult, WebBackendAdapter } from '../../application/ports.js';
|
|
2
|
-
import type { CanonicalContext, Exposure, Signal } from '../../domain/types.js';
|
|
3
|
-
export type FireweaveFetchLike = typeof fetch;
|
|
4
|
-
export interface FireweaveRemoteWebAdapterOptions {
|
|
5
|
-
/** fw-server base URL, e.g. `https://app-server.fireweave.ai`. */
|
|
6
|
-
readonly apiUrl: string;
|
|
7
|
-
/** Fireweave project key. Public by construction — must be scoped accordingly. */
|
|
8
|
-
readonly apiKey: string;
|
|
9
|
-
/** SSRF/misconfiguration allowlist override. `['*']` opts out. */
|
|
10
|
-
readonly allowedHosts?: readonly string[];
|
|
11
|
-
readonly requestTimeoutMs?: number;
|
|
12
|
-
/** Injected fetch (tests). Defaults to the global. */
|
|
13
|
-
readonly fetch?: FireweaveFetchLike;
|
|
14
|
-
}
|
|
15
|
-
export declare class FireweaveRemoteWebAdapter implements WebBackendAdapter {
|
|
16
|
-
readonly name: "fireweave";
|
|
17
|
-
private readonly apiUrl;
|
|
18
|
-
private readonly apiKey;
|
|
19
|
-
private readonly allowedHosts;
|
|
20
|
-
private readonly requestTimeoutMs;
|
|
21
|
-
private readonly fetchImpl;
|
|
22
|
-
private readonly pending;
|
|
23
|
-
private ready;
|
|
24
|
-
private closed;
|
|
25
|
-
constructor(options: FireweaveRemoteWebAdapterOptions);
|
|
26
|
-
initialize(_signal?: AbortSignal): Promise<void>;
|
|
27
|
-
prefetch(context: CanonicalContext, options?: PrefetchOptions): Promise<PrefetchResult>;
|
|
28
|
-
registerTarget(targetingKey: string, options?: RegisterTargetOptions): Promise<RegisterTargetResult>;
|
|
29
|
-
recordExposure(exposure: Exposure): void;
|
|
30
|
-
recordSignal(signal: Signal): void;
|
|
31
|
-
/**
|
|
32
|
-
* `beacon: true` is the unload path. It must not await anything the browser
|
|
33
|
-
* will cancel, so it uses `sendBeacon` when available and a `keepalive`
|
|
34
|
-
* fetch otherwise, and it never re-queues on failure — there is no later.
|
|
35
|
-
*/
|
|
36
|
-
flush(options?: {
|
|
37
|
-
beacon?: boolean;
|
|
38
|
-
}): Promise<void>;
|
|
39
|
-
private sendBeaconOrKeepalive;
|
|
40
|
-
shutdown(): Promise<void>;
|
|
41
|
-
features(): AdapterRuntimeFeatures;
|
|
42
|
-
private toResolution;
|
|
43
|
-
private requestJson;
|
|
44
|
-
}
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FireweaveRemoteWebAdapter = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Fireweave remote adapter for browsers — the production path.
|
|
6
|
-
*
|
|
7
|
-
* Speaks only the vendor-neutral Fireweave remote protocol to fw-server:
|
|
8
|
-
* POST /v1/control-points/evaluate (batch — one call per context, not per key)
|
|
9
|
-
* POST /v1/capture
|
|
10
|
-
* POST /v1/targets/register
|
|
11
|
-
*
|
|
12
|
-
* Auth: `Authorization: Bearer <project key>`. Never a vendor key: the
|
|
13
|
-
* constructor rejects `phc_`/`phs_`/`phx_` shapes outright (ADR-0009).
|
|
14
|
-
*
|
|
15
|
-
* Credentials are EXPLICIT constructor options. This package never reads
|
|
16
|
-
* `import.meta.env`, `process.env`, or any other ambient source — the embedding
|
|
17
|
-
* app decides what goes into its bundle, and the SDK never picks something up
|
|
18
|
-
* on its own.
|
|
19
|
-
*
|
|
20
|
-
* See spec/remote-protocol.md § Browser clients.
|
|
21
|
-
*/
|
|
22
|
-
const errors_js_1 = require("../../domain/errors.js");
|
|
23
|
-
const hosts_js_1 = require("../hosts.js");
|
|
24
|
-
const validation_js_1 = require("../../domain/validation.js");
|
|
25
|
-
const EVALUATE_PATH = '/v1/control-points/evaluate';
|
|
26
|
-
const CAPTURE_PATH = '/v1/capture';
|
|
27
|
-
const REGISTER_TARGET_PATH = '/v1/targets/register';
|
|
28
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 3_000;
|
|
29
|
-
const ALLOWED_REASONS = new Set([
|
|
30
|
-
'STATIC',
|
|
31
|
-
'DEFAULT',
|
|
32
|
-
'TARGETING_MATCH',
|
|
33
|
-
'SPLIT',
|
|
34
|
-
'CACHED',
|
|
35
|
-
'STALE',
|
|
36
|
-
'DISABLED',
|
|
37
|
-
'UNKNOWN',
|
|
38
|
-
'ERROR',
|
|
39
|
-
]);
|
|
40
|
-
function toReason(reason) {
|
|
41
|
-
return ALLOWED_REASONS.has(reason) ? reason : undefined;
|
|
42
|
-
}
|
|
43
|
-
function mapHttpStatus(status) {
|
|
44
|
-
if (status === 401)
|
|
45
|
-
return new errors_js_1.FireweaveError('Authentication');
|
|
46
|
-
if (status === 403)
|
|
47
|
-
return new errors_js_1.FireweaveError('Authorization');
|
|
48
|
-
if (status === 429)
|
|
49
|
-
return new errors_js_1.FireweaveError('RateLimited');
|
|
50
|
-
return new errors_js_1.FireweaveError('BackendUnavailable');
|
|
51
|
-
}
|
|
52
|
-
class FireweaveRemoteWebAdapter {
|
|
53
|
-
name = 'fireweave';
|
|
54
|
-
apiUrl;
|
|
55
|
-
apiKey;
|
|
56
|
-
allowedHosts;
|
|
57
|
-
requestTimeoutMs;
|
|
58
|
-
fetchImpl;
|
|
59
|
-
pending = [];
|
|
60
|
-
ready = false;
|
|
61
|
-
closed = false;
|
|
62
|
-
constructor(options) {
|
|
63
|
-
// Validate at construction, not at first use: a misconfigured key should
|
|
64
|
-
// fail where the developer can see it, not on a user's first evaluation.
|
|
65
|
-
(0, hosts_js_1.assertNotSecretKey)(options.apiKey);
|
|
66
|
-
this.apiUrl = options.apiUrl.trim().replace(/\/+$/, '');
|
|
67
|
-
this.apiKey = options.apiKey.trim();
|
|
68
|
-
this.allowedHosts = options.allowedHosts;
|
|
69
|
-
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
70
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
71
|
-
}
|
|
72
|
-
async initialize(_signal) {
|
|
73
|
-
if (this.closed)
|
|
74
|
-
throw new errors_js_1.FireweaveError('AlreadyClosed');
|
|
75
|
-
if (this.apiUrl.length === 0)
|
|
76
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
77
|
-
(0, hosts_js_1.assertHostAllowed)(this.apiUrl, this.allowedHosts);
|
|
78
|
-
this.ready = true;
|
|
79
|
-
}
|
|
80
|
-
async prefetch(context, options) {
|
|
81
|
-
if (this.closed)
|
|
82
|
-
throw new errors_js_1.FireweaveError('AlreadyClosed');
|
|
83
|
-
if (!this.ready)
|
|
84
|
-
throw new errors_js_1.FireweaveError('NotReady');
|
|
85
|
-
const targetingKeyResult = (0, validation_js_1.validateTargetingKey)(context.targetingKey, true);
|
|
86
|
-
if (!targetingKeyResult.ok)
|
|
87
|
-
throw targetingKeyResult.error;
|
|
88
|
-
const targetingKey = targetingKeyResult.value ?? '';
|
|
89
|
-
// `$`-prefixed and `fireweave.`-prefixed attributes are backend directives,
|
|
90
|
-
// not person properties, and groups travel in their own fields.
|
|
91
|
-
const attributes = {};
|
|
92
|
-
for (const [k, v] of Object.entries(context.attributes)) {
|
|
93
|
-
if (k === 'groups' || k === 'groupProperties')
|
|
94
|
-
continue;
|
|
95
|
-
if (k.startsWith('$') || k.startsWith('fireweave.'))
|
|
96
|
-
continue;
|
|
97
|
-
attributes[k] = v;
|
|
98
|
-
}
|
|
99
|
-
const body = { targetingKey };
|
|
100
|
-
if (Object.keys(attributes).length > 0)
|
|
101
|
-
body['attributes'] = attributes;
|
|
102
|
-
if (context.groups !== undefined)
|
|
103
|
-
body['groups'] = context.groups;
|
|
104
|
-
if (context.groupProperties !== undefined)
|
|
105
|
-
body['groupProperties'] = context.groupProperties;
|
|
106
|
-
if (options?.controlPointKeys !== undefined && options.controlPointKeys.length > 0) {
|
|
107
|
-
body['controlPointKeys'] = options.controlPointKeys;
|
|
108
|
-
}
|
|
109
|
-
const response = await this.requestJson(EVALUATE_PATH, body, options?.signal);
|
|
110
|
-
const out = new Map();
|
|
111
|
-
for (const item of response.decisions ?? []) {
|
|
112
|
-
out.set(item.controlPointKey, this.toResolution(item, response.quotaLimited === true));
|
|
113
|
-
}
|
|
114
|
-
return out;
|
|
115
|
-
}
|
|
116
|
-
async registerTarget(targetingKey, options = {}) {
|
|
117
|
-
if (this.closed)
|
|
118
|
-
return { ok: false, error: new errors_js_1.FireweaveError('AlreadyClosed') };
|
|
119
|
-
if (!this.ready)
|
|
120
|
-
return { ok: false, error: new errors_js_1.FireweaveError('NotReady') };
|
|
121
|
-
const targetingKeyResult = (0, validation_js_1.validateTargetingKey)(targetingKey.trim() === '' ? undefined : targetingKey, true);
|
|
122
|
-
if (!targetingKeyResult.ok)
|
|
123
|
-
return { ok: false, error: targetingKeyResult.error };
|
|
124
|
-
const body = { targetingKey };
|
|
125
|
-
if (options.kind !== undefined)
|
|
126
|
-
body['kind'] = options.kind;
|
|
127
|
-
if (options.environment !== undefined)
|
|
128
|
-
body['environment'] = options.environment;
|
|
129
|
-
if (options.properties !== undefined && Object.keys(options.properties).length > 0) {
|
|
130
|
-
body['properties'] = options.properties;
|
|
131
|
-
}
|
|
132
|
-
// Retried once, and only when the taxonomy says a retry could succeed: a
|
|
133
|
-
// rejected payload or a bad key would be rejected identically.
|
|
134
|
-
let lastError;
|
|
135
|
-
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
136
|
-
try {
|
|
137
|
-
await this.requestJson(REGISTER_TARGET_PATH, body, options.signal);
|
|
138
|
-
return { ok: true };
|
|
139
|
-
}
|
|
140
|
-
catch (err) {
|
|
141
|
-
lastError = err instanceof errors_js_1.FireweaveError ? err : new errors_js_1.FireweaveError('BackendUnavailable');
|
|
142
|
-
if (!lastError.retryable)
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return { ok: false, ...(lastError !== undefined ? { error: lastError } : {}) };
|
|
147
|
-
}
|
|
148
|
-
recordExposure(exposure) {
|
|
149
|
-
if (this.closed || !this.ready)
|
|
150
|
-
return;
|
|
151
|
-
const event = {
|
|
152
|
-
type: 'exposure',
|
|
153
|
-
targetingKey: exposure.targetingKey,
|
|
154
|
-
controlPointKey: exposure.controlPointKey,
|
|
155
|
-
value: exposure.value,
|
|
156
|
-
};
|
|
157
|
-
if (exposure.variant !== undefined)
|
|
158
|
-
event.variant = exposure.variant;
|
|
159
|
-
if (exposure.rolloutId !== undefined)
|
|
160
|
-
event.rolloutId = exposure.rolloutId;
|
|
161
|
-
if (exposure.changeId !== undefined)
|
|
162
|
-
event.changeId = exposure.changeId;
|
|
163
|
-
if (exposure.stampId !== undefined)
|
|
164
|
-
event.stampId = exposure.stampId;
|
|
165
|
-
this.pending.push(event);
|
|
166
|
-
}
|
|
167
|
-
recordSignal(signal) {
|
|
168
|
-
if (this.closed || !this.ready)
|
|
169
|
-
return;
|
|
170
|
-
const event = {
|
|
171
|
-
type: 'signal',
|
|
172
|
-
targetingKey: signal.targetingKey ?? signal.rolloutId ?? 'fireweave-web-sdk',
|
|
173
|
-
name: signal.name,
|
|
174
|
-
properties: {
|
|
175
|
-
kind: signal.kind,
|
|
176
|
-
...(signal.status !== undefined ? { status: signal.status } : {}),
|
|
177
|
-
...(signal.errorKind !== undefined ? { errorKind: signal.errorKind } : {}),
|
|
178
|
-
...(signal.message !== undefined ? { message: signal.message } : {}),
|
|
179
|
-
...(signal.value !== undefined ? { value: signal.value } : {}),
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
if (signal.controlPointKey !== undefined)
|
|
183
|
-
event.controlPointKey = signal.controlPointKey;
|
|
184
|
-
if (signal.variant !== undefined)
|
|
185
|
-
event.variant = signal.variant;
|
|
186
|
-
if (signal.rolloutId !== undefined)
|
|
187
|
-
event.rolloutId = signal.rolloutId;
|
|
188
|
-
if (signal.timestamp !== undefined)
|
|
189
|
-
event.timestamp = signal.timestamp;
|
|
190
|
-
this.pending.push(event);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* `beacon: true` is the unload path. It must not await anything the browser
|
|
194
|
-
* will cancel, so it uses `sendBeacon` when available and a `keepalive`
|
|
195
|
-
* fetch otherwise, and it never re-queues on failure — there is no later.
|
|
196
|
-
*/
|
|
197
|
-
async flush(options = {}) {
|
|
198
|
-
if (this.closed || this.pending.length === 0)
|
|
199
|
-
return;
|
|
200
|
-
const batch = this.pending.splice(0, this.pending.length);
|
|
201
|
-
const payload = JSON.stringify({ events: batch });
|
|
202
|
-
if (options.beacon === true) {
|
|
203
|
-
this.sendBeaconOrKeepalive(payload);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
try {
|
|
207
|
-
await this.requestJson(CAPTURE_PATH, { events: batch });
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
// Never throw out of flush; re-queue so a later flush can retry.
|
|
211
|
-
this.pending.unshift(...batch);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
sendBeaconOrKeepalive(payload) {
|
|
215
|
-
const url = `${this.apiUrl}${CAPTURE_PATH}`;
|
|
216
|
-
const nav = globalThis.navigator;
|
|
217
|
-
// sendBeacon cannot set an Authorization header, so it is only usable where
|
|
218
|
-
// the server accepts the key another way. Prefer keepalive fetch, which
|
|
219
|
-
// can, and fall back to a beacon only if fetch is unavailable.
|
|
220
|
-
try {
|
|
221
|
-
void this.fetchImpl(url, {
|
|
222
|
-
method: 'POST',
|
|
223
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${this.apiKey}` },
|
|
224
|
-
body: payload,
|
|
225
|
-
keepalive: true,
|
|
226
|
-
}).catch(() => undefined);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
// fall through to beacon
|
|
231
|
-
}
|
|
232
|
-
if (typeof nav?.sendBeacon === 'function') {
|
|
233
|
-
try {
|
|
234
|
-
nav.sendBeacon(url, new Blob([payload], { type: 'application/json' }));
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// Unload-time delivery is best-effort by definition.
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
async shutdown() {
|
|
242
|
-
if (this.closed)
|
|
243
|
-
return;
|
|
244
|
-
await this.flush();
|
|
245
|
-
this.closed = true;
|
|
246
|
-
this.ready = false;
|
|
247
|
-
}
|
|
248
|
-
features() {
|
|
249
|
-
return {
|
|
250
|
-
remoteEvaluation: true,
|
|
251
|
-
// Structural, not configurable — the browser never evaluates locally.
|
|
252
|
-
localEvaluation: false,
|
|
253
|
-
localOnly: false,
|
|
254
|
-
exposureEmission: true,
|
|
255
|
-
sideEffectFreeReads: true,
|
|
256
|
-
groupAnalytics: true,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
toResolution(item, quotaLimited) {
|
|
260
|
-
if (item.found === false) {
|
|
261
|
-
return quotaLimited ? { found: false, quotaLimited: true } : { found: false };
|
|
262
|
-
}
|
|
263
|
-
const resolution = {
|
|
264
|
-
found: true,
|
|
265
|
-
value: item.value,
|
|
266
|
-
enabled: item.enabled ?? true,
|
|
267
|
-
};
|
|
268
|
-
if (item.variant != null)
|
|
269
|
-
resolution.variant = item.variant;
|
|
270
|
-
const reason = toReason(item.reason);
|
|
271
|
-
if (reason !== undefined)
|
|
272
|
-
resolution.reason = reason;
|
|
273
|
-
if (item.payload !== undefined)
|
|
274
|
-
resolution.payload = item.payload;
|
|
275
|
-
const meta = item.controlPointMetadata ?? {};
|
|
276
|
-
if (typeof meta['fireweave.controlPointVersion'] === 'number') {
|
|
277
|
-
resolution.version = meta['fireweave.controlPointVersion'];
|
|
278
|
-
}
|
|
279
|
-
if (typeof meta['fireweave.reasonCode'] === 'string') {
|
|
280
|
-
resolution.reasonCode = meta['fireweave.reasonCode'];
|
|
281
|
-
}
|
|
282
|
-
if (quotaLimited || meta['fireweave.quotaLimited'] === true)
|
|
283
|
-
resolution.quotaLimited = true;
|
|
284
|
-
return resolution;
|
|
285
|
-
}
|
|
286
|
-
async requestJson(path, body, outerSignal) {
|
|
287
|
-
const controller = new AbortController();
|
|
288
|
-
const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
289
|
-
const onOuterAbort = () => controller.abort();
|
|
290
|
-
if (outerSignal !== undefined) {
|
|
291
|
-
if (outerSignal.aborted)
|
|
292
|
-
controller.abort();
|
|
293
|
-
else
|
|
294
|
-
outerSignal.addEventListener('abort', onOuterAbort, { once: true });
|
|
295
|
-
}
|
|
296
|
-
try {
|
|
297
|
-
let response;
|
|
298
|
-
try {
|
|
299
|
-
response = await this.fetchImpl(`${this.apiUrl}${path}`, {
|
|
300
|
-
method: 'POST',
|
|
301
|
-
headers: {
|
|
302
|
-
'content-type': 'application/json',
|
|
303
|
-
authorization: `Bearer ${this.apiKey}`,
|
|
304
|
-
},
|
|
305
|
-
body: JSON.stringify(body),
|
|
306
|
-
signal: controller.signal,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
catch (err) {
|
|
310
|
-
const aborted = err instanceof Error && err.name === 'AbortError';
|
|
311
|
-
throw aborted ? new errors_js_1.FireweaveError('Timeout') : new errors_js_1.FireweaveError('Network');
|
|
312
|
-
}
|
|
313
|
-
if (response.status >= 400)
|
|
314
|
-
throw mapHttpStatus(response.status);
|
|
315
|
-
try {
|
|
316
|
-
return (await response.json());
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
throw new errors_js_1.FireweaveError('MalformedResponse');
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
finally {
|
|
323
|
-
clearTimeout(timer);
|
|
324
|
-
if (outerSignal !== undefined)
|
|
325
|
-
outerSignal.removeEventListener('abort', onOuterAbort);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
exports.FireweaveRemoteWebAdapter = FireweaveRemoteWebAdapter;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export declare const DEFAULT_ALLOWED_HOSTS: readonly string[];
|
|
2
|
-
export declare function isLoopbackHostname(hostname: string): boolean;
|
|
3
|
-
/**
|
|
4
|
-
* Validate a backend URL against the allowlist.
|
|
5
|
-
* Throws `FireweaveError('Configuration')` — fixed message, no host echoed,
|
|
6
|
-
* because this message can land in a shared browser console.
|
|
7
|
-
*/
|
|
8
|
-
export declare function assertHostAllowed(host: string, allowedHosts?: readonly string[]): void;
|
|
9
|
-
/** Throw if `apiKey` looks like a vendor or secret key rather than a Fireweave project key. */
|
|
10
|
-
export declare function assertNotSecretKey(apiKey: string): void;
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_ALLOWED_HOSTS = void 0;
|
|
4
|
-
exports.isLoopbackHostname = isLoopbackHostname;
|
|
5
|
-
exports.assertHostAllowed = assertHostAllowed;
|
|
6
|
-
exports.assertNotSecretKey = assertNotSecretKey;
|
|
7
|
-
/**
|
|
8
|
-
* Backend host allowlist — the same SSRF guard the server SDK applies, kept
|
|
9
|
-
* for a different reason.
|
|
10
|
-
*
|
|
11
|
-
* A browser cannot be tricked into reaching a cloud metadata endpoint the way a
|
|
12
|
-
* server can; the browser's own origin model already prevents that. What this
|
|
13
|
-
* guard buys here is narrower and still worth having: a mistyped or tampered
|
|
14
|
-
* `apiUrl` cannot quietly send a project key, a targeting key, and a user's
|
|
15
|
-
* evaluation attributes to an unintended host. The allowlist is ON by default,
|
|
16
|
-
* so reaching a self-hosted fw-server is a deliberate, explicit act.
|
|
17
|
-
*
|
|
18
|
-
* Scheme policy matches the server SDK: https everywhere except loopback,
|
|
19
|
-
* where plain http is allowed for local development against a dev server.
|
|
20
|
-
*/
|
|
21
|
-
const errors_js_1 = require("../domain/errors.js");
|
|
22
|
-
exports.DEFAULT_ALLOWED_HOSTS = Object.freeze([
|
|
23
|
-
'app-server.fireweave.ai',
|
|
24
|
-
'staging-app-server.fireweave.ai',
|
|
25
|
-
'localhost',
|
|
26
|
-
'127.0.0.1',
|
|
27
|
-
'::1',
|
|
28
|
-
]);
|
|
29
|
-
function isLoopbackHostname(hostname) {
|
|
30
|
-
return (hostname === 'localhost' ||
|
|
31
|
-
hostname === '127.0.0.1' ||
|
|
32
|
-
hostname === '::1' ||
|
|
33
|
-
hostname === '[::1]');
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Validate a backend URL against the allowlist.
|
|
37
|
-
* Throws `FireweaveError('Configuration')` — fixed message, no host echoed,
|
|
38
|
-
* because this message can land in a shared browser console.
|
|
39
|
-
*/
|
|
40
|
-
function assertHostAllowed(host, allowedHosts) {
|
|
41
|
-
let url;
|
|
42
|
-
try {
|
|
43
|
-
url = new URL(host);
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
47
|
-
}
|
|
48
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
49
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
50
|
-
}
|
|
51
|
-
const loopback = isLoopbackHostname(url.hostname);
|
|
52
|
-
if (url.protocol === 'http:' && !loopback) {
|
|
53
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
54
|
-
}
|
|
55
|
-
const list = allowedHosts !== undefined && allowedHosts.length > 0 ? allowedHosts : exports.DEFAULT_ALLOWED_HOSTS;
|
|
56
|
-
if (list.includes('*'))
|
|
57
|
-
return; // explicit opt-out
|
|
58
|
-
const allowed = list.some((h) => url.hostname === h ||
|
|
59
|
-
(h === 'localhost' && loopback) ||
|
|
60
|
-
(h === '::1' && url.hostname === '[::1]'));
|
|
61
|
-
if (!allowed) {
|
|
62
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Key shapes this SDK refuses to accept.
|
|
67
|
-
*
|
|
68
|
-
* The whole security argument for a browser package (ADR-0009) is that it never
|
|
69
|
-
* wants a secret. Enforcing that at the door — rather than trusting that no
|
|
70
|
-
* code path asks for one — turns the claim into something a caller cannot
|
|
71
|
-
* accidentally violate by pasting the wrong value into their bundle config.
|
|
72
|
-
*/
|
|
73
|
-
const FORBIDDEN_KEY_PREFIXES = Object.freeze(['phs_', 'phx_', 'phc_']);
|
|
74
|
-
/** Throw if `apiKey` looks like a vendor or secret key rather than a Fireweave project key. */
|
|
75
|
-
function assertNotSecretKey(apiKey) {
|
|
76
|
-
const key = apiKey.trim();
|
|
77
|
-
if (key.length === 0)
|
|
78
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
79
|
-
for (const prefix of FORBIDDEN_KEY_PREFIXES) {
|
|
80
|
-
if (key.startsWith(prefix)) {
|
|
81
|
-
throw new errors_js_1.FireweaveError('Configuration');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
package/dist/start/build-info.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SDK_CHANNEL = exports.SDK_VERSION = void 0;
|
|
4
|
-
// GENERATED by tools/release/version.sh apply web — do not edit by hand.
|
|
5
|
-
// The start profile reads SDK_CHANNEL to choose its default fw-server host
|
|
6
|
-
// (docs/adr/0012-start-profile.md). test/unit/start-build-info.test.ts pins
|
|
7
|
-
// SDK_VERSION to package.json, so a release that forgets to stamp fails CI.
|
|
8
|
-
exports.SDK_VERSION = '3.0.0';
|
|
9
|
-
exports.SDK_CHANNEL = 'production';
|