@voxgig/station 0.0.2 → 0.2.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/README.md +26 -32
- package/dist/src/Station.d.ts +1 -133
- package/dist/src/Station.js +46 -434
- package/dist/src/Station.js.map +1 -1
- package/dist/src/adapter.js +0 -54
- package/dist/src/adapter.js.map +1 -1
- package/dist/src/config-shape.js +0 -11
- package/dist/src/config-shape.js.map +1 -1
- package/dist/src/descriptor.js +0 -36
- package/dist/src/descriptor.js.map +1 -1
- package/dist/src/error.js +0 -5
- package/dist/src/error.js.map +1 -1
- package/dist/src/factory.d.ts +0 -9
- package/dist/src/factory.js +0 -37
- package/dist/src/factory.js.map +1 -1
- package/dist/src/feature.d.ts +0 -75
- package/dist/src/feature.js +0 -87
- package/dist/src/feature.js.map +1 -1
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +11 -22
- package/dist/src/index.js.map +1 -1
- package/dist/src/loader.d.ts +0 -29
- package/dist/src/loader.js +0 -66
- package/dist/src/loader.js.map +1 -1
- package/dist/src/profile.d.ts +10 -3
- package/dist/src/profile.js +97 -23
- package/dist/src/profile.js.map +1 -1
- package/dist/src/secrets.d.ts +5 -0
- package/dist/src/secrets.js +106 -32
- package/dist/src/secrets.js.map +1 -1
- package/dist/src/shape.d.ts +0 -22
- package/dist/src/shape.js +0 -113
- package/dist/src/shape.js.map +1 -1
- package/package.json +3 -4
- package/src/Station.ts +34 -441
- package/src/adapter.ts +0 -57
- package/src/config-shape.ts +0 -11
- package/src/descriptor.ts +0 -36
- package/src/error.ts +0 -5
- package/src/factory.ts +0 -37
- package/src/feature.ts +0 -87
- package/src/index.ts +1 -14
- package/src/loader.ts +0 -66
- package/src/profile.ts +115 -23
- package/src/secrets.ts +104 -32
- package/src/shape.ts +0 -113
- package/src/types.ts +0 -59
- package/dist/src/index.browser.d.ts +0 -14
- package/dist/src/index.browser.js +0 -64
- package/dist/src/index.browser.js.map +0 -1
- package/dist/src/profilecore.d.ts +0 -10
- package/dist/src/profilecore.js +0 -180
- package/dist/src/profilecore.js.map +0 -1
- package/src/index.browser.ts +0 -56
- package/src/profilecore.ts +0 -201
package/dist/src/Station.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Station = void 0;
|
|
4
|
-
exports.setConfigFileIO = setConfigFileIO;
|
|
5
4
|
exports.instanceRef = instanceRef;
|
|
6
5
|
exports.checkInstanceName = checkInstanceName;
|
|
7
6
|
exports.checkInstanceTag = checkInstanceTag;
|
|
@@ -9,34 +8,16 @@ const adapter_1 = require("./adapter");
|
|
|
9
8
|
const error_1 = require("./error");
|
|
10
9
|
const events_1 = require("./events");
|
|
11
10
|
const descriptor_1 = require("./descriptor");
|
|
12
|
-
const
|
|
11
|
+
const profile_1 = require("./profile");
|
|
13
12
|
const factory_1 = require("./factory");
|
|
14
13
|
const loader_1 = require("./loader");
|
|
15
14
|
const feature_1 = require("./feature");
|
|
16
15
|
const shape_1 = require("./shape");
|
|
17
16
|
const secrets_1 = require("./secrets");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
/** §6.1: `as` IS A TAG, NOT A FREE NAME.
|
|
23
|
-
*
|
|
24
|
-
* The api comes from the SDK being passed, so the resulting ref is
|
|
25
|
-
* `<api>$<tag>` and multi-instance works imperatively too. A full ref is
|
|
26
|
-
* also accepted and is VALIDATED: its name must equal the SDK's api
|
|
27
|
-
* slug, or it is `station_instance_api`.
|
|
28
|
-
*
|
|
29
|
-
* Both forms are needed because the imperative path is where the api is
|
|
30
|
-
* implicit in an argument rather than written in a key. An `as` that
|
|
31
|
-
* took an arbitrary name would reintroduce exactly the second-identity
|
|
32
|
-
* problem the ref re-key removed: under the ref invariant
|
|
33
|
-
* `as: 'solar-eu'` would denote the untagged `solar-eu` DEFINITION, not
|
|
34
|
-
* an instance of the SDK just handed in, and registry grouping, api
|
|
35
|
-
* defaults and every ref consumer would disagree about what it is.
|
|
36
|
-
*
|
|
37
|
-
* A bare connect(SDK) with no name falls back to the descriptor slug,
|
|
38
|
-
* which is today's behaviour and why the single-instance case is
|
|
39
|
-
* unchanged to the byte. */
|
|
17
|
+
// The station library core, solo mode (design D1): fully functional
|
|
18
|
+
// in-process with no other component running. The proxy (D2) is a
|
|
19
|
+
// deferred amplifier - `require` therefore fails on the operation path
|
|
20
|
+
// (design §2.1/§14), and `auto` degrades to solo with one warning event.
|
|
40
21
|
function instanceRef(api, fopts) {
|
|
41
22
|
const explicit = firstNonEmpty(fopts?.instance);
|
|
42
23
|
if (null != explicit) {
|
|
@@ -52,30 +33,8 @@ function instanceRef(api, fopts) {
|
|
|
52
33
|
}
|
|
53
34
|
return api;
|
|
54
35
|
}
|
|
55
|
-
// A full ref is validated against the api; a `$`-less string is a TAG
|
|
56
|
-
// and is joined to it.
|
|
57
|
-
//
|
|
58
|
-
// §6.1 gives both branches and does not say how to disambiguate a
|
|
59
|
-
// `$`-less string, which is a real ambiguity because a bare name is
|
|
60
|
-
// itself a valid (untagged) ref: `as: 'stripe'` on api `stripe` could
|
|
61
|
-
// read as the untagged ref `stripe` or as tag `stripe`. It is read as
|
|
62
|
-
// a TAG, giving `stripe$stripe`, because §6.1 says twice and
|
|
63
|
-
// emphatically that `as` is a tag rather than a free name, and a rule
|
|
64
|
-
// with no exceptions is the one that ports the same way twenty times.
|
|
65
|
-
// The alternative - collapsing when the tag happens to equal the api -
|
|
66
|
-
// is a special case that would make `as` mean different things at
|
|
67
|
-
// different values. Someone who wants the untagged instance passes no
|
|
68
|
-
// `as` at all, which is the documented spelling for it.
|
|
69
36
|
return checkref(-1 === as.indexOf('$') ? api + '$' + as : checkapi(api, as));
|
|
70
37
|
}
|
|
71
|
-
// The ref grammar is the JOINT identity model's (station-and-plugin.md
|
|
72
|
-
// §2, plugin design §4): a name is a package-ish specifier
|
|
73
|
-
// (`^[a-zA-Z@][a-zA-Z0-9.~_\-/]*$`), a tag is not (`^[a-zA-Z0-9.~_-]+$`
|
|
74
|
-
// or empty - it MAY start with a digit because auto-tagging assigns
|
|
75
|
-
// integer tags, and admits neither `@` nor `/`); both cap at 1024; the
|
|
76
|
-
// split is on the FIRST `$`, so `a$b$c` is a good name with a bad tag.
|
|
77
|
-
// Discharges the C4 divergence rows that pinned instanceRef accepting
|
|
78
|
-
// what plugin's grammar rejects.
|
|
79
38
|
const REF_NAME_RE = /^[a-zA-Z@][a-zA-Z0-9.~_\-\/]*$/;
|
|
80
39
|
const REF_TAG_RE = /^[a-zA-Z0-9.~_-]+$/;
|
|
81
40
|
const REF_MAX = 1024;
|
|
@@ -92,8 +51,6 @@ function checkInstanceTag(tag) {
|
|
|
92
51
|
if ('string' !== typeof tag) {
|
|
93
52
|
return false;
|
|
94
53
|
}
|
|
95
|
-
// The empty tag is an ordinary tag: the single-instance case writes
|
|
96
|
-
// no tag and never learns tags exist.
|
|
97
54
|
if (0 === tag.length) {
|
|
98
55
|
return true;
|
|
99
56
|
}
|
|
@@ -120,21 +77,13 @@ function checkref(ref) {
|
|
|
120
77
|
return '' === tag ? name : ref;
|
|
121
78
|
}
|
|
122
79
|
function checkapi(api, ref) {
|
|
123
|
-
if ((0,
|
|
124
|
-
throw new error_1.StationError('station_instance_api', 'instance "' + ref + '" names api "' + (0,
|
|
80
|
+
if ((0, profile_1.refapi)(ref) !== api) {
|
|
81
|
+
throw new error_1.StationError('station_instance_api', 'instance "' + ref + '" names api "' + (0, profile_1.refapi)(ref) + '", but the SDK ' +
|
|
125
82
|
'passed is api "' + api + '"; `as` is a tag, not a free name (§6.1)');
|
|
126
83
|
}
|
|
127
84
|
return ref;
|
|
128
85
|
}
|
|
129
86
|
class Station {
|
|
130
|
-
/** §6.2's second path, and the front door the docs name.
|
|
131
|
-
*
|
|
132
|
-
* The package exported only the free `provide()` and the class had no
|
|
133
|
-
* static — so the documented `Station.provide(...)` call failed, and
|
|
134
|
-
* so did the remediation `station_no_factory` prints, which tells the
|
|
135
|
-
* user to make exactly that call. Delegates to the same
|
|
136
|
-
* process-global table the free function fills; there is one
|
|
137
|
-
* registry, not two. */
|
|
138
87
|
static provide(api, factory) {
|
|
139
88
|
(0, factory_1.provide)(api, factory);
|
|
140
89
|
}
|
|
@@ -162,14 +111,12 @@ class Station {
|
|
|
162
111
|
static current() {
|
|
163
112
|
return Station.ambient;
|
|
164
113
|
}
|
|
165
|
-
// Test seam: drop the ambient instance.
|
|
166
114
|
static reset() {
|
|
167
115
|
Station.ambient = null;
|
|
168
116
|
Station.ambientOpts = null;
|
|
169
117
|
}
|
|
170
118
|
constructor(opts) {
|
|
171
119
|
this.registry = new Map();
|
|
172
|
-
// §6.1: `sdk(name)` caches; `create()` deliberately does not.
|
|
173
120
|
this.clients = new Map();
|
|
174
121
|
// The raw config, kept for §8.7's provenance: the resolved profile
|
|
175
122
|
// has already collapsed the levels that provenance has to name.
|
|
@@ -181,49 +128,20 @@ class Station {
|
|
|
181
128
|
* registration. */
|
|
182
129
|
this.aliasOf = new Map();
|
|
183
130
|
this.closed = false;
|
|
184
|
-
// §7.4: THE DESCRIPTOR IS SHARED, because it describes the api rather
|
|
185
|
-
// than any use of it. `normalizeDescriptor` runs once per api and
|
|
186
|
-
// every instance of that api holds a reference to the same object - at
|
|
187
|
-
// 26 instances over 20 apis that is 20 normalizations, not 26, and the
|
|
188
|
-
// canonical serialization the proxy dedupes registrations by is
|
|
189
|
-
// computed once per api too.
|
|
190
|
-
//
|
|
191
|
-
// Keyed by the slug the config carries, which is available before
|
|
192
|
-
// normalization only from the config itself, so the miss path
|
|
193
|
-
// normalizes and then keys on the result.
|
|
194
131
|
this.descriptorCache = new Map();
|
|
195
132
|
this.opts = opts || {};
|
|
196
133
|
const config = undefined !== this.opts.config
|
|
197
134
|
? this.opts.config
|
|
198
|
-
: (
|
|
199
|
-
// §6.3: an in-code config is repo-scoped by construction - the
|
|
200
|
-
// application wrote it. A file is repo-scoped unless it came from
|
|
201
|
-
// ~/.voxgig/station.json.
|
|
202
|
-
// Explicit wins, then an in-code config (the application wrote it,
|
|
203
|
-
// so it is repo-scoped by construction), then where the file was
|
|
204
|
-
// found. Inferring BEFORE reading the explicit option was a real
|
|
205
|
-
// precedence bug: it made `repoScoped: false` unsettable for any
|
|
206
|
-
// caller passing a config in code, which is every test of the rule.
|
|
135
|
+
: (0, profile_1.loadConfig)(this.opts.folder);
|
|
207
136
|
this.repoScoped = this.opts.repoScoped
|
|
208
137
|
?? (undefined !== this.opts.config
|
|
209
138
|
? true
|
|
210
|
-
:
|
|
211
|
-
'user' !== configfileio.configScope(this.opts.folder)));
|
|
212
|
-
// Normalize, then validate (design §4.2). A malformed station.json
|
|
213
|
-
// fails open() with EVERY error at once - an eighteen-instance
|
|
214
|
-
// config must not die because the eighteenth has a typo'd package
|
|
215
|
-
// name.
|
|
216
|
-
//
|
|
217
|
-
// resolveProfile then reads the RAW config, NOT the normalized one.
|
|
218
|
-
// The normalized form is an input to validation and to nothing
|
|
219
|
-
// else: block defaults synthesized before the profile merge would
|
|
220
|
-
// let a one-key overlay overwrite the base's `active: false` and
|
|
221
|
-
// silently re-enable a barred integration (§3.3, §4.2).
|
|
139
|
+
: 'user' !== (0, profile_1.configScope)(this.opts.folder));
|
|
222
140
|
if (null != config) {
|
|
223
141
|
(0, shape_1.validateConfig)((0, shape_1.normalizeConfig)(config));
|
|
224
142
|
}
|
|
225
143
|
this.raw = config ?? null;
|
|
226
|
-
this.profile = (0,
|
|
144
|
+
this.profile = (0, profile_1.resolveProfile)(config ?? null, (0, profile_1.selectProfile)(this.opts.profile));
|
|
227
145
|
this.broker = new secrets_1.SecretBroker(this.profile.providers);
|
|
228
146
|
this.buffer = new events_1.EventBuffer();
|
|
229
147
|
const proxy = this.opts.proxy ?? 'auto';
|
|
@@ -237,7 +155,6 @@ class Station {
|
|
|
237
155
|
});
|
|
238
156
|
}
|
|
239
157
|
}
|
|
240
|
-
// --- binding forms (design §3.1) ---
|
|
241
158
|
// connect(SDK, opts): station constructs the SDK itself, activating
|
|
242
159
|
// the adapter with §3.3 ordering. The activation entry plus the
|
|
243
160
|
// extend-supplied instance ride the tolerance added to the generated
|
|
@@ -256,35 +173,26 @@ class Station {
|
|
|
256
173
|
throw new error_1.StationError('station_no_plugin', 'station is closed');
|
|
257
174
|
}
|
|
258
175
|
opts = opts || {};
|
|
259
|
-
const
|
|
176
|
+
const { as, instance, ...sdkopts } = opts;
|
|
177
|
+
const fmap = { ...(sdkopts.feature || {}) };
|
|
260
178
|
fmap['station'] = {
|
|
261
179
|
...(fmap['station'] || {}),
|
|
262
180
|
active: true, station: this, calleropts: opts,
|
|
263
|
-
// §6.1: `as` is a
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
...(null == opts.as ? {} : { as: opts.as }),
|
|
267
|
-
...(null == opts.instance ? {} : { instance: opts.instance }),
|
|
181
|
+
// §6.1: `as` is a tag, resolved against the SDK's api in _register.
|
|
182
|
+
...(null == as ? {} : { as }),
|
|
183
|
+
...(null == instance ? {} : { instance }),
|
|
268
184
|
};
|
|
269
185
|
const options = {
|
|
270
|
-
...
|
|
186
|
+
...sdkopts,
|
|
271
187
|
feature: fmap,
|
|
272
188
|
// The carried adapter rides extend for SDKs generated WITHOUT
|
|
273
189
|
// the station feature; when the generated class exists the
|
|
274
190
|
// constructor uses it and the extend copy is skipped by name
|
|
275
191
|
// (both delegate to featureBinding, so behavior is identical).
|
|
276
|
-
extend: [...(
|
|
192
|
+
extend: [...(sdkopts.extend || []), (0, adapter_1.adapterFeature)(this, opts)],
|
|
277
193
|
};
|
|
278
194
|
return new SDK(options);
|
|
279
195
|
}
|
|
280
|
-
// Inverted binding (design §3.1): build the plain options map a
|
|
281
|
-
// generated constructor already accepts - the handle, the activation
|
|
282
|
-
// entry, and the profile's per-plugin base (applied here, at
|
|
283
|
-
// options-build time, so caller opts passed in `extra` still win).
|
|
284
|
-
// §6.1: `options(instanceName?, extra?)`. The name is optional and
|
|
285
|
-
// leading, so every existing `options({...})` call is unchanged - the
|
|
286
|
-
// inverted binding is the static languages' path and they need to say
|
|
287
|
-
// which instance they are building without a second method.
|
|
288
196
|
options(a, b) {
|
|
289
197
|
const named = 'string' === typeof a;
|
|
290
198
|
const instance = named ? a : undefined;
|
|
@@ -297,14 +205,6 @@ class Station {
|
|
|
297
205
|
};
|
|
298
206
|
return { ...extra, feature: fmap };
|
|
299
207
|
}
|
|
300
|
-
// --- registration (design §3 item 1, called by the adapter) ---
|
|
301
|
-
// The registry entry whose client IS this object, or null. Used by
|
|
302
|
-
// featureBinding for idempotency: connect/adopt activate the station
|
|
303
|
-
// entry AND ride the carried adapter on extend, so on an SDK whose
|
|
304
|
-
// generated config carries a real station feature class the same
|
|
305
|
-
// construction reaches featureBinding twice - the second arrival
|
|
306
|
-
// must no-op, while a genuinely second client of the same SDK class
|
|
307
|
-
// still fails _register's slug check (§10.2).
|
|
308
208
|
_boundEntry(client) {
|
|
309
209
|
for (const entry of this.registry.values()) {
|
|
310
210
|
if (entry.client === client) {
|
|
@@ -313,29 +213,10 @@ class Station {
|
|
|
313
213
|
}
|
|
314
214
|
return null;
|
|
315
215
|
}
|
|
316
|
-
/** The profile block that governs an instance — its own if the
|
|
317
|
-
* profile declares it, otherwise its **api's**.
|
|
318
|
-
*
|
|
319
|
-
* `resolveProfile` builds `profile.sdk` from the declared refs alone
|
|
320
|
-
* ("an api block declares no instance, so the ref set comes from the
|
|
321
|
-
* two `sdk` maps"), shallow-merging `profile.api[a]` into each. That
|
|
322
|
-
* is right for a declared instance and leaves an IMPERATIVE one —
|
|
323
|
-
* `connect(SDK, { as: 'test' })`, named but never written into
|
|
324
|
-
* config — with no block at all. The api-level `secret`, `base` and
|
|
325
|
-
* most seriously `policy.hosts` then did not reach it, so a profile
|
|
326
|
-
* that denies egress everywhere denied nothing for a tagged client.
|
|
327
|
-
*
|
|
328
|
-
* ONE RULE, ONE PLACE: registration and the transport seam both ask
|
|
329
|
-
* here, because they disagreeing is how the credential and the
|
|
330
|
-
* allowlist came apart in the first place. */
|
|
331
216
|
blockFor(name) {
|
|
332
217
|
return this.profile.sdk[this.declaredRef(name)] ??
|
|
333
|
-
this.profile.api[(0,
|
|
218
|
+
this.profile.api[(0, profile_1.refapi)(name)];
|
|
334
219
|
}
|
|
335
|
-
/** The DECLARED instance an assigned tag stands for, or the name
|
|
336
|
-
* itself. `create('stripe$prod')` registers under `stripe$1`, and
|
|
337
|
-
* every question about that client's configuration — its secret, its
|
|
338
|
-
* base, its egress policy — is a question about `stripe$prod`. */
|
|
339
220
|
declaredRef(name) {
|
|
340
221
|
return this.aliasOf.get(name) ?? name;
|
|
341
222
|
}
|
|
@@ -347,45 +228,17 @@ class Station {
|
|
|
347
228
|
// with no name falls back to the descriptor slug, which is today's
|
|
348
229
|
// behaviour and why the single-instance case is unchanged.
|
|
349
230
|
const name = instanceRef(api, fopts);
|
|
350
|
-
// §7.1: the check moves to the instance key. Two clients of one api
|
|
351
|
-
// is the NORMAL case now; two bindings of one instance is still the
|
|
352
|
-
// error it was.
|
|
353
231
|
if (this.registry.has(name)) {
|
|
354
232
|
throw new error_1.StationError('station_bound_twice', 'instance "' + name + '" is already registered; binding one client ' +
|
|
355
233
|
'twice is an error (§10.2)');
|
|
356
234
|
}
|
|
357
235
|
const profilePlugin = this.blockFor(name);
|
|
358
|
-
// Secret name precedence: the feature option (in-code, design §9
|
|
359
|
-
// config.options.secret) beats the profile, which beats the
|
|
360
|
-
// INSTANCE-derived default.
|
|
361
|
-
//
|
|
362
|
-
// §5.1: `secretnameDefault` takes the instance name, not the slug.
|
|
363
|
-
// For an untagged instance the two are the same string, so the
|
|
364
|
-
// single-instance case is unchanged to the byte - `voxgig-solardemo`
|
|
365
|
-
// still derives `voxgig_solardemo.apikey` and still reads
|
|
366
|
-
// VOXGIG_SOLARDEMO_APIKEY. `stripe$test` derives
|
|
367
|
-
// `stripe_test.apikey`, because each instance IS a separate
|
|
368
|
-
// credentialed use of the API.
|
|
369
|
-
//
|
|
370
|
-
// The descriptor's own `auth.secretname` stays the API-level
|
|
371
|
-
// default and is NOT used here (§7.4): one descriptor is shared by
|
|
372
|
-
// every instance of an api and cannot hold two instance-derived
|
|
373
|
-
// names, so whichever instance built the cached copy would report
|
|
374
|
-
// the other's secret metadata wrongly. This is the authority; that
|
|
375
|
-
// field is documentation.
|
|
376
|
-
// ...and the DEFAULT takes the declared name too, not the assigned
|
|
377
|
-
// tag: `stripe$1` created from `stripe$test` derives
|
|
378
|
-
// `stripe_test.apikey`, so every per-request client of one instance
|
|
379
|
-
// shares one broker cache entry (§5.3).
|
|
380
236
|
const secretname = firstNonEmpty(fopts?.secret, profilePlugin?.secret) ||
|
|
381
237
|
(0, descriptor_1.secretnameDefault)(this.declaredRef(name));
|
|
382
238
|
const rung = descriptor.auth.active ? 'R1' : 'none';
|
|
383
239
|
const binding = {
|
|
384
240
|
plugin: name,
|
|
385
241
|
instance: api,
|
|
386
|
-
// §7.2: two live instances of one api MUST have distinct
|
|
387
|
-
// placeholders or the injection seam cannot tell which credential
|
|
388
|
-
// a header wants.
|
|
389
242
|
placeholder: descriptor.auth.active ? (0, secrets_1.placeholderFor)(name) : undefined,
|
|
390
243
|
secretname: descriptor.auth.active ? secretname : undefined,
|
|
391
244
|
rung,
|
|
@@ -406,22 +259,6 @@ class Station {
|
|
|
406
259
|
});
|
|
407
260
|
return { binding, profilePlugin };
|
|
408
261
|
}
|
|
409
|
-
/** The descriptor describes the API, and is SHARED by every instance
|
|
410
|
-
* of it — which is §7.4's own reasoning for why the effective secret
|
|
411
|
-
* name moved to `Binding` and is not read off the descriptor.
|
|
412
|
-
*
|
|
413
|
-
* The same argument applies to feature activation, and this did not
|
|
414
|
-
* follow it: the cache is keyed by slug alone while
|
|
415
|
-
* `normalizeDescriptor`'s second argument decides each feature row's
|
|
416
|
-
* `active`. So two instances of one api constructed with different
|
|
417
|
-
* feature maps shared the FIRST one's feature state, and
|
|
418
|
-
* `descriptorOf()` / `canonicalDescriptor()` became
|
|
419
|
-
* construction-order-dependent.
|
|
420
|
-
*
|
|
421
|
-
* Normalized with no per-instance features, so the shared value holds
|
|
422
|
-
* only API-stable metadata — which is what `factory.ts` already does
|
|
423
|
-
* when it normalizes at provide time. Per-instance activation is
|
|
424
|
-
* `featuresOf(name)`'s answer, and always was. */
|
|
425
262
|
describe(config, _feature) {
|
|
426
263
|
const slug = String(config?.main?.slug ?? '');
|
|
427
264
|
if ('' !== slug) {
|
|
@@ -437,7 +274,7 @@ class Station {
|
|
|
437
274
|
_hoist(name, value) {
|
|
438
275
|
this.broker.hoist(name, value);
|
|
439
276
|
this.emit({
|
|
440
|
-
t: Date.now(), kind: 'station', plugin: name, api: (0,
|
|
277
|
+
t: Date.now(), kind: 'station', plugin: name, api: (0, profile_1.refapi)(name),
|
|
441
278
|
meta: {
|
|
442
279
|
warn: 'a resident credential was hoisted into the broker and ' +
|
|
443
280
|
'replaced by the placeholder; prefer configuring the secret ' +
|
|
@@ -445,7 +282,6 @@ class Station {
|
|
|
445
282
|
},
|
|
446
283
|
});
|
|
447
284
|
}
|
|
448
|
-
// --- the transport middleware (design §3.3, §5.3) ---
|
|
449
285
|
async _transport(name, inner, fctx, fullurl, fetchdef) {
|
|
450
286
|
// Fail-closed means traffic (§2.1): with the proxy deferred,
|
|
451
287
|
// `require` can never attach, so every operation fails here - the
|
|
@@ -459,20 +295,6 @@ class Station {
|
|
|
459
295
|
const placeholder = (0, secrets_1.placeholderFor)(name);
|
|
460
296
|
const live = 'live' === fctx.client._mode;
|
|
461
297
|
const profilePlugin = this.blockFor(name);
|
|
462
|
-
// Policy mode (design §16): `live` is the default; `block` is the
|
|
463
|
-
// kill switch and refuses live egress outright. Enforced at the
|
|
464
|
-
// same seam as the hosts allowlist, and like it only on LIVE
|
|
465
|
-
// traffic - a test-mode client's mock transport is not egress.
|
|
466
|
-
//
|
|
467
|
-
// §14 names no mode-specific code, so `block` raises
|
|
468
|
-
// `station_host_allow` - the closest existing catalog code: the
|
|
469
|
-
// same `_allow` gate grammar, the same seam, the same meaning
|
|
470
|
-
// (egress denied by this plugin's policy), with the message naming
|
|
471
|
-
// the mode. `record`/`replay`/`mock` are proxy-era modes accepted
|
|
472
|
-
// by the grammar now (so a config written for the proxy is not a
|
|
473
|
-
// validation error) and fail `station_no_proxy` on the operation
|
|
474
|
-
// path until a proxy is attached - the same fail-closed behaviour
|
|
475
|
-
// `require` shows above.
|
|
476
298
|
const mode = profilePlugin?.policy?.mode ?? 'live';
|
|
477
299
|
if ('live' !== mode && live) {
|
|
478
300
|
const err = 'block' === mode
|
|
@@ -483,12 +305,6 @@ class Station {
|
|
|
483
305
|
this.emitErr(name, fctx, err);
|
|
484
306
|
return err;
|
|
485
307
|
}
|
|
486
|
-
// Egress policy (design §16), solo half: the hosts allowlist is
|
|
487
|
-
// enforced at the seam every request crosses. When a policy is
|
|
488
|
-
// present, redirects come back manual - a 3xx is a response like
|
|
489
|
-
// any other, so a Location off the allowlist cannot pull an
|
|
490
|
-
// automatic credentialed follow-up to an unapproved host (§8.2's
|
|
491
|
-
// rule, applied at the library seam).
|
|
492
308
|
const hosts = profilePlugin?.policy?.hosts;
|
|
493
309
|
if (null != hosts && live) {
|
|
494
310
|
let hostname = '';
|
|
@@ -504,6 +320,8 @@ class Station {
|
|
|
504
320
|
}
|
|
505
321
|
}
|
|
506
322
|
let senddef = fetchdef;
|
|
323
|
+
let sendurl = fullurl;
|
|
324
|
+
let placed = false;
|
|
507
325
|
if (null != hosts && live) {
|
|
508
326
|
senddef = { ...senddef, redirect: 'manual' };
|
|
509
327
|
}
|
|
@@ -513,20 +331,6 @@ class Station {
|
|
|
513
331
|
// enter in-memory mock stores. Copy-on-inject: the object graph
|
|
514
332
|
// reachable from ctx/spec/ctrl keeps the placeholder, ever (§5.3).
|
|
515
333
|
if (live && null != entry && 'R1' === entry.rung) {
|
|
516
|
-
// §7.4: THE EFFECTIVE NAME, resolved once at registration and
|
|
517
|
-
// stored on the entry. Re-deriving it here got the precedence
|
|
518
|
-
// right and the FALLBACK wrong: `descriptor.auth.secretname` is
|
|
519
|
-
// the API-level default, and one descriptor is shared by every
|
|
520
|
-
// instance of an api — so a tagged instance with no explicit
|
|
521
|
-
// `secret` read `stripe.apikey` where registration had recorded
|
|
522
|
-
// `stripe_test.apikey`. Either the request fails despite the
|
|
523
|
-
// credential being configured, or it succeeds with a sibling's.
|
|
524
|
-
//
|
|
525
|
-
// No fallback, and the `!` is the invariant rather than a shrug:
|
|
526
|
-
// this branch is guarded by `'R1' === entry.rung`, which is set
|
|
527
|
-
// only when `descriptor.auth.active` — the same condition under
|
|
528
|
-
// which `entry.secretname` is populated. Substituting a different
|
|
529
|
-
// name when it is absent is the bug above, written again.
|
|
530
334
|
const secretname = entry.secretname;
|
|
531
335
|
let value;
|
|
532
336
|
try {
|
|
@@ -536,29 +340,30 @@ class Station {
|
|
|
536
340
|
this.emitErr(name, fctx, e);
|
|
537
341
|
return e instanceof Error ? e : new Error(String(e));
|
|
538
342
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
343
|
+
const sent = (0, secrets_1.substitute)(fullurl, senddef.headers, placeholder, value);
|
|
344
|
+
sendurl = sent.url;
|
|
345
|
+
senddef = { ...senddef, headers: sent.headers };
|
|
346
|
+
const defurl = senddef.url;
|
|
347
|
+
if ('string' === typeof defurl) {
|
|
348
|
+
senddef.url = (0, secrets_1.substitute)(defurl, null, placeholder, value).url;
|
|
545
349
|
}
|
|
350
|
+
placed = sendurl !== fullurl || senddef.url !== defurl;
|
|
546
351
|
}
|
|
547
352
|
const corr = fctx.station$?.corr;
|
|
548
353
|
const started = Date.now();
|
|
549
354
|
let res;
|
|
550
355
|
try {
|
|
551
|
-
res = await inner(fctx,
|
|
356
|
+
res = await inner(fctx, sendurl, senddef);
|
|
552
357
|
}
|
|
553
358
|
catch (e) {
|
|
554
359
|
this.emitHttp(name, corr, fullurl, senddef, 0, started, 0);
|
|
555
360
|
this.emitErr(name, fctx, e);
|
|
556
|
-
throw e;
|
|
361
|
+
throw this.scrubbed(e, placed);
|
|
557
362
|
}
|
|
558
363
|
if (res instanceof Error) {
|
|
559
364
|
this.emitHttp(name, corr, fullurl, senddef, 0, started, 0);
|
|
560
365
|
this.emitErr(name, fctx, res);
|
|
561
|
-
return res;
|
|
366
|
+
return this.scrubbed(res, placed);
|
|
562
367
|
}
|
|
563
368
|
let bytes = 0;
|
|
564
369
|
const cl = res?.headers?.get?.('content-length');
|
|
@@ -568,6 +373,13 @@ class Station {
|
|
|
568
373
|
this.emitHttp(name, corr, fullurl, senddef, res?.status || 0, started, bytes);
|
|
569
374
|
return res;
|
|
570
375
|
}
|
|
376
|
+
// A transport error may quote its URL, which carries a query-placed value.
|
|
377
|
+
scrubbed(err, placed) {
|
|
378
|
+
if (placed && err instanceof Error) {
|
|
379
|
+
err.message = this.redact(err.message);
|
|
380
|
+
}
|
|
381
|
+
return err;
|
|
382
|
+
}
|
|
571
383
|
emitHttp(slug, corr, fullurl, fetchdef, status, started, bytes) {
|
|
572
384
|
let host = '', path = '';
|
|
573
385
|
try {
|
|
@@ -579,7 +391,7 @@ class Station {
|
|
|
579
391
|
path = fullurl;
|
|
580
392
|
}
|
|
581
393
|
this.emit({
|
|
582
|
-
t: started, kind: 'http', plugin: slug, api: (0,
|
|
394
|
+
t: started, kind: 'http', plugin: slug, api: (0, profile_1.refapi)(slug), corr,
|
|
583
395
|
http: {
|
|
584
396
|
method: fetchdef?.method || 'GET', host, path, status,
|
|
585
397
|
durationMs: Date.now() - started, bytes,
|
|
@@ -588,13 +400,7 @@ class Station {
|
|
|
588
400
|
}
|
|
589
401
|
emitErr(name, fctx, err) {
|
|
590
402
|
this.emit({
|
|
591
|
-
|
|
592
|
-
// what groups its siblings. Construction events carried both and
|
|
593
|
-
// the runtime ones did not, so a consumer could group `construct`
|
|
594
|
-
// by api and then get `api: undefined` for every request, error
|
|
595
|
-
// and hoist from the same client — the grouping working exactly
|
|
596
|
-
// until it was used.
|
|
597
|
-
t: Date.now(), kind: 'error', plugin: name, api: (0, profilecore_1.refapi)(name),
|
|
403
|
+
t: Date.now(), kind: 'error', plugin: name, api: (0, profile_1.refapi)(name),
|
|
598
404
|
corr: fctx?.station$?.corr,
|
|
599
405
|
err: {
|
|
600
406
|
code: err?.code,
|
|
@@ -604,13 +410,10 @@ class Station {
|
|
|
604
410
|
},
|
|
605
411
|
});
|
|
606
412
|
}
|
|
607
|
-
// Op events from the hook bridge (design §3 item 3).
|
|
608
413
|
_opEvent(name, ctx, outcome) {
|
|
609
414
|
const st = ctx.station$ || {};
|
|
610
415
|
this.emit({
|
|
611
|
-
t: Date.now(), kind: 'op', plugin: name, api: (0,
|
|
612
|
-
// ctx.op is the SDK's resolved Operation: name + entity, in the
|
|
613
|
-
// descriptor's lowercase spelling.
|
|
416
|
+
t: Date.now(), kind: 'op', plugin: name, api: (0, profile_1.refapi)(name), corr: st.corr,
|
|
614
417
|
op: {
|
|
615
418
|
entity: String(ctx.op?.entity ?? ctx.entity?.name ?? ''),
|
|
616
419
|
op: String(ctx.op?.name ?? ''),
|
|
@@ -619,7 +422,6 @@ class Station {
|
|
|
619
422
|
},
|
|
620
423
|
});
|
|
621
424
|
}
|
|
622
|
-
// --- the query/observe surface (design §3.2, §6) ---
|
|
623
425
|
// One entry per LIVE INSTANCE (§6.1), and exhaustive: auto-tagged
|
|
624
426
|
// entries are not collapsed here, because inspection, health
|
|
625
427
|
// reporting and cleanup all need to enumerate the clients `create()`
|
|
@@ -639,14 +441,6 @@ class Station {
|
|
|
639
441
|
warnings: e.warnings.slice(),
|
|
640
442
|
}));
|
|
641
443
|
}
|
|
642
|
-
// --- the declarative front door (design §6) ---
|
|
643
|
-
/** The instance, constructed on first call and CACHED: same name ->
|
|
644
|
-
* same object. That caching is what makes "get it where you need it"
|
|
645
|
-
* a real instruction - call it in a request handler, in a worker, in
|
|
646
|
-
* a test, and the first call pays construction while the rest are a
|
|
647
|
-
* map lookup.
|
|
648
|
-
*
|
|
649
|
-
* SYNCHRONOUS (§6.3), which is what bounds the loader to CommonJS. */
|
|
650
444
|
sdk(name) {
|
|
651
445
|
const cached = this.clients.get(name);
|
|
652
446
|
if (null != cached) {
|
|
@@ -656,40 +450,11 @@ class Station {
|
|
|
656
450
|
this.clients.set(name, client);
|
|
657
451
|
return client;
|
|
658
452
|
}
|
|
659
|
-
/** An UNCACHED client from the same resolved config plus overrides,
|
|
660
|
-
* for the case that genuinely wants a distinct one - a per-request
|
|
661
|
-
* credential scope, a test double. Deliberately the longer name.
|
|
662
|
-
*
|
|
663
|
-
* It registers under an AUTO-ASSIGNED TAG, because §7.5 registers
|
|
664
|
-
* every constructed adapter under its instance name and
|
|
665
|
-
* `station_bound_twice` fires on a second binding of one name: a
|
|
666
|
-
* second `create('stripe')` would otherwise throw, which is exactly
|
|
667
|
-
* the per-request case this exists for. The tag is the lowest unused
|
|
668
|
-
* positive integer, so an auto-tagged instance is an ORDINARY
|
|
669
|
-
* instance rather than a parallel identity scheme - `plugins()`, the
|
|
670
|
-
* placeholder, the event stream and `station_bound_twice` all keep
|
|
671
|
-
* working on one identity model.
|
|
672
|
-
*
|
|
673
|
-
* The SECRET NAME does not follow the assigned tag: it resolves from
|
|
674
|
-
* the DECLARED instance the tag was assigned under, so every client
|
|
675
|
-
* of one instance shares one broker cache entry rather than
|
|
676
|
-
* re-resolving per request (§5.3). */
|
|
677
453
|
create(name, overrides) {
|
|
678
454
|
return this.build(name, this.autotag(name), overrides);
|
|
679
455
|
}
|
|
680
|
-
/** The lowest positive integer tag not already taken, by a LIVE
|
|
681
|
-
* instance or a DECLARED one.
|
|
682
|
-
*
|
|
683
|
-
* The registry alone was not enough: a profile may declare
|
|
684
|
-
* `stripe$1`, and until something constructs it `registry.has` says
|
|
685
|
-
* false — so `create('stripe$prod')` took that identity for a client
|
|
686
|
-
* built from the `stripe$prod` block. `instances()` then reported the
|
|
687
|
-
* declared `stripe$1` as live with the wrong client, and a later
|
|
688
|
-
* `sdk('stripe$1')` failed `station_bound_twice` against a binding
|
|
689
|
-
* that was never its own. Declaration reserves the name whether or
|
|
690
|
-
* not it has been built. */
|
|
691
456
|
autotag(name) {
|
|
692
|
-
const api = (0,
|
|
457
|
+
const api = (0, profile_1.refapi)(name);
|
|
693
458
|
for (let n = 1;; n++) {
|
|
694
459
|
const ref = api + '$' + n;
|
|
695
460
|
if (!this.registry.has(ref) && null == this.profile.sdk[ref]) {
|
|
@@ -710,38 +475,12 @@ class Station {
|
|
|
710
475
|
throw new error_1.StationError('station_instance_inactive', 'instance "' + name + '" is declared with `active: false`, which ' +
|
|
711
476
|
'bars it from running while keeping it visible in instances()');
|
|
712
477
|
}
|
|
713
|
-
const api = (0,
|
|
478
|
+
const api = (0, profile_1.refapi)(name);
|
|
714
479
|
const entry = this.resolveFactory(api, block);
|
|
715
|
-
// §8.5 VALIDATES HERE, not only in `check()`. The schema arrives
|
|
716
|
-
// with the factory, so the moment a factory is resolved is the
|
|
717
|
-
// first moment validation is possible — and it was running in
|
|
718
|
-
// `check()` alone. Two gaps followed: production `sdk()` silently
|
|
719
|
-
// ignored an unknown option like `retry.retires`, and `check()`
|
|
720
|
-
// itself missed the case where the factory is discovered by the
|
|
721
|
-
// LOADER (its pre-check sees no registered factory, then `sdk()`
|
|
722
|
-
// loads and constructs unvalidated). One call here closes both,
|
|
723
|
-
// because every path to a constructor comes through this line.
|
|
724
480
|
const faults = (0, feature_1.checkfeatures)(this.featuresOf(name).merged, entry.descriptor);
|
|
725
481
|
if (0 < faults.length) {
|
|
726
|
-
// `Fault.code` is a plain string in `feature.ts`; the two codes it
|
|
727
|
-
// actually produces are both in the union, and `check()` passes
|
|
728
|
-
// the same value through untyped. Narrowed here rather than
|
|
729
|
-
// widening the union, so a new fault code is a compile error at
|
|
730
|
-
// the place that has to decide what it means.
|
|
731
482
|
throw new error_1.StationError(faults[0].code, faults.map((f) => f.message).join('; '));
|
|
732
483
|
}
|
|
733
|
-
// §8.4: compose the merged feature map into the ORDERED form and
|
|
734
|
-
// hand it to the constructor. No new seam - it is the same
|
|
735
|
-
// `options.feature` map `connect()` already uses for station's own
|
|
736
|
-
// placement, with more in it, and a JS object preserves the
|
|
737
|
-
// insertion order of string keys so the order rides the map.
|
|
738
|
-
//
|
|
739
|
-
// Station's own entry is composed AFTER the user merge and always
|
|
740
|
-
// wins (§8.4), which is why `station` is dropped here and added by
|
|
741
|
-
// `options()`: a config file that can switch off the component
|
|
742
|
-
// reading it is not a surface, it is a trap. `feature.station` is
|
|
743
|
-
// already `station_feature_reserved` at validation, so this is the
|
|
744
|
-
// second half of one rule rather than a second rule.
|
|
745
484
|
const resolved = this.featuresOf(name);
|
|
746
485
|
const fmap = {};
|
|
747
486
|
for (const f of (0, feature_1.composefeatures)((0, feature_1.resolveorder)(resolved.merged).filter((o) => 'station' !== o.name))) {
|
|
@@ -754,38 +493,9 @@ class Station {
|
|
|
754
493
|
...(overrides || {}),
|
|
755
494
|
feature: { ...fmap, ...((overrides || {}).feature || {}) },
|
|
756
495
|
};
|
|
757
|
-
// §5.3, and `create`'s own doc comment: "the SECRET NAME does not
|
|
758
|
-
// follow the assigned tag: it resolves from the DECLARED instance
|
|
759
|
-
// the tag was assigned under."
|
|
760
|
-
//
|
|
761
|
-
// THE ALIAS IS RECORDED, NOT THE FIELDS. The first fix for this
|
|
762
|
-
// carried the declared `secret` through the feature options and
|
|
763
|
-
// stopped there — which left `policy`, `base` and everything else
|
|
764
|
-
// behind, so an auto-tagged client silently lost its declared
|
|
765
|
-
// instance's HOSTS ALLOWLIST and fell back to the wider api-level
|
|
766
|
-
// one. Carrying configuration a field at a time is how that
|
|
767
|
-
// happens; recording what the tag STANDS FOR is one rule that every
|
|
768
|
-
// lookup already goes through.
|
|
769
|
-
//
|
|
770
|
-
// Only when the tag was ASSIGNED — a caller naming its own is
|
|
771
|
-
// naming an instance, not aliasing one.
|
|
772
496
|
if (null != as && as !== name) {
|
|
773
497
|
this.aliasOf.set(as, name);
|
|
774
498
|
}
|
|
775
|
-
// ...AND THE CARRIED ADAPTER RIDES EXTEND, exactly as it does on
|
|
776
|
-
// `connect`. §3.1's retrofit case — an SDK generated before the
|
|
777
|
-
// station feature, which `factoryFromModule` explicitly supports —
|
|
778
|
-
// has no generated feature to consume the `feature.station`
|
|
779
|
-
// activation this path sets, so declarative `sdk()` either failed
|
|
780
|
-
// on an unknown feature or returned an unregistered, unwrapped
|
|
781
|
-
// client with no credential injection and no events. The imperative
|
|
782
|
-
// path carried it and the declarative one did not, which is the
|
|
783
|
-
// whole defect.
|
|
784
|
-
//
|
|
785
|
-
// Safe on a REGENERATED SDK too: the constructor uses its own
|
|
786
|
-
// station feature and skips the extend copy by name, and both
|
|
787
|
-
// delegate to `featureBinding`, whose `_boundEntry` check no-ops a
|
|
788
|
-
// second arrival for the same client.
|
|
789
499
|
const withAdapter = {
|
|
790
500
|
...opts,
|
|
791
501
|
extend: [...(opts.extend || []), (0, adapter_1.adapterFeature)(this, opts)],
|
|
@@ -794,8 +504,6 @@ class Station {
|
|
|
794
504
|
// imperative path, so registration has one spelling (§7.5).
|
|
795
505
|
return entry.construct(this.options(as ?? name, withAdapter));
|
|
796
506
|
}
|
|
797
|
-
/** §6.2's three paths, in order of preference: self-registration,
|
|
798
|
-
* `Station.provide`, then the loader. */
|
|
799
507
|
resolveFactory(api, block) {
|
|
800
508
|
const direct = (0, factory_1.factoryFor)(api);
|
|
801
509
|
if (null != direct) {
|
|
@@ -813,9 +521,6 @@ class Station {
|
|
|
813
521
|
'that self-registers, call Station.provide("' + api + '", ...), or ' +
|
|
814
522
|
'set `api.' + api + '.package` so the loader can import it');
|
|
815
523
|
}
|
|
816
|
-
/** `package` is honoured only from repo-scoped config (§6.3), and a
|
|
817
|
-
* user-level one is IGNORED WITH A WARNING rather than imported - it
|
|
818
|
-
* names code to load and sits outside the repo's review boundary. */
|
|
819
524
|
loaderPackage(api, block) {
|
|
820
525
|
const pkg = block.package;
|
|
821
526
|
if (null == pkg || '' === pkg) {
|
|
@@ -849,7 +554,7 @@ class Station {
|
|
|
849
554
|
if (false === block.active) {
|
|
850
555
|
continue;
|
|
851
556
|
}
|
|
852
|
-
const api = (0,
|
|
557
|
+
const api = (0, profile_1.refapi)(name);
|
|
853
558
|
if (null != (0, factory_1.factoryFor)(api)) {
|
|
854
559
|
continue;
|
|
855
560
|
}
|
|
@@ -860,14 +565,8 @@ class Station {
|
|
|
860
565
|
await (0, loader_1.loadAsync)(api, pkg, block.export);
|
|
861
566
|
}
|
|
862
567
|
}
|
|
863
|
-
/** The merged, ordered feature set for one instance, WITH
|
|
864
|
-
* PROVENANCE (§8.7): which config level set each value.
|
|
865
|
-
*
|
|
866
|
-
* Provenance is the half that makes a fleet view usable rather than
|
|
867
|
-
* merely correct - at 26 instances "why is retry off here" is the
|
|
868
|
-
* question, and a merged map alone cannot answer it. */
|
|
869
568
|
featuresOf(name) {
|
|
870
|
-
const api = (0,
|
|
569
|
+
const api = (0, profile_1.refapi)(name);
|
|
871
570
|
const profiles = (this.raw?.profiles || {});
|
|
872
571
|
const base = profiles['default'] || {};
|
|
873
572
|
const overlay = 'default' === this.profile.name
|
|
@@ -879,8 +578,6 @@ class Station {
|
|
|
879
578
|
this.profile.name + '.sdk',
|
|
880
579
|
];
|
|
881
580
|
const sources = (0, feature_1.featuresources)(base, overlay, api, name);
|
|
882
|
-
// Last writer per (feature, key) wins, and the level that wrote it
|
|
883
|
-
// is what `from` records.
|
|
884
581
|
const from = {};
|
|
885
582
|
sources.forEach((src, i) => {
|
|
886
583
|
if (null == src || 'object' !== typeof src) {
|
|
@@ -898,23 +595,6 @@ class Station {
|
|
|
898
595
|
}
|
|
899
596
|
});
|
|
900
597
|
let merged = (0, feature_1.mergefeatures)(sources);
|
|
901
|
-
// Policy budget (design §16): rps/concurrency ceilings ride "the
|
|
902
|
-
// SDK `ratelimit` feature, configured by station". Composed HERE,
|
|
903
|
-
// into the merged map every consumer reads, rather than patched in
|
|
904
|
-
// at construction alone - so `build()` orders it with the ordinary
|
|
905
|
-
// constraint-and-band rules (ordering and the station pin hold),
|
|
906
|
-
// `check()`'s §8.5 pass validates it against the SDK's own
|
|
907
|
-
// declaration (a budget on an SDK with no ratelimit feature is
|
|
908
|
-
// `station_feature_unknown`, not a setting that quietly did
|
|
909
|
-
// nothing), and the §8.7 fleet view answers "is ratelimit on?"
|
|
910
|
-
// truthfully, with `policy.budget` as the provenance level.
|
|
911
|
-
//
|
|
912
|
-
// `rps` maps to the token bucket's refill `rate` (per second -
|
|
913
|
-
// the same unit); `concurrency` to its capacity `burst`, the
|
|
914
|
-
// number of requests that can be in flight from a full bucket.
|
|
915
|
-
// POLICY WINS over a `feature.ratelimit` config entry on the keys
|
|
916
|
-
// it sets - it is enforcement, not a default - and other tuning
|
|
917
|
-
// keys survive beside it.
|
|
918
598
|
const budget = this.blockFor(name)?.policy?.budget;
|
|
919
599
|
if (null != budget && 'object' === typeof budget && !Array.isArray(budget)) {
|
|
920
600
|
const prior = merged.ratelimit;
|
|
@@ -934,18 +614,6 @@ class Station {
|
|
|
934
614
|
}
|
|
935
615
|
merged = { ...merged, ratelimit: entry };
|
|
936
616
|
}
|
|
937
|
-
// THE IMPLICIT STATION ENTRY, added for ORDERING ONLY. `station` is
|
|
938
|
-
// never in `merged` — `feature.station` is reserved and rejected at
|
|
939
|
-
// validation (§8.4) — so `checkpin` found no station row and
|
|
940
|
-
// returned immediately, every time. It was a permanent no-op: a
|
|
941
|
-
// constraint like `retry.order.after: 'station'` was treated as
|
|
942
|
-
// vacuous rather than rejected, and the reported order omitted the
|
|
943
|
-
// one feature whose position is supposedly pinned.
|
|
944
|
-
//
|
|
945
|
-
// Added here rather than into `merged`, which stays the user's own
|
|
946
|
-
// merge result. `build` already filters `station` out of the map it
|
|
947
|
-
// hands the constructor, because §8.4 composes station's entry
|
|
948
|
-
// after the user merge and always wins.
|
|
949
617
|
const ordered = (0, feature_1.resolveorder)({ ...merged, station: { active: true } });
|
|
950
618
|
(0, feature_1.checkpin)(ordered);
|
|
951
619
|
return { ordered: ordered.map((o) => o.name), merged, from };
|
|
@@ -953,16 +621,6 @@ class Station {
|
|
|
953
621
|
/** The fleet feature view: instance x feature, effective options, and
|
|
954
622
|
* which config level set each (§8.7). */
|
|
955
623
|
features(filter) {
|
|
956
|
-
// §8.7's documented shape is an OBJECT, and only the object form
|
|
957
|
-
// can express the question the view exists for: `{feature: 'debug'}`
|
|
958
|
-
// — "is debug on anywhere?", the one that is twenty greps today.
|
|
959
|
-
// The implementation took a bare string and compared it against
|
|
960
|
-
// both `name` and `api`, so every documented call returned an empty
|
|
961
|
-
// list, an object never equalling either string.
|
|
962
|
-
//
|
|
963
|
-
// The string form is kept as shorthand for "this instance or this
|
|
964
|
-
// api", because it is what the tests and the imperative path
|
|
965
|
-
// already use and it costs one line.
|
|
966
624
|
const f = 'string' === typeof filter
|
|
967
625
|
? { instance: filter, api: filter, loose: true }
|
|
968
626
|
: { ...(filter || {}), loose: false };
|
|
@@ -996,10 +654,6 @@ class Station {
|
|
|
996
654
|
from: { [want]: row.from[want] || {} },
|
|
997
655
|
}));
|
|
998
656
|
}
|
|
999
|
-
/** Eagerly resolve and construct every ACTIVE instance - for CI
|
|
1000
|
-
* (§6.6). The point is to turn availability errors, which are
|
|
1001
|
-
* deliberately deferred to first use, into one failure at a moment
|
|
1002
|
-
* somebody is watching. */
|
|
1003
657
|
check() {
|
|
1004
658
|
const ok = [];
|
|
1005
659
|
const failed = [];
|
|
@@ -1008,10 +662,6 @@ class Station {
|
|
|
1008
662
|
continue;
|
|
1009
663
|
}
|
|
1010
664
|
try {
|
|
1011
|
-
// §8.5 runs FIRST and needs no construction: the schema arrives
|
|
1012
|
-
// with the factory, not with a live client, so a feature typo
|
|
1013
|
-
// is a CI failure rather than a setting that quietly did
|
|
1014
|
-
// nothing in production.
|
|
1015
665
|
const entry = (0, factory_1.factoryFor)(row.api);
|
|
1016
666
|
if (null != entry) {
|
|
1017
667
|
const faults = (0, feature_1.checkfeatures)(this.featuresOf(row.name).merged, entry.descriptor);
|
|
@@ -1034,42 +684,14 @@ class Station {
|
|
|
1034
684
|
}
|
|
1035
685
|
return { ok, failed };
|
|
1036
686
|
}
|
|
1037
|
-
/** Batch-resolve secrets for ACTIVE instances (§5.5).
|
|
1038
|
-
*
|
|
1039
|
-
* With no argument it warms the active ones only, because reaching
|
|
1040
|
-
* for a credential belonging to a disabled integration is the wrong
|
|
1041
|
-
* default. `warm(names)` warms exactly what it is given, inactive
|
|
1042
|
-
* included, because an explicit name is an explicit request. */
|
|
1043
687
|
async warm(names) {
|
|
1044
688
|
const wanted = null != names
|
|
1045
689
|
? names
|
|
1046
690
|
: this.instances().filter((r) => r.active).map((r) => r.name);
|
|
1047
|
-
// CONCURRENT, not serial. Awaiting inside the loop made `warm`
|
|
1048
|
-
// cost the SUM of every provider round-trip, which defeats the one
|
|
1049
|
-
// thing the method exists for: resolving the fleet in a batch at
|
|
1050
|
-
// startup rather than paying per first-request. sekreto exposes no
|
|
1051
|
-
// batch `get`, so concurrency is the available shape.
|
|
1052
|
-
//
|
|
1053
|
-
// Names are deduped first: the broker's resolution cache is keyed
|
|
1054
|
-
// by SECRET NAME (§5.3), so several instances sharing one api-level
|
|
1055
|
-
// `secret` should cost one round-trip — and firing them together
|
|
1056
|
-
// would race past the cache and make several.
|
|
1057
691
|
const plan = [];
|
|
1058
692
|
const warmed = [];
|
|
1059
693
|
const missed = [];
|
|
1060
694
|
for (const name of wanted) {
|
|
1061
|
-
// THE REGISTRY IS THE AUTHORITY, and asking the profile instead
|
|
1062
|
-
// was wrong twice: an IMPERATIVE instance has no `profile.sdk`
|
|
1063
|
-
// block, so it was pushed to `missed` and never warmed at all —
|
|
1064
|
-
// and the re-derivation dropped the in-code `secret` feature
|
|
1065
|
-
// option, which beats the profile (§9). A registered instance
|
|
1066
|
-
// already carries the resolved name.
|
|
1067
|
-
// A NAME NOBODY DECLARED OR REGISTERED IS A MISS, not a lookup.
|
|
1068
|
-
// Widening the fallback to cover imperative instances also let a
|
|
1069
|
-
// typo — `stripe$prodd` — derive a secret name and call the
|
|
1070
|
-
// provider, so a nonexistent instance could be reported `warmed`
|
|
1071
|
-
// off a shared api-level credential. Registered OR declared, and
|
|
1072
|
-
// nothing else.
|
|
1073
695
|
const entry = this.registry.get(name);
|
|
1074
696
|
if (null == entry && null == this.profile.sdk[name]) {
|
|
1075
697
|
missed.push(name);
|
|
@@ -1120,7 +742,7 @@ class Station {
|
|
|
1120
742
|
const entry = this.registry.get(name);
|
|
1121
743
|
return {
|
|
1122
744
|
name,
|
|
1123
|
-
api: (0,
|
|
745
|
+
api: (0, profile_1.refapi)(name),
|
|
1124
746
|
// `active: false` means BARRED FROM RUNNING - a declaration that
|
|
1125
747
|
// stays in the file and here while being refused a client.
|
|
1126
748
|
active: false !== sdk[name].active,
|
|
@@ -1153,12 +775,6 @@ class Station {
|
|
|
1153
775
|
return {
|
|
1154
776
|
mode: 'solo',
|
|
1155
777
|
profile: this.profile.name,
|
|
1156
|
-
// §7.1: the registry is keyed by INSTANCE, so a status page that
|
|
1157
|
-
// projects only `slug` shows two indistinguishable rows for
|
|
1158
|
-
// `stripe$test` and `stripe$live` and omits the names it is keyed
|
|
1159
|
-
// by — an operator cannot tell which one is unhealthy. `slug`
|
|
1160
|
-
// stays for compatibility; `name` and `api` are what answer the
|
|
1161
|
-
// question.
|
|
1162
778
|
plugins: this.plugins().map((p) => ({
|
|
1163
779
|
name: p.name, api: p.api, slug: p.slug, rung: p.rung,
|
|
1164
780
|
})),
|
|
@@ -1171,10 +787,6 @@ class Station {
|
|
|
1171
787
|
refreshSecrets() {
|
|
1172
788
|
this.broker.refresh();
|
|
1173
789
|
}
|
|
1174
|
-
// close(): flush (solo: nothing in flight), then warn on profile
|
|
1175
|
-
// plugin keys that matched no registered plugin - a typo'd key
|
|
1176
|
-
// silently configuring nothing is the worst outcome for a
|
|
1177
|
-
// secrets-and-policy file (design §11).
|
|
1178
790
|
close() {
|
|
1179
791
|
if (this.closed) {
|
|
1180
792
|
return;
|