@voxgig/station 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/dist/src/Station.d.ts +238 -0
  4. package/dist/src/Station.js +1213 -0
  5. package/dist/src/Station.js.map +1 -0
  6. package/dist/src/adapter.d.ts +9 -0
  7. package/dist/src/adapter.js +171 -0
  8. package/dist/src/adapter.js.map +1 -0
  9. package/dist/src/config-shape.d.ts +1 -0
  10. package/dist/src/config-shape.js +329 -0
  11. package/dist/src/config-shape.js.map +1 -0
  12. package/dist/src/descriptor.d.ts +8 -0
  13. package/dist/src/descriptor.js +170 -0
  14. package/dist/src/descriptor.js.map +1 -0
  15. package/dist/src/error.d.ts +8 -0
  16. package/dist/src/error.js +60 -0
  17. package/dist/src/error.js.map +1 -0
  18. package/dist/src/events.d.ts +15 -0
  19. package/dist/src/events.js +46 -0
  20. package/dist/src/events.js.map +1 -0
  21. package/dist/src/factory.d.ts +24 -0
  22. package/dist/src/factory.js +79 -0
  23. package/dist/src/factory.js.map +1 -0
  24. package/dist/src/feature.d.ts +106 -0
  25. package/dist/src/feature.js +324 -0
  26. package/dist/src/feature.js.map +1 -0
  27. package/dist/src/index.browser.d.ts +14 -0
  28. package/dist/src/index.browser.js +64 -0
  29. package/dist/src/index.browser.js.map +1 -0
  30. package/dist/src/index.d.ts +14 -0
  31. package/dist/src/index.js +56 -0
  32. package/dist/src/index.js.map +1 -0
  33. package/dist/src/loader.d.ts +45 -0
  34. package/dist/src/loader.js +181 -0
  35. package/dist/src/loader.js.map +1 -0
  36. package/dist/src/profile.d.ts +6 -0
  37. package/dist/src/profile.js +77 -0
  38. package/dist/src/profile.js.map +1 -0
  39. package/dist/src/profilecore.d.ts +10 -0
  40. package/dist/src/profilecore.js +180 -0
  41. package/dist/src/profilecore.js.map +1 -0
  42. package/dist/src/secrets.d.ts +12 -0
  43. package/dist/src/secrets.js +93 -0
  44. package/dist/src/secrets.js.map +1 -0
  45. package/dist/src/shape.d.ts +42 -0
  46. package/dist/src/shape.js +444 -0
  47. package/dist/src/shape.js.map +1 -0
  48. package/dist/src/types.d.ts +138 -0
  49. package/dist/src/types.js +5 -0
  50. package/dist/src/types.js.map +1 -0
  51. package/package.json +60 -0
  52. package/src/Station.ts +1310 -0
  53. package/src/adapter.ts +183 -0
  54. package/src/config-shape.ts +326 -0
  55. package/src/descriptor.ts +179 -0
  56. package/src/error.ts +64 -0
  57. package/src/events.ts +45 -0
  58. package/src/factory.ts +93 -0
  59. package/src/feature.ts +321 -0
  60. package/src/index.browser.ts +56 -0
  61. package/src/index.ts +50 -0
  62. package/src/loader.ts +188 -0
  63. package/src/profile.ts +67 -0
  64. package/src/profilecore.ts +201 -0
  65. package/src/secrets.ts +96 -0
  66. package/src/shape.ts +435 -0
  67. package/src/types.ts +185 -0
package/src/error.ts ADDED
@@ -0,0 +1,64 @@
1
+ // Error codes follow the SDKs' house grammar (design §14):
2
+ // <subject>_<condition>, absence as no_<thing>, gates as _allow.
3
+ // The `errors` corpus section pins the exact strings.
4
+
5
+ const CODES = [
6
+ 'station_no_proxy',
7
+ 'station_secret_no_value',
8
+ 'station_secret_error',
9
+ 'station_secret_name',
10
+ 'station_host_allow',
11
+ 'station_grant_expired',
12
+ 'station_wrap_order',
13
+ 'station_protocol',
14
+ 'station_no_plugin',
15
+ 'station_no_entity',
16
+ 'station_no_op',
17
+ 'station_agent_allow',
18
+ 'station_body_limit',
19
+ 'station_replay_lossy',
20
+ 'station_open_conflict',
21
+ 'station_bound_twice',
22
+
23
+ // Declarative config (station-declarative-config.md §6.4). Stage 1
24
+ // raises the first three; `station_feature_reserved` is the pure-data
25
+ // half of §8.4/§8.6, checkable before feature.ts exists because both
26
+ // rules are lexical - a `station` key in a feature map, a `feature`
27
+ // key inside `options`.
28
+ 'station_config_invalid',
29
+ 'station_config_secret',
30
+ 'station_secret_collision',
31
+ 'station_feature_reserved',
32
+
33
+ // Instances (design §6.4). `as` is a tag, not a free name: a full ref
34
+ // whose name is not the SDK's api slug is this.
35
+ 'station_instance_api',
36
+ 'station_no_instance',
37
+ 'station_instance_inactive',
38
+ 'station_sdk_load',
39
+ 'station_no_factory',
40
+ 'station_factory_conflict',
41
+
42
+ // Features (design §8.4, §8.5). The checker is derived from the SDK's
43
+ // own declaration, so a feature typo is a CI failure rather than a
44
+ // setting that quietly did nothing in production.
45
+ 'station_feature_unknown',
46
+ 'station_feature_option',
47
+ 'station_feature_order',
48
+ ] as const
49
+
50
+ export type StationErrorCode = typeof CODES[number]
51
+
52
+ export class StationError extends Error {
53
+ code: StationErrorCode
54
+
55
+ constructor(code: StationErrorCode, message: string) {
56
+ super(code + ': ' + message)
57
+ this.name = 'StationError'
58
+ this.code = code
59
+ }
60
+ }
61
+
62
+ export function isKnownCode(code: string): boolean {
63
+ return (CODES as readonly string[]).includes(code)
64
+ }
package/src/events.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { StationEvent } from './types'
2
+
3
+ // The solo event surface (design §6): a bounded ring buffer plus a live
4
+ // tap with serialized callbacks. Events never fail an operation; overflow
5
+ // drops oldest and the drop count is visible in status().
6
+
7
+ export class EventBuffer {
8
+ private ring: StationEvent[] = []
9
+ private max: number
10
+ private drops = 0
11
+ private taps: ((ev: StationEvent) => void)[] = []
12
+
13
+ constructor(max?: number) {
14
+ this.max = null == max ? 1000 : max
15
+ }
16
+
17
+ emit(ev: StationEvent): void {
18
+ this.ring.push(ev)
19
+ if (this.ring.length > this.max) {
20
+ this.ring.shift()
21
+ this.drops++
22
+ }
23
+ // Serialized, and a throwing tap must not fail the operation that
24
+ // emitted the event.
25
+ for (const fn of this.taps) {
26
+ try { fn(ev) } catch (_e) { }
27
+ }
28
+ }
29
+
30
+ events(): StationEvent[] {
31
+ return this.ring.slice()
32
+ }
33
+
34
+ tap(fn: (ev: StationEvent) => void): () => void {
35
+ this.taps.push(fn)
36
+ return () => {
37
+ const i = this.taps.indexOf(fn)
38
+ if (-1 !== i) { this.taps.splice(i, 1) }
39
+ }
40
+ }
41
+
42
+ status(): { buffered: number, dropped: number } {
43
+ return { buffered: this.ring.length, dropped: this.drops }
44
+ }
45
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,93 @@
1
+ /* The factory table (design §6.2).
2
+ *
3
+ * A FACTORY IS A CONSTRUCTOR *PLUS* THE SDK'S STATIC CONFIG, not a bare
4
+ * function, and leaving the second half out was a hole in the first
5
+ * draft of §6.2.
6
+ *
7
+ * Station composes the ordered feature array FOR the constructor, so it
8
+ * needs the transport roles and the feature option schemas BEFORE
9
+ * construction — but §7.5 has the adapter build and register its
10
+ * descriptor DURING construction. Nothing would be known in time.
11
+ *
12
+ * The config is available, though: `configDefinition` emits it as a
13
+ * module-level constant in the generated package, so it exists as soon
14
+ * as the package is linked and long before any instance is built.
15
+ * Station normalizes the descriptor AT PROVIDE TIME, and three things
16
+ * follow:
17
+ *
18
+ * - §7.4's per-api descriptor cache is populated at registration
19
+ * rather than on first construction;
20
+ * - `check()` can validate every instance's feature config WITHOUT
21
+ * constructing anything;
22
+ * - the adapter's registration during construction becomes a
23
+ * reconciliation — same descriptor, now bound to a live client —
24
+ * rather than the first sighting.
25
+ *
26
+ * The table is PROCESS-GLOBAL because path 1 of §6.2 is module
27
+ * self-registration: a generated package registers itself when it is
28
+ * linked, which happens once per process and not once per Station. */
29
+
30
+ import { StationError } from './error'
31
+ import { normalizeDescriptor } from './descriptor'
32
+
33
+ export type Factory = {
34
+ construct: (options: any) => any
35
+ config: any
36
+ }
37
+
38
+ export type FactoryEntry = {
39
+ api: string
40
+ construct: (options: any) => any
41
+ config: any
42
+ descriptor: any
43
+ warnings: string[]
44
+ }
45
+
46
+ const TABLE = new Map<string, FactoryEntry>()
47
+
48
+ /** Register an api's `{construct, config}` pair.
49
+ *
50
+ * Idempotent per api: registering the SAME pair twice is a no-op,
51
+ * because module self-registration and an explicit `provide` for one
52
+ * api is an ordinary thing for an application to end up with. A second
53
+ * registration with a DIFFERENT factory is `station_factory_conflict` —
54
+ * silently picking one of two SDK builds is not a thing to do quietly. */
55
+ export function provide(api: string, factory: Factory): FactoryEntry {
56
+ const slug = String(api)
57
+ const prior = TABLE.get(slug)
58
+ if (null != prior) {
59
+ if (prior.construct === factory.construct && prior.config === factory.config) {
60
+ return prior
61
+ }
62
+ throw new StationError('station_factory_conflict',
63
+ 'two different factories registered for api "' + slug + '"; a ' +
64
+ 'process has one build of an SDK, and picking between two ' +
65
+ 'silently is not a thing to do quietly')
66
+ }
67
+
68
+ // AT PROVIDE TIME, which is the whole point of carrying `config`.
69
+ const { descriptor, warnings } = normalizeDescriptor(factory.config, undefined)
70
+ const entry: FactoryEntry = {
71
+ api: slug,
72
+ construct: factory.construct,
73
+ config: factory.config,
74
+ descriptor,
75
+ warnings,
76
+ }
77
+ TABLE.set(slug, entry)
78
+ return entry
79
+ }
80
+
81
+ export function factoryFor(api: string): FactoryEntry | undefined {
82
+ return TABLE.get(String(api))
83
+ }
84
+
85
+ export function provided(): string[] {
86
+ return Array.from(TABLE.keys()).sort()
87
+ }
88
+
89
+ /** Test seam. The table is process-global by design, so a suite that
90
+ * registers factories has to be able to put the process back. */
91
+ export function resetFactories(): void {
92
+ TABLE.clear()
93
+ }
package/src/feature.ts ADDED
@@ -0,0 +1,321 @@
1
+ /* Feature management (design §8): the three-level merge, the
2
+ * constraint-and-band resolver, and the descriptor-derived checker.
3
+ *
4
+ * The resolver is written to voxgig/plugin's §7 semantics so plugin can
5
+ * extract it — this is one of the pieces the joint plan means by
6
+ * "station builds natively to plugin's semantics". Keeping the two
7
+ * spellings identical is what C4 holds station to. */
8
+
9
+ import { StationError } from './error'
10
+
11
+ // ---------------------------------------------------------------------
12
+ // §8.3 — the merge
13
+ // ---------------------------------------------------------------------
14
+
15
+ /** Reserved on a feature entry: not options, and never passed through
16
+ * to the SDK's own option map. */
17
+ export const RESERVED_KEYS = ['active', 'order']
18
+
19
+ /** The six sources, in §3.3's order extended by the profile level:
20
+ *
21
+ * 1 base.feature 4 overlay.feature
22
+ * 2 base.api[<api>].feature 5 overlay.api[<api>].feature
23
+ * 3 base.sdk[<ref>].feature 6 overlay.sdk[<ref>].feature
24
+ *
25
+ * PROFILE SPECIFICITY OUTRANKS BLOCK SPECIFICITY, and within a profile
26
+ * the narrower block wins — the same principle as §3.3, one level down.
27
+ *
28
+ * `feature` is the ONE key where §3.3's shallow-per-key rule is wrong:
29
+ * composition is the entire point, a fleet default plus a per-instance
30
+ * tweak. So it is a TWO-LEVEL merge — per feature name, then per option
31
+ * key — and no deeper. A map-valued option REPLACES wholesale, which is
32
+ * what `{"$MERGE": {"deep": 2}}` states and what a port defaulting to a
33
+ * deep merge would silently get wrong.
34
+ *
35
+ * Same defaults-after-merge rule as §3.3, one level down: an entry
36
+ * mentioned at one level with only a tuning key must NOT synthesize
37
+ * `active` and switch on a feature a broader level turned off. That is
38
+ * the §3.3 defect one level down, and it is why the caller passes RAW
39
+ * blocks here. */
40
+ export function mergefeatures(sources: any[]): { [name: string]: any } {
41
+ const out: { [name: string]: any } = {}
42
+ for (const src of sources) {
43
+ if (!ismap(src)) { continue }
44
+ for (const name of Object.keys(src)) {
45
+ const entry = src[name]
46
+ if (!ismap(entry)) { out[name] = entry; continue }
47
+ // Per option key, and NOT deeper.
48
+ out[name] = { ...(ismap(out[name]) ? out[name] : {}), ...entry }
49
+ }
50
+ }
51
+ return out
52
+ }
53
+
54
+ /** The six sources for one instance, in order. Assembled here rather
55
+ * than at the call site so the order lives in exactly one place. */
56
+ export function featuresources(
57
+ base: any, overlay: any, api: string, ref: string
58
+ ): any[] {
59
+ return [
60
+ base?.feature,
61
+ base?.api?.[api]?.feature,
62
+ base?.sdk?.[ref]?.feature,
63
+ overlay?.feature,
64
+ overlay?.api?.[api]?.feature,
65
+ overlay?.sdk?.[ref]?.feature,
66
+ ]
67
+ }
68
+
69
+ // ---------------------------------------------------------------------
70
+ // §8.4 — activation and order
71
+ // ---------------------------------------------------------------------
72
+
73
+ /** `test` substitutes the base transport, so it takes the innermost
74
+ * band; `station` sits immediately outside it, pinned; everything else
75
+ * is band 0, outside station.
76
+ *
77
+ * THE DEFAULT IS TODAY'S BEHAVIOUR EXPRESSED IN THE NEW MODEL rather
78
+ * than as a special case: a project that writes no `order` anywhere
79
+ * sees exactly today's nesting, and sdkgen's two `makeOptions` special
80
+ * cases become two band values rather than two branches. */
81
+ export const BAND_DEFAULT = 0
82
+ export const BAND_STATION = 100
83
+ export const BAND_TEST = 200
84
+
85
+ /** Higher is further IN. */
86
+ export function defaultband(name: string): number {
87
+ if ('test' === name) { return BAND_TEST }
88
+ if ('station' === name) { return BAND_STATION }
89
+ return BAND_DEFAULT
90
+ }
91
+
92
+ export type Ordered = { name: string, band: number, entry: any }
93
+
94
+ /** Resolve the activation order: constraints, then bands, then the
95
+ * feature's position in the merged map.
96
+ *
97
+ * `before`/`after` take a feature name or a list of them and are
98
+ * SATISFIED VACUOUSLY when the named feature is absent — `after: 'test'`
99
+ * loads fine in a project with no test feature, which is sdkgen's
100
+ * `__after__` behaviour kept rather than reinvented.
101
+ *
102
+ * Constraints beat bands; bands break ties no constraint decides;
103
+ * remaining ties break by declaration position, so the result is a
104
+ * stable topological sort with no alphabetical accident in it.
105
+ *
106
+ * Returns OUTERMOST FIRST, which is the array form the constructor
107
+ * takes and the direction plugin's chain composes in. */
108
+ export function resolveorder(merged: { [name: string]: any }): Ordered[] {
109
+ const names = Object.keys(merged).filter((n) => active(merged[n]))
110
+ const pos = new Map<string, number>()
111
+ names.forEach((n, i) => pos.set(n, i))
112
+
113
+ const band = new Map<string, number>()
114
+ for (const n of names) {
115
+ const o = ismap(merged[n]) ? merged[n].order : undefined
116
+ const b = ismap(o) && 'number' === typeof o.band ? o.band : defaultband(n)
117
+ band.set(n, b)
118
+ }
119
+
120
+ // edges: from OUTER to INNER. `after: X` means "further in than X".
121
+ const inner = new Map<string, Set<string>>()
122
+ for (const n of names) { inner.set(n, new Set()) }
123
+
124
+ const listof = (v: any): string[] =>
125
+ null == v ? [] : (Array.isArray(v) ? v : [v]).map(String)
126
+
127
+ for (const n of names) {
128
+ const o = ismap(merged[n]) ? merged[n].order : undefined
129
+ if (!ismap(o)) { continue }
130
+ // Vacuous when absent: an unknown name is not an error here.
131
+ for (const other of listof(o.after)) {
132
+ if (inner.has(other)) { inner.get(other)!.add(n) }
133
+ }
134
+ for (const other of listof(o.before)) {
135
+ if (inner.has(other)) { inner.get(n)!.add(other) }
136
+ }
137
+ }
138
+
139
+ const indeg = new Map<string, number>()
140
+ for (const n of names) { indeg.set(n, 0) }
141
+ for (const n of names) {
142
+ for (const m of inner.get(n)!) { indeg.set(m, (indeg.get(m) || 0) + 1) }
143
+ }
144
+
145
+ // Kahn, picking the lowest band first (outermost), then declaration
146
+ // position — so ties break the same way in every port.
147
+ const ready: string[] = names.filter((n) => 0 === indeg.get(n))
148
+ const out: Ordered[] = []
149
+ const pick = (): string => {
150
+ ready.sort((a, b) => {
151
+ const d = band.get(a)! - band.get(b)!
152
+ return 0 !== d ? d : pos.get(a)! - pos.get(b)!
153
+ })
154
+ return ready.shift()!
155
+ }
156
+
157
+ while (0 < ready.length) {
158
+ const n = pick()
159
+ out.push({ name: n, band: band.get(n)!, entry: merged[n] })
160
+ for (const m of inner.get(n)!) {
161
+ indeg.set(m, indeg.get(m)! - 1)
162
+ if (0 === indeg.get(m)) { ready.push(m) }
163
+ }
164
+ }
165
+
166
+ if (out.length !== names.length) {
167
+ const stuck = names.filter((n) => !out.some((o) => o.name === n)).sort()
168
+ throw new StationError('station_feature_order',
169
+ 'feature ordering constraints form a cycle among [' +
170
+ stuck.join(', ') + ']')
171
+ }
172
+
173
+ return out
174
+ }
175
+
176
+ /** A feature named in the config is one you are ASKING for, so an entry
177
+ * with no `active` is active. */
178
+ function active(entry: any): boolean {
179
+ if (!ismap(entry)) { return false !== entry }
180
+ return false !== entry.active
181
+ }
182
+
183
+ /** Station's own position is PINNED and not orderable (§8.4): an order
184
+ * that moves `station` away from immediately-outside-the-base is
185
+ * REJECTED, not honoured.
186
+ *
187
+ * The pin is `innermost`, and the spelling matters. A chain composes
188
+ * with the FIRST binding outermost, so a pin written in sort terms —
189
+ * "station first" — would place every other wrapper between the adapter
190
+ * and the base: the exact inversion of the invariant, and one that
191
+ * would leave station's wire-truth events observing the wrong boundary
192
+ * while still looking ordered. */
193
+ export function checkpin(ordered: Ordered[]): void {
194
+ const i = ordered.findIndex((o) => 'station' === o.name)
195
+ if (-1 === i) { return }
196
+
197
+ const base = ordered.findIndex((o) => 'test' === o.name)
198
+ // station must be the innermost wrapper: last, or immediately
199
+ // outside the base-transport feature when one is active.
200
+ const want = -1 === base ? ordered.length - 1 : base - 1
201
+ if (i !== want) {
202
+ throw new StationError('station_feature_order',
203
+ 'an ordering would move `station` away from immediately outside ' +
204
+ 'the base transport; its position is pinned innermost and is not ' +
205
+ 'orderable (§8.4)')
206
+ }
207
+ }
208
+
209
+ // ---------------------------------------------------------------------
210
+ // §8.5 — the checker, derived from the descriptor
211
+ // ---------------------------------------------------------------------
212
+
213
+ export type FeatureFault = {
214
+ code: string
215
+ feature: string
216
+ key?: string
217
+ message: string
218
+ }
219
+
220
+ /** Check a merged feature map against the SDK'S OWN DECLARATION.
221
+ *
222
+ * The schema arrives with the FACTORY rather than with a live client
223
+ * (§6.2), so this needs no construction and no network — which is what
224
+ * lets `check()` run it for every instance in CI.
225
+ *
226
+ * Derived from the descriptor, never hand-written, so it cannot drift:
227
+ * when a feature gains an option, the next regeneration teaches station
228
+ * about it with no station change.
229
+ *
230
+ * SCALARS AGREE BY CONSTRUCTION; COMPOUND OPTIONS ARE KIND-CHECKED, and
231
+ * that limit is real. An empty list default says nothing reliable about
232
+ * its element type and a nested map default says nothing about its
233
+ * value shapes, so `methods: [{}]` against a `['GET']` default is
234
+ * caught while `noProxy: []` accepts anything list-shaped. Closing that
235
+ * needs the feature model to declare an explicit option schema rather
236
+ * than only a default. */
237
+ export function checkfeatures(
238
+ merged: { [name: string]: any }, descriptor: any
239
+ ): FeatureFault[] {
240
+ const faults: FeatureFault[] = []
241
+ const declared: any[] = descriptor?.features || []
242
+ const byname = new Map<string, any>()
243
+ for (const f of declared) { byname.set(String(f.name), f) }
244
+
245
+ for (const name of Object.keys(merged).sort()) {
246
+ const spec = byname.get(name)
247
+ if (null == spec) {
248
+ faults.push({
249
+ code: 'station_feature_unknown',
250
+ feature: name,
251
+ message: 'the SDK has no feature "' + name + '"; it declares [' +
252
+ Array.from(byname.keys()).sort().join(', ') + ']',
253
+ })
254
+ continue
255
+ }
256
+
257
+ const entry = merged[name]
258
+ if (!ismap(entry)) { continue }
259
+ const defaults = ismap(spec.options) ? spec.options : {}
260
+
261
+ for (const key of Object.keys(entry).sort()) {
262
+ if (-1 !== RESERVED_KEYS.indexOf(key)) { continue }
263
+
264
+ if (!(key in defaults)) {
265
+ // THE CASE THAT ACTUALLY BITES: `retry.retires: 5` is accepted
266
+ // and silently ignored today, because makeOptions' feature spec
267
+ // is `$OPEN` per feature so the SDK cannot catch it and nothing
268
+ // else looks.
269
+ faults.push({
270
+ code: 'station_feature_option',
271
+ feature: name,
272
+ key,
273
+ message: 'feature "' + name + '" declares no option "' + key +
274
+ '"; it declares [' + Object.keys(defaults).sort().join(', ') + ']',
275
+ })
276
+ continue
277
+ }
278
+
279
+ const want = kindof(defaults[key])
280
+ const got = kindof(entry[key])
281
+ if (want !== got) {
282
+ faults.push({
283
+ code: 'station_feature_option',
284
+ feature: name,
285
+ key,
286
+ message: 'feature "' + name + '" option "' + key + '" expects ' +
287
+ want + ', but found ' + got + ': ' + JSON.stringify(entry[key]),
288
+ })
289
+ }
290
+ }
291
+ }
292
+
293
+ return faults
294
+ }
295
+
296
+ /** Compose the merged map into the ORDERED ARRAY FORM the constructor
297
+ * takes. No new seam: it is what `connect()` already does for station's
298
+ * own placement, with more in it. */
299
+ export function composefeatures(ordered: Ordered[]): any[] {
300
+ return ordered.map((o) => {
301
+ const entry = ismap(o.entry) ? o.entry : {}
302
+ const out: any = { name: o.name, active: true }
303
+ for (const k of Object.keys(entry)) {
304
+ if (-1 !== RESERVED_KEYS.indexOf(k)) { continue }
305
+ out[k] = entry[k]
306
+ }
307
+ return out
308
+ })
309
+ }
310
+
311
+ function kindof(v: any): string {
312
+ if (null === v || undefined === v) { return 'null' }
313
+ if (Array.isArray(v)) { return 'list' }
314
+ if ('number' === typeof v) { return 'number' }
315
+ if ('object' === typeof v) { return 'map' }
316
+ return typeof v
317
+ }
318
+
319
+ function ismap(v: any): boolean {
320
+ return null != v && 'object' === typeof v && !Array.isArray(v)
321
+ }
@@ -0,0 +1,56 @@
1
+ // @voxgig/station - one control surface for outbound integrations.
2
+ //
3
+ // THE BROWSER ENTRY (§2.2, §17 Phase 1), selected by the `browser`
4
+ // condition in package.json `exports`. The same surface as `index.ts`
5
+ // MINUS the file-loading half - `loadConfig`, `findConfigFile` and
6
+ // `configScope` are node-only (`profile.ts`, the one module in this
7
+ // package with top-level `node:` imports) and are simply not here, so
8
+ // this entry's module graph pulls no `node:` builtin
9
+ // (`test/browser.test.ts` walks the compiled graph and asserts it).
10
+ //
11
+ // `Station.open()` on this entry takes `config` as an OBJECT - the
12
+ // pre-existing explicit-config path, which skips file loading entirely.
13
+ // With no `config` at all it behaves exactly as node does when no
14
+ // station.json exists: null config, repo-scoped. The ConfigFileIO seam
15
+ // stays unregistered here; a host that genuinely has a config store can
16
+ // register its own through `setConfigFileIO`.
17
+ //
18
+ // §2.2's honesty note stands: in the browser R1/R2 isolation does not
19
+ // exist - an injected credential is readable by page code and DevTools
20
+ // - so browser station is observability-only with app-held credentials
21
+ // until a same-origin proxy endpoint exists.
22
+
23
+ export { Station, instanceRef, setConfigFileIO } from './Station'
24
+ export type { ConfigFileIO } from './Station'
25
+ export { provide, factoryFor, provided, resetFactories } from './factory'
26
+ export type { Factory, FactoryEntry } from './factory'
27
+ export {
28
+ DEFAULT_EXPORT, camelify, checkPackage, factoryFromModule, loadAsync, loadSync,
29
+ } from './loader'
30
+ export { adapterFeature, featureBinding } from './adapter'
31
+ export type { FeatureBinding } from './adapter'
32
+ export { StationError } from './error'
33
+ export {
34
+ canonicalSerialize,
35
+ envtoken,
36
+ normalizeDescriptor,
37
+ secretnameDefault,
38
+ } from './descriptor'
39
+ export { placeholderFor } from './secrets'
40
+ export { refapi, resolveProfile, selectProfile } from './profilecore'
41
+ export type { ResolvedProfile } from './profilecore'
42
+ export {
43
+ BLOCK_DEFAULTS, MERGE_SENSITIVE, PROFILE_DEFAULTS,
44
+ configShape, normalizeConfig, validateConfig,
45
+ } from './shape'
46
+ export type {
47
+ Binding,
48
+ Descriptor,
49
+ PolicyBlock,
50
+ Profile,
51
+ ResolvedInstance,
52
+ SdkBlock,
53
+ StationConfig,
54
+ StationEvent,
55
+ StationOptions,
56
+ } from './types'
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ // @voxgig/station - one control surface for outbound integrations.
2
+ //
3
+ // THE NODE ENTRY. `index.browser.ts` is the browser condition's entry
4
+ // (§2.2, §17 Phase 1): same surface MINUS the file-loading half
5
+ // (loadConfig/findConfigFile/configScope) and this registration, which
6
+ // is what hands Station the node-only file half without Station itself
7
+ // importing a `node:` module.
8
+
9
+ import { configScope, loadConfig } from './profile'
10
+ import { setConfigFileIO } from './Station'
11
+
12
+ setConfigFileIO({ loadConfig, configScope })
13
+
14
+ export { Station, instanceRef, setConfigFileIO } from './Station'
15
+ export type { ConfigFileIO } from './Station'
16
+ export { provide, factoryFor, provided, resetFactories } from './factory'
17
+ export type { Factory, FactoryEntry } from './factory'
18
+ export {
19
+ DEFAULT_EXPORT, camelify, checkPackage, factoryFromModule, loadAsync, loadSync,
20
+ } from './loader'
21
+ export { adapterFeature, featureBinding } from './adapter'
22
+ export type { FeatureBinding } from './adapter'
23
+ export { StationError } from './error'
24
+ export {
25
+ canonicalSerialize,
26
+ envtoken,
27
+ normalizeDescriptor,
28
+ secretnameDefault,
29
+ } from './descriptor'
30
+ export { placeholderFor } from './secrets'
31
+ export {
32
+ configScope, findConfigFile, loadConfig, refapi, resolveProfile,
33
+ selectProfile,
34
+ } from './profile'
35
+ export type { ResolvedProfile } from './profile'
36
+ export {
37
+ BLOCK_DEFAULTS, MERGE_SENSITIVE, PROFILE_DEFAULTS,
38
+ configShape, normalizeConfig, validateConfig,
39
+ } from './shape'
40
+ export type {
41
+ Binding,
42
+ Descriptor,
43
+ PolicyBlock,
44
+ Profile,
45
+ ResolvedInstance,
46
+ SdkBlock,
47
+ StationConfig,
48
+ StationEvent,
49
+ StationOptions,
50
+ } from './types'