@fireweaveai/server-sdk-cjs 0.0.0-reserve.0
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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/application/client.d.ts +77 -0
- package/dist/application/client.js +144 -0
- package/dist/application/mode.d.ts +70 -0
- package/dist/application/mode.js +74 -0
- package/dist/application/ports.d.ts +94 -0
- package/dist/application/ports.js +2 -0
- package/dist/application/runtime.d.ts +101 -0
- package/dist/application/runtime.js +346 -0
- package/dist/domain/context.d.ts +67 -0
- package/dist/domain/context.js +90 -0
- package/dist/domain/errors.d.ts +45 -0
- package/dist/domain/errors.js +99 -0
- package/dist/domain/target.d.ts +2 -0
- package/dist/domain/target.js +2 -0
- package/dist/domain/types.d.ts +107 -0
- package/dist/domain/types.js +2 -0
- package/dist/domain/validation.d.ts +124 -0
- package/dist/domain/validation.js +355 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +52 -0
- package/dist/infrastructure/adapters/inmemory.d.ts +77 -0
- package/dist/infrastructure/adapters/inmemory.js +151 -0
- package/dist/infrastructure/adapters/local.d.ts +108 -0
- package/dist/infrastructure/adapters/local.js +105 -0
- package/dist/infrastructure/adapters/remote.d.ts +74 -0
- package/dist/infrastructure/adapters/remote.js +373 -0
- package/dist/infrastructure/hosts.d.ts +18 -0
- package/dist/infrastructure/hosts.js +72 -0
- package/dist/register-browser.d.ts +1 -0
- package/dist/register-browser.js +7 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +13 -0
- package/dist/start/browser.d.ts +12 -0
- package/dist/start/browser.js +36 -0
- package/dist/start/build-info.d.ts +2 -0
- package/dist/start/build-info.js +9 -0
- package/dist/start/control-points.d.ts +20 -0
- package/dist/start/control-points.js +59 -0
- package/dist/start/env.d.ts +11 -0
- package/dist/start/env.js +64 -0
- package/dist/start/fw.d.ts +29 -0
- package/dist/start/fw.js +76 -0
- package/dist/start/index.d.ts +23 -0
- package/dist/start/index.js +28 -0
- package/dist/start/instance.d.ts +17 -0
- package/dist/start/instance.js +56 -0
- package/dist/start/names.d.ts +22 -0
- package/dist/start/names.js +25 -0
- package/dist/start/resolve.d.ts +40 -0
- package/dist/start/resolve.js +139 -0
- package/dist/start/state.d.ts +96 -0
- package/dist/start/state.js +303 -0
- package/package.json +58 -0
|
@@ -0,0 +1,373 @@
|
|
|
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;
|
|
@@ -0,0 +1,18 @@
|
|
|
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;
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Backend host allowlist (SSRF guard).
|
|
8
|
+
*
|
|
9
|
+
* The allowlist is ON by default: when no explicit `allowedHosts` is
|
|
10
|
+
* configured, only the canonical Fireweave hosts plus loopback are permitted,
|
|
11
|
+
* so a typo'd or tampered `host` cannot be aimed at cloud metadata endpoints.
|
|
12
|
+
* Custom / self-hosted deployments require an explicit `allowedHosts` entry;
|
|
13
|
+
* `['*']` opts out entirely.
|
|
14
|
+
*
|
|
15
|
+
* Scheme policy: https is required for non-loopback hosts; plain http is
|
|
16
|
+
* permitted on loopback only (the repo's test-server stub).
|
|
17
|
+
*/
|
|
18
|
+
const errors_js_1 = require("../domain/errors.js");
|
|
19
|
+
/**
|
|
20
|
+
* Canonical default host allowlist.
|
|
21
|
+
*
|
|
22
|
+
* v3 replaced the third-party analytics hostnames this list used to carry
|
|
23
|
+
* (ADR-0006) with Fireweave's own fw-server hostnames. The security property is
|
|
24
|
+
* unchanged — an unconfigured allowlist still denies everything it does not name
|
|
25
|
+
* — but the *contents* changed: code doing
|
|
26
|
+
* `allowedHosts: [...DEFAULT_ALLOWED_HOSTS, …]` no longer reaches the former
|
|
27
|
+
* vendor endpoints, which is the intent.
|
|
28
|
+
*/
|
|
29
|
+
exports.DEFAULT_ALLOWED_HOSTS = Object.freeze([
|
|
30
|
+
'app-server.fireweave.ai',
|
|
31
|
+
'staging-app-server.fireweave.ai',
|
|
32
|
+
'localhost',
|
|
33
|
+
'127.0.0.1',
|
|
34
|
+
'::1',
|
|
35
|
+
]);
|
|
36
|
+
/** True for loopback hostnames (http and test stubs are permitted here). */
|
|
37
|
+
function isLoopbackHostname(hostname) {
|
|
38
|
+
return (hostname === 'localhost' ||
|
|
39
|
+
hostname === '127.0.0.1' ||
|
|
40
|
+
hostname === '::1' ||
|
|
41
|
+
hostname === '[::1]');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate a backend host URL against the allowlist.
|
|
45
|
+
* Throws FireweaveError('Configuration') — fixed message, no host echo.
|
|
46
|
+
*/
|
|
47
|
+
function assertHostAllowed(host, allowedHosts) {
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(host);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new errors_js_1.FireweaveError('Configuration');
|
|
54
|
+
}
|
|
55
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
56
|
+
throw new errors_js_1.FireweaveError('Configuration');
|
|
57
|
+
}
|
|
58
|
+
const loopback = isLoopbackHostname(url.hostname);
|
|
59
|
+
if (url.protocol === 'http:' && !loopback) {
|
|
60
|
+
// https required for anything that leaves the machine.
|
|
61
|
+
throw new errors_js_1.FireweaveError('Configuration');
|
|
62
|
+
}
|
|
63
|
+
const list = allowedHosts !== undefined && allowedHosts.length > 0 ? allowedHosts : exports.DEFAULT_ALLOWED_HOSTS;
|
|
64
|
+
if (list.includes('*'))
|
|
65
|
+
return; // explicit opt-out
|
|
66
|
+
const allowed = list.some((h) => url.hostname === h ||
|
|
67
|
+
(h === 'localhost' && loopback) ||
|
|
68
|
+
(h === '::1' && url.hostname === '[::1]'));
|
|
69
|
+
if (!allowed) {
|
|
70
|
+
throw new errors_js_1.FireweaveError('Configuration');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/** Browser build of @fireweaveai/server-sdk/register: fails loudly, never starts. */
|
|
4
|
+
const index_js_1 = require("./index.js");
|
|
5
|
+
throw new index_js_1.FireweaveError('Configuration', {
|
|
6
|
+
message: '[fireweave] @fireweaveai/server-sdk/register is for server runtimes. Browser apps use @fireweaveai/web-sdk.',
|
|
7
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/register.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* @fireweaveai/server-sdk/register: start FireWeave from the environment alone.
|
|
5
|
+
*
|
|
6
|
+
* import '@fireweaveai/server-sdk/register'; // first import of the entrypoint
|
|
7
|
+
* node --import @fireweaveai/server-sdk/register app.js
|
|
8
|
+
* bun --preload @fireweaveai/server-sdk/register app.ts
|
|
9
|
+
*
|
|
10
|
+
* Use src/fireweave/start.ts instead when you pass control points or other options.
|
|
11
|
+
*/
|
|
12
|
+
const index_js_1 = require("./start/index.js");
|
|
13
|
+
(0, index_js_1.start)();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function start(): void;
|
|
2
|
+
export declare function defineControlPoints<T>(controlPoints: T): T;
|
|
3
|
+
export declare function resetForTests(): Promise<void>;
|
|
4
|
+
export declare const fw: Readonly<{
|
|
5
|
+
controlPoints: {};
|
|
6
|
+
identify: () => never;
|
|
7
|
+
instanceKey: () => never;
|
|
8
|
+
status: () => never;
|
|
9
|
+
client: () => never;
|
|
10
|
+
shutdown: () => never;
|
|
11
|
+
}>;
|
|
12
|
+
export { SDK_VERSION, SDK_CHANNEL } from './build-info.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SDK_CHANNEL = exports.SDK_VERSION = exports.fw = void 0;
|
|
4
|
+
exports.start = start;
|
|
5
|
+
exports.defineControlPoints = defineControlPoints;
|
|
6
|
+
exports.resetForTests = resetForTests;
|
|
7
|
+
/**
|
|
8
|
+
* Browser build of @fireweaveai/server-sdk/start (the "browser" export
|
|
9
|
+
* condition). Server keys must never reach a bundle, so every entry point
|
|
10
|
+
* fails loudly and names the right package.
|
|
11
|
+
*/
|
|
12
|
+
const index_js_1 = require("../index.js");
|
|
13
|
+
const serverOnly = () => new index_js_1.FireweaveError('Configuration', {
|
|
14
|
+
message: '[fireweave] @fireweaveai/server-sdk/start is for server runtimes. Browser apps use @fireweaveai/web-sdk.',
|
|
15
|
+
});
|
|
16
|
+
function start() {
|
|
17
|
+
throw serverOnly();
|
|
18
|
+
}
|
|
19
|
+
function defineControlPoints(controlPoints) {
|
|
20
|
+
return controlPoints;
|
|
21
|
+
}
|
|
22
|
+
async function resetForTests() { }
|
|
23
|
+
const fail = () => {
|
|
24
|
+
throw serverOnly();
|
|
25
|
+
};
|
|
26
|
+
exports.fw = Object.freeze({
|
|
27
|
+
controlPoints: new Proxy({}, { get: () => fail }),
|
|
28
|
+
identify: fail,
|
|
29
|
+
instanceKey: fail,
|
|
30
|
+
status: fail,
|
|
31
|
+
client: fail,
|
|
32
|
+
shutdown: fail,
|
|
33
|
+
});
|
|
34
|
+
var build_info_js_1 = require("./build-info.js");
|
|
35
|
+
Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return build_info_js_1.SDK_VERSION; } });
|
|
36
|
+
Object.defineProperty(exports, "SDK_CHANNEL", { enumerable: true, get: function () { return build_info_js_1.SDK_CHANNEL; } });
|
|
@@ -0,0 +1,9 @@
|
|
|
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 server — 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';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ControlPointDefinition {
|
|
2
|
+
/** Value served in local mode. Ignored in remote mode. */
|
|
3
|
+
readonly local: boolean;
|
|
4
|
+
/** Optional note for humans and agents. Never sent anywhere. */
|
|
5
|
+
readonly description?: string;
|
|
6
|
+
}
|
|
7
|
+
export type ControlPointMap = Readonly<Record<string, ControlPointDefinition>>;
|
|
8
|
+
/** Validate a control-points object and return a frozen copy. Throws Configuration on a bad entry. */
|
|
9
|
+
export declare function normalizeControlPoints(controlPoints: unknown): ControlPointMap;
|
|
10
|
+
/**
|
|
11
|
+
* Declare the app's control points. Returns its argument unchanged, typed, and
|
|
12
|
+
* checks it at import time so a typo fails where it was made.
|
|
13
|
+
*
|
|
14
|
+
* export const controlPoints = defineControlPoints({
|
|
15
|
+
* 'new-checkout': { local: true },
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
export declare function defineControlPoints<const T extends Record<string, ControlPointDefinition>>(controlPoints: T): T;
|
|
19
|
+
/** The core local adapter's seed map. */
|
|
20
|
+
export declare function toLocalControlPoints(controlPoints: ControlPointMap): Record<string, boolean>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeControlPoints = normalizeControlPoints;
|
|
4
|
+
exports.defineControlPoints = defineControlPoints;
|
|
5
|
+
exports.toLocalControlPoints = toLocalControlPoints;
|
|
6
|
+
/**
|
|
7
|
+
* The control-points object: every control point the app reads, with the value
|
|
8
|
+
* served in local mode. It lives in its own file
|
|
9
|
+
* (src/fireweave/control-points.ts by default) and is passed as
|
|
10
|
+
* start({ controlPoints }).
|
|
11
|
+
*
|
|
12
|
+
* It holds local values only. In remote mode fw-server and the rollout decide,
|
|
13
|
+
* and call sites keep `false` as their default (RAMP-1), so this file can
|
|
14
|
+
* never switch a feature on in production.
|
|
15
|
+
*/
|
|
16
|
+
const index_js_1 = require("../index.js");
|
|
17
|
+
const configError = (message) => new index_js_1.FireweaveError('Configuration', { message });
|
|
18
|
+
/** Validate a control-points object and return a frozen copy. Throws Configuration on a bad entry. */
|
|
19
|
+
function normalizeControlPoints(controlPoints) {
|
|
20
|
+
if (controlPoints === undefined || controlPoints === null)
|
|
21
|
+
return Object.freeze({});
|
|
22
|
+
if (typeof controlPoints !== 'object' || Array.isArray(controlPoints)) {
|
|
23
|
+
throw configError('[fireweave] start({ controlPoints }) must be an object of { key: { local: boolean } }.');
|
|
24
|
+
}
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [key, entry] of Object.entries(controlPoints)) {
|
|
27
|
+
const keyResult = (0, index_js_1.validateControlPointKey)(key);
|
|
28
|
+
if (!keyResult.ok)
|
|
29
|
+
throw configError(`[fireweave] controlPoints: '${key}' is not a valid control point key.`);
|
|
30
|
+
if (typeof entry !== 'object' || entry === null || typeof entry.local !== 'boolean') {
|
|
31
|
+
throw configError(`[fireweave] controlPoints['${key}'] must be { local: true } or { local: false }.`);
|
|
32
|
+
}
|
|
33
|
+
const { local, description } = entry;
|
|
34
|
+
if (description !== undefined && typeof description !== 'string') {
|
|
35
|
+
throw configError(`[fireweave] controlPoints['${key}'].description must be a string.`);
|
|
36
|
+
}
|
|
37
|
+
out[key] = Object.freeze(description === undefined ? { local } : { local, description });
|
|
38
|
+
}
|
|
39
|
+
return Object.freeze(out);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Declare the app's control points. Returns its argument unchanged, typed, and
|
|
43
|
+
* checks it at import time so a typo fails where it was made.
|
|
44
|
+
*
|
|
45
|
+
* export const controlPoints = defineControlPoints({
|
|
46
|
+
* 'new-checkout': { local: true },
|
|
47
|
+
* });
|
|
48
|
+
*/
|
|
49
|
+
function defineControlPoints(controlPoints) {
|
|
50
|
+
normalizeControlPoints(controlPoints);
|
|
51
|
+
return controlPoints;
|
|
52
|
+
}
|
|
53
|
+
/** The core local adapter's seed map. */
|
|
54
|
+
function toLocalControlPoints(controlPoints) {
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [key, entry] of Object.entries(controlPoints))
|
|
57
|
+
out[key] = entry.local;
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Reads one variable. Returns undefined when unset, empty, or only whitespace. */
|
|
2
|
+
export type EnvReader = (name: string) => string | undefined;
|
|
3
|
+
/** Reader over an explicit bag (start({ env }) and tests). Only string values count. */
|
|
4
|
+
export declare function envFromBag(bag: Readonly<Record<string, unknown>>): EnvReader;
|
|
5
|
+
/** Reader over the running process: Deno.env first, then process.env. */
|
|
6
|
+
export declare function processEnv(): EnvReader;
|
|
7
|
+
/**
|
|
8
|
+
* A reader that never throws, for values only needed to word a warning or an
|
|
9
|
+
* error (for example the key a local start ignores).
|
|
10
|
+
*/
|
|
11
|
+
export declare function quiet(read: EnvReader): EnvReader;
|