@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.
Files changed (55) hide show
  1. package/README.md +26 -32
  2. package/dist/src/Station.d.ts +1 -133
  3. package/dist/src/Station.js +46 -434
  4. package/dist/src/Station.js.map +1 -1
  5. package/dist/src/adapter.js +0 -54
  6. package/dist/src/adapter.js.map +1 -1
  7. package/dist/src/config-shape.js +0 -11
  8. package/dist/src/config-shape.js.map +1 -1
  9. package/dist/src/descriptor.js +0 -36
  10. package/dist/src/descriptor.js.map +1 -1
  11. package/dist/src/error.js +0 -5
  12. package/dist/src/error.js.map +1 -1
  13. package/dist/src/factory.d.ts +0 -9
  14. package/dist/src/factory.js +0 -37
  15. package/dist/src/factory.js.map +1 -1
  16. package/dist/src/feature.d.ts +0 -75
  17. package/dist/src/feature.js +0 -87
  18. package/dist/src/feature.js.map +1 -1
  19. package/dist/src/index.d.ts +1 -2
  20. package/dist/src/index.js +11 -22
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/loader.d.ts +0 -29
  23. package/dist/src/loader.js +0 -66
  24. package/dist/src/loader.js.map +1 -1
  25. package/dist/src/profile.d.ts +10 -3
  26. package/dist/src/profile.js +97 -23
  27. package/dist/src/profile.js.map +1 -1
  28. package/dist/src/secrets.d.ts +5 -0
  29. package/dist/src/secrets.js +106 -32
  30. package/dist/src/secrets.js.map +1 -1
  31. package/dist/src/shape.d.ts +0 -22
  32. package/dist/src/shape.js +0 -113
  33. package/dist/src/shape.js.map +1 -1
  34. package/package.json +3 -4
  35. package/src/Station.ts +34 -441
  36. package/src/adapter.ts +0 -57
  37. package/src/config-shape.ts +0 -11
  38. package/src/descriptor.ts +0 -36
  39. package/src/error.ts +0 -5
  40. package/src/factory.ts +0 -37
  41. package/src/feature.ts +0 -87
  42. package/src/index.ts +1 -14
  43. package/src/loader.ts +0 -66
  44. package/src/profile.ts +115 -23
  45. package/src/secrets.ts +104 -32
  46. package/src/shape.ts +0 -113
  47. package/src/types.ts +0 -59
  48. package/dist/src/index.browser.d.ts +0 -14
  49. package/dist/src/index.browser.js +0 -64
  50. package/dist/src/index.browser.js.map +0 -1
  51. package/dist/src/profilecore.d.ts +0 -10
  52. package/dist/src/profilecore.js +0 -180
  53. package/dist/src/profilecore.js.map +0 -1
  54. package/src/index.browser.ts +0 -56
  55. package/src/profilecore.ts +0 -201
package/src/adapter.ts CHANGED
@@ -1,34 +1,16 @@
1
1
  import { StationError } from './error'
2
2
  import { Station } from './Station'
3
3
 
4
- // The station side of the plugin contract (design §3), in ONE place:
5
- // featureBinding() is what both entry paths delegate to -
6
- // - the GENERATED station feature (sdkgen-station's ts template) calls
7
- // it from its init() and forwards its hook methods;
8
- // - the library's carried adapter (adapterFeature, the adopt/connect
9
- // retrofit for SDKs generated without the feature) is a thin shell
10
- // over the same call.
11
- // Registration at init, wrap position verified, transport wrapped with
12
- // copy-on-inject, hooks bridged to op events. Anything changed here
13
- // changes both paths - which is the point.
14
4
 
15
5
  let corrSeq = 0
16
6
 
17
7
  export type FeatureBinding = {
18
- // The INSTANCE name (§7.1). Keeps the field name so the generated
19
- // adapter contract is unchanged; for a single-instance project it is
20
- // the api slug, exactly as before.
21
8
  slug: string
22
9
  PrePoint(ctx: any): void
23
10
  PreDone(ctx: any): void
24
11
  PreUnexpected(ctx: any): void
25
12
  }
26
13
 
27
- // Resolve the station this activation binds to: an explicit handle in
28
- // the feature options (connect/adopt and st.options() pass one), else
29
- // the ambient instance. No station open -> null: an activated feature
30
- // with no opened station is an inert no-op that emits nothing and
31
- // fails nothing (design §3.1).
32
14
  export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
33
15
  const station: Station | null =
34
16
  (fopts?.station instanceof Station ? fopts.station : null) ?? Station.current()
@@ -36,18 +18,11 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
36
18
 
37
19
  const client = ctx.client
38
20
 
39
- // Same construction, second arrival (generated feature + carried
40
- // adapter both active on one client): the first bind won, this one
41
- // is inert. See Station._boundEntry.
42
21
  if (null != station._boundEntry(client)) { return null }
43
22
  const utility = ctx.utility
44
23
  const options = ctx.options
45
24
  const calleropts = fopts?.calleropts
46
25
 
47
- // Position guard (design §3.3): the wrap must sit immediately
48
- // outside the base transport - inside retry/cache/ratelimit - or
49
- // its http events stop being wire truth. Position in
50
- // client._features IS init order, so verify it and fail loudly.
51
26
  const names = client._features.map((f: any) => f.name)
52
27
  const self = names.indexOf('station')
53
28
  const testAt = names.indexOf('test')
@@ -58,15 +33,8 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
58
33
  'feature order is [' + names.join(', ') + ']')
59
34
  }
60
35
 
61
- // §7.5: registration is driven by station now. `fopts.instance` is
62
- // where station puts the instance name it knew before construction
63
- // began; `_register` reads it and falls back to the descriptor slug,
64
- // which is today's behaviour for a bare connect(SDK).
65
36
  const reg = station._register(client, ctx.config, options, calleropts, fopts)
66
37
  const { binding, profilePlugin } = reg
67
- // The INSTANCE name, not the api slug. Everything below keys on it -
68
- // the placeholder, the transport seam, the op events - because two
69
- // live instances of one api must be distinguishable at each.
70
38
  const name = binding.plugin
71
39
 
72
40
  // Base URL precedence (design §3.5): caller opts (7) beat the
@@ -78,18 +46,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
78
46
  options.base = profilePlugin.base
79
47
  }
80
48
 
81
- // Policy allowlists (design §16): `allow.op` / `allow.method` are
82
- // "the same vocabulary the SDKs already enforce (`options.allow`, and
83
- // the raw-access gate every target implements); station sets these
84
- // SDK options from policy so enforcement is in the SDK's own
85
- // pipeline" - the point_op_allow / spec_method_allow gates. The SDK's
86
- // own option form is a comma-separated string, so the policy's list
87
- // joins into it. Applied at binding time, which is inside the
88
- // constructor, and on BOTH entry paths - connect/adopt and the
89
- // declarative build both delegate here. Unlike `base` above, which is
90
- // a default the caller may override, an allowlist is ENFORCEMENT:
91
- // policy wins over whatever the options carry, on exactly the keys it
92
- // sets.
93
49
  const pallow = profilePlugin?.policy?.allow
94
50
  if (null != pallow && 'object' === typeof pallow) {
95
51
  const allow = {
@@ -104,10 +60,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
104
60
  if ('none' !== binding.rung) {
105
61
  const placeholder = binding.placeholder!
106
62
 
107
- // A real credential already resident in the options is hoisted
108
- // into the broker and replaced by the placeholder before
109
- // construction completes (design §3.1 adopt) - options() and
110
- // prepare() output become placeholder-safe from here on.
111
63
  const resident = options.apikey
112
64
  if ('string' === typeof resident && '' !== resident && placeholder !== resident) {
113
65
  station._hoist(name, resident)
@@ -115,9 +67,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
115
67
  options.apikey = placeholder
116
68
  }
117
69
 
118
- // Wrap the transport. Copy-on-inject (design §5.3) happens inside
119
- // station._transport; auth-inactive plugins skip credential
120
- // planning but the wrap still observes (design §5.3).
121
70
  const inner = utility.fetcher
122
71
  if (true === (inner as any).__station__) {
123
72
  throw new StationError('station_bound_twice',
@@ -132,8 +81,6 @@ export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
132
81
  return {
133
82
  slug: name,
134
83
 
135
- // Hook bridge (design §3 item 3): operation semantics correlated
136
- // with the HTTP events via a per-operation id on the SDK's own ctx.
137
84
  PrePoint(opctx: any): void {
138
85
  opctx.station$ = { corr: 'c' + (++corrSeq), start: Date.now() }
139
86
  },
@@ -158,10 +105,6 @@ export function adapterFeature(station: Station, calleropts: any): any {
158
105
  active: true,
159
106
  _binding: null as FeatureBinding | null,
160
107
 
161
- // featureAdd reads _options for positioning: immediately after the
162
- // test feature's base transport (design §3.3). When test is absent
163
- // from the add order this is a no-op append, which for a bare SDK
164
- // still lands the wrap immediately outside the base transport.
165
108
  _options: { __after__: 'test' },
166
109
 
167
110
  init(ctx: any, fopts: any): void {
@@ -1,14 +1,3 @@
1
- /* MIRROR of `spec/config-shape.json` - design §4.3, verbatim.
2
- *
3
- * The JSON under `spec/` is the artifact every port reads; this module
4
- * is the TypeScript port's shipped copy of it. It exists because
5
- * `package.json` ships `dist/src` only, so a runtime read of `spec/`
6
- * works in the repo and breaks in a published consumer - and
7
- * `validateConfig` runs at `open()`, not just under test.
8
- *
9
- * DO NOT EDIT THIS FILE. Edit `spec/config-shape.json`, then re-run
10
- * `make sync-shape`. `test/shape.test.ts` fails on drift.
11
- */
12
1
 
13
2
  export const CONFIG_SHAPE: any = {
14
3
  "station": [
package/src/descriptor.ts CHANGED
@@ -1,9 +1,5 @@
1
1
  import { Descriptor, DescriptorEntity, DescriptorPoint } from './types'
2
2
 
3
- // The ONLY way to build an env-var token in station, mirroring sdkgen's
4
- // packageMeta envToken exactly: 'gnarly-pets' -> 'GNARLY_PETS'. The
5
- // `secretname` corpus section pins the round-trip against sekreto's
6
- // envkey() and sdkgen's envName() - the one place three grammars meet.
7
3
  export function envtoken(name: any): string {
8
4
  return String(name || '')
9
5
  .toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '')
@@ -16,10 +12,6 @@ export function secretnameDefault(slug: string): string {
16
12
  return envtoken(slug).toLowerCase() + '.apikey'
17
13
  }
18
14
 
19
- // Best-effort slug from a camel name, for SDKs whose embedded config
20
- // predates main.slug (design §4 legacy sentinels). The hyphen caveat is
21
- // real: 'VoxgigSolardemo' -> 'voxgigsolardemo', NOT 'voxgig-solardemo' -
22
- // callers surface a warning event when this path is taken.
23
15
  function legacySlug(name: string): string {
24
16
  return String(name || '').toLowerCase()
25
17
  }
@@ -90,20 +82,6 @@ export function normalizeDescriptor(config: any, activeFeatures?: Record<string,
90
82
  entities[ename] = { fields, ops }
91
83
  }
92
84
 
93
- // §7.4: the features list gains `options` and `transport`, because
94
- // it was throwing away what the SDK already embeds -
95
- // `config.feature[name].options` is the feature's own declared key
96
- // set WITH TYPED DEFAULTS, which is the schema §8.5 validates
97
- // against, and `transport` is the role §8.4 orders by.
98
- //
99
- // Both are already inside the SDK; the descriptor stops discarding
100
- // them. ADDITIVE, so descriptor v1 consumers are unaffected.
101
- //
102
- // `transport` is carried rather than inferred: the obvious signal, an
103
- // empty `hook: {}`, is wrong for station, which both wraps AND
104
- // dispatches hooks. Absent until sdkgen emits it (§11 items 6-7), and
105
- // §8.4's role checks degrade to nothing until then rather than
106
- // guessing.
107
85
  const features: {
108
86
  name: string, active: boolean,
109
87
  options?: Record<string, any>, transport?: string,
@@ -134,19 +112,6 @@ export function normalizeDescriptor(config: any, activeFeatures?: Record<string,
134
112
  return { descriptor, warnings }
135
113
  }
136
114
 
137
- // Canonical serialization (design §4): UTF-8, object keys sorted
138
- // bytewise, no insignificant whitespace, minimal JSON escaping. The
139
- // proxy dedupes registrations by a hash of this, so every language must
140
- // produce identical bytes - the `canonical-serialize` corpus section
141
- // carries the adversarial cases.
142
- //
143
- // The bytewise sort compares UTF-8 BYTE SEQUENCES, not UTF-16 code
144
- // units, through TextEncoder rather than Buffer: TextEncoder is a
145
- // standard global in Node and every browser, so this module stays on
146
- // the browser entry's import graph (§2.2, §17 Phase 1) with no node:
147
- // dependency. Uint8Array elements are unsigned bytes, so plain numeric
148
- // comparison IS the bytewise order Buffer.compare gave - the corpus'
149
- // non-ASCII sort case pins the equivalence.
150
115
  const UTF8 = new TextEncoder()
151
116
 
152
117
  function bytecompare(a: string, b: string): number {
@@ -170,7 +135,6 @@ export function canonicalSerialize(value: any): string {
170
135
  }
171
136
  if ('object' === typeof value) {
172
137
  const keys = Object.keys(value).filter((k) => undefined !== value[k])
173
- // Bytewise sort: compare UTF-8 byte sequences, not UTF-16 code units.
174
138
  keys.sort(bytecompare)
175
139
  return '{' + keys.map((k) =>
176
140
  JSON.stringify(k) + ':' + canonicalSerialize(value[k])).join(',') + '}'
package/src/error.ts CHANGED
@@ -20,11 +20,6 @@ const CODES = [
20
20
  'station_open_conflict',
21
21
  'station_bound_twice',
22
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
23
  'station_config_invalid',
29
24
  'station_config_secret',
30
25
  'station_secret_collision',
package/src/factory.ts CHANGED
@@ -1,31 +1,3 @@
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
1
 
30
2
  import { StationError } from './error'
31
3
  import { normalizeDescriptor } from './descriptor'
@@ -45,13 +17,6 @@ export type FactoryEntry = {
45
17
 
46
18
  const TABLE = new Map<string, FactoryEntry>()
47
19
 
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
20
  export function provide(api: string, factory: Factory): FactoryEntry {
56
21
  const slug = String(api)
57
22
  const prior = TABLE.get(slug)
@@ -86,8 +51,6 @@ export function provided(): string[] {
86
51
  return Array.from(TABLE.keys()).sort()
87
52
  }
88
53
 
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
54
  export function resetFactories(): void {
92
55
  TABLE.clear()
93
56
  }
package/src/feature.ts CHANGED
@@ -1,10 +1,3 @@
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
1
 
9
2
  import { StationError } from './error'
10
3
 
@@ -12,31 +5,8 @@ import { StationError } from './error'
12
5
  // §8.3 — the merge
13
6
  // ---------------------------------------------------------------------
14
7
 
15
- /** Reserved on a feature entry: not options, and never passed through
16
- * to the SDK's own option map. */
17
8
  export const RESERVED_KEYS = ['active', 'order']
18
9
 
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
10
  export function mergefeatures(sources: any[]): { [name: string]: any } {
41
11
  const out: { [name: string]: any } = {}
42
12
  for (const src of sources) {
@@ -44,15 +14,12 @@ export function mergefeatures(sources: any[]): { [name: string]: any } {
44
14
  for (const name of Object.keys(src)) {
45
15
  const entry = src[name]
46
16
  if (!ismap(entry)) { out[name] = entry; continue }
47
- // Per option key, and NOT deeper.
48
17
  out[name] = { ...(ismap(out[name]) ? out[name] : {}), ...entry }
49
18
  }
50
19
  }
51
20
  return out
52
21
  }
53
22
 
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
23
  export function featuresources(
57
24
  base: any, overlay: any, api: string, ref: string
58
25
  ): any[] {
@@ -70,19 +37,10 @@ export function featuresources(
70
37
  // §8.4 — activation and order
71
38
  // ---------------------------------------------------------------------
72
39
 
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
40
  export const BAND_DEFAULT = 0
82
41
  export const BAND_STATION = 100
83
42
  export const BAND_TEST = 200
84
43
 
85
- /** Higher is further IN. */
86
44
  export function defaultband(name: string): number {
87
45
  if ('test' === name) { return BAND_TEST }
88
46
  if ('station' === name) { return BAND_STATION }
@@ -91,20 +49,6 @@ export function defaultband(name: string): number {
91
49
 
92
50
  export type Ordered = { name: string, band: number, entry: any }
93
51
 
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
52
  export function resolveorder(merged: { [name: string]: any }): Ordered[] {
109
53
  const names = Object.keys(merged).filter((n) => active(merged[n]))
110
54
  const pos = new Map<string, number>()
@@ -117,7 +61,6 @@ export function resolveorder(merged: { [name: string]: any }): Ordered[] {
117
61
  band.set(n, b)
118
62
  }
119
63
 
120
- // edges: from OUTER to INNER. `after: X` means "further in than X".
121
64
  const inner = new Map<string, Set<string>>()
122
65
  for (const n of names) { inner.set(n, new Set()) }
123
66
 
@@ -127,7 +70,6 @@ export function resolveorder(merged: { [name: string]: any }): Ordered[] {
127
70
  for (const n of names) {
128
71
  const o = ismap(merged[n]) ? merged[n].order : undefined
129
72
  if (!ismap(o)) { continue }
130
- // Vacuous when absent: an unknown name is not an error here.
131
73
  for (const other of listof(o.after)) {
132
74
  if (inner.has(other)) { inner.get(other)!.add(n) }
133
75
  }
@@ -173,23 +115,11 @@ export function resolveorder(merged: { [name: string]: any }): Ordered[] {
173
115
  return out
174
116
  }
175
117
 
176
- /** A feature named in the config is one you are ASKING for, so an entry
177
- * with no `active` is active. */
178
118
  function active(entry: any): boolean {
179
119
  if (!ismap(entry)) { return false !== entry }
180
120
  return false !== entry.active
181
121
  }
182
122
 
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
123
  export function checkpin(ordered: Ordered[]): void {
194
124
  const i = ordered.findIndex((o) => 'station' === o.name)
195
125
  if (-1 === i) { return }
@@ -217,23 +147,6 @@ export type FeatureFault = {
217
147
  message: string
218
148
  }
219
149
 
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
150
  export function checkfeatures(
238
151
  merged: { [name: string]: any }, descriptor: any
239
152
  ): FeatureFault[] {
package/src/index.ts CHANGED
@@ -1,18 +1,5 @@
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
1
 
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'
2
+ export { Station, instanceRef } from './Station'
16
3
  export { provide, factoryFor, provided, resetFactories } from './factory'
17
4
  export type { Factory, FactoryEntry } from './factory'
18
5
  export {
package/src/loader.ts CHANGED
@@ -1,43 +1,9 @@
1
- /* The loader (design §6.3), where the language allows it.
2
- *
3
- * In ts, js, py, rb, php, perl, lua, elixir and clojure a module can be
4
- * imported by name at runtime, so `api.<slug>.package` closes the loop:
5
- * station imports the package (which triggers self-registration, §6.2
6
- * path 1) and then looks up the factory.
7
- *
8
- * `sdk()` IS SYNCHRONOUS, AND IN TS/JS THAT BOUNDS WHAT THE LOADER CAN
9
- * IMPORT. A CommonJS package loads synchronously through `require`; an
10
- * ESM-only package only loads through `import()`, which is a promise.
11
- * Making `sdk()` async to cover that would put an `await` in front of
12
- * every call site in every language for a cost only two of them pay.
13
- * So the rule is explicit rather than discovered at runtime:
14
- *
15
- * - the synchronous loader handles CommonJS, which is every SDK sdkgen
16
- * generates (the ts target is `"type": "commonjs"`);
17
- * - ESM packages load through an explicit `await station.load()`
18
- * preload, after which `sdk()` is synchronous again for everything —
19
- * one `await` at startup rather than one per call site;
20
- * - a package only `import()` can load, reached through `sdk()`
21
- * without a preload, is `station_sdk_load` WITH THE REMEDY IN THE
22
- * MESSAGE. Never a silent half-loaded client.
23
- *
24
- * THIS IS A CODE-LOADING SURFACE DRIVEN BY A CONFIG FILE, so it has
25
- * rules, and they are enforced here rather than documented and hoped
26
- * for. See `checkPackage` and the `repoScoped` argument. */
27
1
 
28
2
  import { StationError } from './error'
29
3
  import { Factory, factoryFor, provide } from './factory'
30
4
 
31
- /** The fixed alias every generated package exports.
32
- *
33
- * `export` defaults to this rather than to a derived class name because
34
- * it is the same identifier in every generated package, where
35
- * `camelify(slug) + 'SDK'` is a rule that has to be recomputed and can
36
- * be wrong. The derived name is the SECOND attempt and an explicit
37
- * `export` the third. */
38
5
  export const DEFAULT_EXPORT = 'SDK'
39
6
 
40
- /** `stripe-eu` -> `StripeEu`, for the second-attempt export name. */
41
7
  export function camelify(slug: string): string {
42
8
  return String(slug).split(/[^A-Za-z0-9]+/).filter((s) => '' !== s)
43
9
  .map((s) => s[0].toUpperCase() + s.slice(1))
@@ -51,13 +17,6 @@ export function camelify(slug: string): string {
51
17
  * name. */
52
18
  export function checkPackage(api: string, pkg: string): string {
53
19
  const p = String(pkg)
54
- // A TRAVERSAL SEGMENT IS NOT A LEADING MARKER, and checking only the
55
- // first character missed it: `pkg/../../escape` starts with neither
56
- // `.` nor `/`, so it passed — and node resolves it through
57
- // `node_modules/pkg/../../escape`, importing application-local code
58
- // from outside the named dependency. The whole point of this function
59
- // is that a configured package stays inside the dependency graph a
60
- // reviewer can see.
61
20
  const seg = p.split('/').some((x) => '.' === x || '..' === x)
62
21
  const bad =
63
22
  '' === p ||
@@ -75,10 +34,6 @@ export function checkPackage(api: string, pkg: string): string {
75
34
  return p
76
35
  }
77
36
 
78
- /** Build a `{construct, config}` pair from a module that self-registered
79
- * nothing — the retrofit path for a package whose SDK predates the
80
- * station feature. It is not descriptor-blind: a generated main module
81
- * exports its constructor AND the `config` singleton beside it. */
82
37
  export function factoryFromModule(
83
38
  api: string, mod: any, exportName?: string
84
39
  ): Factory {
@@ -124,8 +79,6 @@ export function loadSync(api: string, pkg: string, exportName?: string): boolean
124
79
  mod = require(pkg)
125
80
  }
126
81
  catch (e: any) {
127
- // ERR_REQUIRE_ESM is the case the preload exists for, and the
128
- // message has to name the remedy rather than the symptom.
129
82
  const code = String(e?.code || '')
130
83
  if ('ERR_REQUIRE_ESM' === code) {
131
84
  throw new StationError('station_sdk_load',
@@ -138,30 +91,12 @@ export function loadSync(api: string, pkg: string, exportName?: string): boolean
138
91
  String(e?.message || e))
139
92
  }
140
93
 
141
- // Path 1: the module self-registered while being imported.
142
94
  if (null != factoryFor(api)) { return true }
143
95
 
144
96
  provide(api, factoryFromModule(api, mod, exportName))
145
97
  return true
146
98
  }
147
99
 
148
- /** The ESM preload behind `await station.load()`. Imports the package
149
- * and fills the SAME table the synchronous path fills. */
150
- /** A dynamic `import()` that SURVIVES THE COMMONJS EMIT.
151
- *
152
- * This package compiles with `module: "commonjs"`, and TypeScript
153
- * rewrites a literal `import(pkg)` into a promise around `require(pkg)`
154
- * — so `await station.load()` threw `ERR_REQUIRE_ESM` for exactly the
155
- * ESM-only packages it exists to preload, leaving no working path to
156
- * those SDKs at all. Verified against the emitted `dist/src/loader.js`,
157
- * which read `mod = require(pkg)`.
158
- *
159
- * `new Function` is the seam the compiler does not look inside. It is
160
- * deliberately the only one in this file, and it takes no caller input:
161
- * `pkg` has already been through `checkPackage`, and the body is a
162
- * constant. Changing the package to `module: "node16"` would fix it
163
- * more cleanly and is a whole-package decision rather than a defect
164
- * fix. */
165
100
  export const nativeImport: (p: string) => Promise<any> =
166
101
  new Function('p', 'return import(p)') as any
167
102
 
@@ -181,7 +116,6 @@ export async function loadAsync(
181
116
 
182
117
  if (null != factoryFor(api)) { return true }
183
118
 
184
- // An ESM namespace puts a CommonJS module's exports under `default`.
185
119
  const flat = null != mod?.default && null == mod?.config ? mod.default : mod
186
120
  provide(api, factoryFromModule(api, flat, exportName))
187
121
  return true