@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.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/src/Station.d.ts +238 -0
- package/dist/src/Station.js +1213 -0
- package/dist/src/Station.js.map +1 -0
- package/dist/src/adapter.d.ts +9 -0
- package/dist/src/adapter.js +171 -0
- package/dist/src/adapter.js.map +1 -0
- package/dist/src/config-shape.d.ts +1 -0
- package/dist/src/config-shape.js +329 -0
- package/dist/src/config-shape.js.map +1 -0
- package/dist/src/descriptor.d.ts +8 -0
- package/dist/src/descriptor.js +170 -0
- package/dist/src/descriptor.js.map +1 -0
- package/dist/src/error.d.ts +8 -0
- package/dist/src/error.js +60 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/events.d.ts +15 -0
- package/dist/src/events.js +46 -0
- package/dist/src/events.js.map +1 -0
- package/dist/src/factory.d.ts +24 -0
- package/dist/src/factory.js +79 -0
- package/dist/src/factory.js.map +1 -0
- package/dist/src/feature.d.ts +106 -0
- package/dist/src/feature.js +324 -0
- package/dist/src/feature.js.map +1 -0
- package/dist/src/index.browser.d.ts +14 -0
- package/dist/src/index.browser.js +64 -0
- package/dist/src/index.browser.js.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +56 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/loader.d.ts +45 -0
- package/dist/src/loader.js +181 -0
- package/dist/src/loader.js.map +1 -0
- package/dist/src/profile.d.ts +6 -0
- package/dist/src/profile.js +77 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/profilecore.d.ts +10 -0
- package/dist/src/profilecore.js +180 -0
- package/dist/src/profilecore.js.map +1 -0
- package/dist/src/secrets.d.ts +12 -0
- package/dist/src/secrets.js +93 -0
- package/dist/src/secrets.js.map +1 -0
- package/dist/src/shape.d.ts +42 -0
- package/dist/src/shape.js +444 -0
- package/dist/src/shape.js.map +1 -0
- package/dist/src/types.d.ts +138 -0
- package/dist/src/types.js +5 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +60 -0
- package/src/Station.ts +1310 -0
- package/src/adapter.ts +183 -0
- package/src/config-shape.ts +326 -0
- package/src/descriptor.ts +179 -0
- package/src/error.ts +64 -0
- package/src/events.ts +45 -0
- package/src/factory.ts +93 -0
- package/src/feature.ts +321 -0
- package/src/index.browser.ts +56 -0
- package/src/index.ts +50 -0
- package/src/loader.ts +188 -0
- package/src/profile.ts +67 -0
- package/src/profilecore.ts +201 -0
- package/src/secrets.ts +96 -0
- package/src/shape.ts +435 -0
- package/src/types.ts +185 -0
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { StationError } from './error'
|
|
2
|
+
import { Station } from './Station'
|
|
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
|
+
|
|
15
|
+
let corrSeq = 0
|
|
16
|
+
|
|
17
|
+
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
|
+
slug: string
|
|
22
|
+
PrePoint(ctx: any): void
|
|
23
|
+
PreDone(ctx: any): void
|
|
24
|
+
PreUnexpected(ctx: any): void
|
|
25
|
+
}
|
|
26
|
+
|
|
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
|
+
export function featureBinding(ctx: any, fopts: any): FeatureBinding | null {
|
|
33
|
+
const station: Station | null =
|
|
34
|
+
(fopts?.station instanceof Station ? fopts.station : null) ?? Station.current()
|
|
35
|
+
if (null == station) { return null }
|
|
36
|
+
|
|
37
|
+
const client = ctx.client
|
|
38
|
+
|
|
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
|
+
if (null != station._boundEntry(client)) { return null }
|
|
43
|
+
const utility = ctx.utility
|
|
44
|
+
const options = ctx.options
|
|
45
|
+
const calleropts = fopts?.calleropts
|
|
46
|
+
|
|
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
|
+
const names = client._features.map((f: any) => f.name)
|
|
52
|
+
const self = names.indexOf('station')
|
|
53
|
+
const testAt = names.indexOf('test')
|
|
54
|
+
const expected = -1 === testAt ? 0 : testAt + 1
|
|
55
|
+
if (self !== expected) {
|
|
56
|
+
throw new StationError('station_wrap_order',
|
|
57
|
+
'station must init immediately after the base transport; ' +
|
|
58
|
+
'feature order is [' + names.join(', ') + ']')
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
const reg = station._register(client, ctx.config, options, calleropts, fopts)
|
|
66
|
+
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
|
+
const name = binding.plugin
|
|
71
|
+
|
|
72
|
+
// Base URL precedence (design §3.5): caller opts (7) beat the
|
|
73
|
+
// profile (4), which beats the SDK's config default (1) already in
|
|
74
|
+
// options.base. Applied only on the connect/adopt path, where the
|
|
75
|
+
// caller opts are knowable; station.options() applies the profile
|
|
76
|
+
// base at options-build time instead.
|
|
77
|
+
if (null != calleropts && null == calleropts.base && null != profilePlugin?.base) {
|
|
78
|
+
options.base = profilePlugin.base
|
|
79
|
+
}
|
|
80
|
+
|
|
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
|
+
const pallow = profilePlugin?.policy?.allow
|
|
94
|
+
if (null != pallow && 'object' === typeof pallow) {
|
|
95
|
+
const allow = {
|
|
96
|
+
...(null != options.allow && 'object' === typeof options.allow
|
|
97
|
+
? options.allow : {}),
|
|
98
|
+
}
|
|
99
|
+
if (Array.isArray(pallow.op)) { allow.op = pallow.op.join(',') }
|
|
100
|
+
if (Array.isArray(pallow.method)) { allow.method = pallow.method.join(',') }
|
|
101
|
+
options.allow = allow
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ('none' !== binding.rung) {
|
|
105
|
+
const placeholder = binding.placeholder!
|
|
106
|
+
|
|
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
|
+
const resident = options.apikey
|
|
112
|
+
if ('string' === typeof resident && '' !== resident && placeholder !== resident) {
|
|
113
|
+
station._hoist(name, resident)
|
|
114
|
+
}
|
|
115
|
+
options.apikey = placeholder
|
|
116
|
+
}
|
|
117
|
+
|
|
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
|
+
const inner = utility.fetcher
|
|
122
|
+
if (true === (inner as any).__station__) {
|
|
123
|
+
throw new StationError('station_bound_twice',
|
|
124
|
+
'plugin "' + name + '" already carries a station wrap')
|
|
125
|
+
}
|
|
126
|
+
const wrapped = async (fctx: any, fullurl: string, fetchdef: any) => {
|
|
127
|
+
return station._transport(name, inner, fctx, fullurl, fetchdef)
|
|
128
|
+
}
|
|
129
|
+
;(wrapped as any).__station__ = true
|
|
130
|
+
utility.fetcher = wrapped
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
slug: name,
|
|
134
|
+
|
|
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
|
+
PrePoint(opctx: any): void {
|
|
138
|
+
opctx.station$ = { corr: 'c' + (++corrSeq), start: Date.now() }
|
|
139
|
+
},
|
|
140
|
+
PreDone(opctx: any): void {
|
|
141
|
+
station._opEvent(name, opctx, resultOutcome(opctx))
|
|
142
|
+
},
|
|
143
|
+
PreUnexpected(opctx: any): void {
|
|
144
|
+
station._opEvent(name, opctx, 'unexpected')
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The carried adapter: the retrofit path for SDKs generated without
|
|
150
|
+
// the station feature (design §3.1 adopt). A duck-typed Feature whose
|
|
151
|
+
// init/hooks delegate to featureBinding - it exists so connect/adopt
|
|
152
|
+
// work on any regenerated SDK, and it must stay behaviorally identical
|
|
153
|
+
// to the generated feature template in sdkgen-station.
|
|
154
|
+
export function adapterFeature(station: Station, calleropts: any): any {
|
|
155
|
+
const feature: any = {
|
|
156
|
+
name: 'station',
|
|
157
|
+
version: '0.0.1',
|
|
158
|
+
active: true,
|
|
159
|
+
_binding: null as FeatureBinding | null,
|
|
160
|
+
|
|
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
|
+
_options: { __after__: 'test' },
|
|
166
|
+
|
|
167
|
+
init(ctx: any, fopts: any): void {
|
|
168
|
+
feature._binding = featureBinding(ctx, { ...fopts, station, calleropts })
|
|
169
|
+
},
|
|
170
|
+
PrePoint(ctx: any): void { feature._binding?.PrePoint(ctx) },
|
|
171
|
+
PreDone(ctx: any): void { feature._binding?.PreDone(ctx) },
|
|
172
|
+
PreUnexpected(ctx: any): void { feature._binding?.PreUnexpected(ctx) },
|
|
173
|
+
}
|
|
174
|
+
return feature
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resultOutcome(ctx: any): string {
|
|
178
|
+
const result = ctx.result
|
|
179
|
+
if (null == result) { return 'unknown' }
|
|
180
|
+
if (null != result.err) { return 'err' }
|
|
181
|
+
if (false === result.ok) { return 'err' }
|
|
182
|
+
return 'ok'
|
|
183
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
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
|
+
|
|
13
|
+
export const CONFIG_SHAPE: any = {
|
|
14
|
+
"station": [
|
|
15
|
+
"`$EXACT`",
|
|
16
|
+
1
|
|
17
|
+
],
|
|
18
|
+
"profiles": {
|
|
19
|
+
"`$CHILD`": {
|
|
20
|
+
"secrets": {
|
|
21
|
+
"providers": "`$LIST`"
|
|
22
|
+
},
|
|
23
|
+
"api": {
|
|
24
|
+
"`$CHILD`": {
|
|
25
|
+
"active": "`$BOOLEAN`",
|
|
26
|
+
"package": [
|
|
27
|
+
"`$ONE`",
|
|
28
|
+
"`$NIL`",
|
|
29
|
+
"`$STRING`"
|
|
30
|
+
],
|
|
31
|
+
"export": [
|
|
32
|
+
"`$ONE`",
|
|
33
|
+
"`$NIL`",
|
|
34
|
+
"`$STRING`"
|
|
35
|
+
],
|
|
36
|
+
"base": [
|
|
37
|
+
"`$ONE`",
|
|
38
|
+
"`$NIL`",
|
|
39
|
+
"`$STRING`"
|
|
40
|
+
],
|
|
41
|
+
"secret": [
|
|
42
|
+
"`$ONE`",
|
|
43
|
+
"`$NIL`",
|
|
44
|
+
"`$STRING`"
|
|
45
|
+
],
|
|
46
|
+
"resolve": [
|
|
47
|
+
"`$ONE`",
|
|
48
|
+
"`$NIL`",
|
|
49
|
+
[
|
|
50
|
+
"`$EXACT`",
|
|
51
|
+
"library"
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"`$EXACT`",
|
|
55
|
+
"proxy"
|
|
56
|
+
]
|
|
57
|
+
],
|
|
58
|
+
"capture": [
|
|
59
|
+
"`$ONE`",
|
|
60
|
+
"`$NIL`",
|
|
61
|
+
[
|
|
62
|
+
"`$EXACT`",
|
|
63
|
+
"meta"
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"`$EXACT`",
|
|
67
|
+
"headers"
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"`$EXACT`",
|
|
71
|
+
"full"
|
|
72
|
+
]
|
|
73
|
+
],
|
|
74
|
+
"policy": [
|
|
75
|
+
"`$ONE`",
|
|
76
|
+
"`$NIL`",
|
|
77
|
+
{
|
|
78
|
+
"allow": [
|
|
79
|
+
"`$ONE`",
|
|
80
|
+
"`$NIL`",
|
|
81
|
+
{
|
|
82
|
+
"method": [
|
|
83
|
+
"`$CHILD`",
|
|
84
|
+
"`$STRING`"
|
|
85
|
+
],
|
|
86
|
+
"op": [
|
|
87
|
+
"`$CHILD`",
|
|
88
|
+
"`$STRING`"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
"budget": [
|
|
93
|
+
"`$ONE`",
|
|
94
|
+
"`$NIL`",
|
|
95
|
+
{
|
|
96
|
+
"concurrency": [
|
|
97
|
+
"`$ONE`",
|
|
98
|
+
"`$NIL`",
|
|
99
|
+
"`$INTEGER`"
|
|
100
|
+
],
|
|
101
|
+
"rps": [
|
|
102
|
+
"`$ONE`",
|
|
103
|
+
"`$NIL`",
|
|
104
|
+
"`$NUMBER`"
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"hosts": [
|
|
109
|
+
"`$CHILD`",
|
|
110
|
+
"`$STRING`"
|
|
111
|
+
],
|
|
112
|
+
"mode": [
|
|
113
|
+
"`$ONE`",
|
|
114
|
+
"`$NIL`",
|
|
115
|
+
[
|
|
116
|
+
"`$EXACT`",
|
|
117
|
+
"live"
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"`$EXACT`",
|
|
121
|
+
"record"
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
"`$EXACT`",
|
|
125
|
+
"replay"
|
|
126
|
+
],
|
|
127
|
+
[
|
|
128
|
+
"`$EXACT`",
|
|
129
|
+
"mock"
|
|
130
|
+
],
|
|
131
|
+
[
|
|
132
|
+
"`$EXACT`",
|
|
133
|
+
"block"
|
|
134
|
+
]
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"agent": [
|
|
139
|
+
"`$ONE`",
|
|
140
|
+
"`$NIL`",
|
|
141
|
+
{
|
|
142
|
+
"write": "`$BOOLEAN`"
|
|
143
|
+
}
|
|
144
|
+
],
|
|
145
|
+
"options": [
|
|
146
|
+
"`$ONE`",
|
|
147
|
+
"`$NIL`",
|
|
148
|
+
"`$MAP`"
|
|
149
|
+
],
|
|
150
|
+
"feature": {
|
|
151
|
+
"`$CHILD`": {
|
|
152
|
+
"active": "`$BOOLEAN`",
|
|
153
|
+
"`$OPEN`": true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"sdk": {
|
|
159
|
+
"`$CHILD`": {
|
|
160
|
+
"active": "`$BOOLEAN`",
|
|
161
|
+
"package": [
|
|
162
|
+
"`$ONE`",
|
|
163
|
+
"`$NIL`",
|
|
164
|
+
"`$STRING`"
|
|
165
|
+
],
|
|
166
|
+
"export": [
|
|
167
|
+
"`$ONE`",
|
|
168
|
+
"`$NIL`",
|
|
169
|
+
"`$STRING`"
|
|
170
|
+
],
|
|
171
|
+
"base": [
|
|
172
|
+
"`$ONE`",
|
|
173
|
+
"`$NIL`",
|
|
174
|
+
"`$STRING`"
|
|
175
|
+
],
|
|
176
|
+
"secret": [
|
|
177
|
+
"`$ONE`",
|
|
178
|
+
"`$NIL`",
|
|
179
|
+
"`$STRING`"
|
|
180
|
+
],
|
|
181
|
+
"resolve": [
|
|
182
|
+
"`$ONE`",
|
|
183
|
+
"`$NIL`",
|
|
184
|
+
[
|
|
185
|
+
"`$EXACT`",
|
|
186
|
+
"library"
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
"`$EXACT`",
|
|
190
|
+
"proxy"
|
|
191
|
+
]
|
|
192
|
+
],
|
|
193
|
+
"capture": [
|
|
194
|
+
"`$ONE`",
|
|
195
|
+
"`$NIL`",
|
|
196
|
+
[
|
|
197
|
+
"`$EXACT`",
|
|
198
|
+
"meta"
|
|
199
|
+
],
|
|
200
|
+
[
|
|
201
|
+
"`$EXACT`",
|
|
202
|
+
"headers"
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
"`$EXACT`",
|
|
206
|
+
"full"
|
|
207
|
+
]
|
|
208
|
+
],
|
|
209
|
+
"policy": [
|
|
210
|
+
"`$ONE`",
|
|
211
|
+
"`$NIL`",
|
|
212
|
+
{
|
|
213
|
+
"allow": [
|
|
214
|
+
"`$ONE`",
|
|
215
|
+
"`$NIL`",
|
|
216
|
+
{
|
|
217
|
+
"method": [
|
|
218
|
+
"`$CHILD`",
|
|
219
|
+
"`$STRING`"
|
|
220
|
+
],
|
|
221
|
+
"op": [
|
|
222
|
+
"`$CHILD`",
|
|
223
|
+
"`$STRING`"
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
"budget": [
|
|
228
|
+
"`$ONE`",
|
|
229
|
+
"`$NIL`",
|
|
230
|
+
{
|
|
231
|
+
"concurrency": [
|
|
232
|
+
"`$ONE`",
|
|
233
|
+
"`$NIL`",
|
|
234
|
+
"`$INTEGER`"
|
|
235
|
+
],
|
|
236
|
+
"rps": [
|
|
237
|
+
"`$ONE`",
|
|
238
|
+
"`$NIL`",
|
|
239
|
+
"`$NUMBER`"
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
"hosts": [
|
|
244
|
+
"`$CHILD`",
|
|
245
|
+
"`$STRING`"
|
|
246
|
+
],
|
|
247
|
+
"mode": [
|
|
248
|
+
"`$ONE`",
|
|
249
|
+
"`$NIL`",
|
|
250
|
+
[
|
|
251
|
+
"`$EXACT`",
|
|
252
|
+
"live"
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
"`$EXACT`",
|
|
256
|
+
"record"
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
"`$EXACT`",
|
|
260
|
+
"replay"
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
"`$EXACT`",
|
|
264
|
+
"mock"
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
"`$EXACT`",
|
|
268
|
+
"block"
|
|
269
|
+
]
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
"agent": [
|
|
274
|
+
"`$ONE`",
|
|
275
|
+
"`$NIL`",
|
|
276
|
+
{
|
|
277
|
+
"write": "`$BOOLEAN`"
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
"options": [
|
|
281
|
+
"`$ONE`",
|
|
282
|
+
"`$NIL`",
|
|
283
|
+
"`$MAP`"
|
|
284
|
+
],
|
|
285
|
+
"feature": {
|
|
286
|
+
"`$CHILD`": {
|
|
287
|
+
"active": "`$BOOLEAN`",
|
|
288
|
+
"`$OPEN`": true
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
"feature": {
|
|
294
|
+
"`$CHILD`": {
|
|
295
|
+
"active": "`$BOOLEAN`",
|
|
296
|
+
"order": {
|
|
297
|
+
"before": [
|
|
298
|
+
"`$ONE`",
|
|
299
|
+
"`$NIL`",
|
|
300
|
+
"`$STRING`",
|
|
301
|
+
[
|
|
302
|
+
"`$CHILD`",
|
|
303
|
+
"`$STRING`"
|
|
304
|
+
]
|
|
305
|
+
],
|
|
306
|
+
"after": [
|
|
307
|
+
"`$ONE`",
|
|
308
|
+
"`$NIL`",
|
|
309
|
+
"`$STRING`",
|
|
310
|
+
[
|
|
311
|
+
"`$CHILD`",
|
|
312
|
+
"`$STRING`"
|
|
313
|
+
]
|
|
314
|
+
],
|
|
315
|
+
"band": [
|
|
316
|
+
"`$ONE`",
|
|
317
|
+
"`$NIL`",
|
|
318
|
+
"`$INTEGER`"
|
|
319
|
+
]
|
|
320
|
+
},
|
|
321
|
+
"`$OPEN`": true
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Descriptor, DescriptorEntity, DescriptorPoint } from './types'
|
|
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
|
+
export function envtoken(name: any): string {
|
|
8
|
+
return String(name || '')
|
|
9
|
+
.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The default sekreto name for a plugin (design §5.1): envtoken(slug)
|
|
13
|
+
// lowercased, plus '.apikey'. sekreto's envkey() then yields exactly the
|
|
14
|
+
// env var the SDK's README documents: gnarly_pets.apikey -> GNARLY_PETS_APIKEY.
|
|
15
|
+
export function secretnameDefault(slug: string): string {
|
|
16
|
+
return envtoken(slug).toLowerCase() + '.apikey'
|
|
17
|
+
}
|
|
18
|
+
|
|
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
|
+
function legacySlug(name: string): string {
|
|
24
|
+
return String(name || '').toLowerCase()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Normalize a generated SDK's embedded config into descriptor v1
|
|
28
|
+
// (design §4). The config is the one every SDK carries (Config.main /
|
|
29
|
+
// .feature / .options / .entity); the descriptor is a VIEW over it.
|
|
30
|
+
// Returns the descriptor plus any legacy warnings.
|
|
31
|
+
export function normalizeDescriptor(config: any, activeFeatures?: Record<string, any>):
|
|
32
|
+
{ descriptor: Descriptor, warnings: string[] } {
|
|
33
|
+
|
|
34
|
+
const warnings: string[] = []
|
|
35
|
+
const main = config?.main || {}
|
|
36
|
+
const options = config?.options || {}
|
|
37
|
+
|
|
38
|
+
const name = String(main.name || '')
|
|
39
|
+
let slug: string = main.slug
|
|
40
|
+
if (null == slug || '' === slug) {
|
|
41
|
+
slug = legacySlug(name)
|
|
42
|
+
warnings.push('descriptor: legacy config has no main.slug; derived "' +
|
|
43
|
+
slug + '" from the camel name - hyphens in the original name are lost')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const version = null != main.version ? String(main.version) : '0.0.0'
|
|
47
|
+
const target = null != main.target ? String(main.target) : 'unknown'
|
|
48
|
+
|
|
49
|
+
const server: { name: string, value: string }[] = []
|
|
50
|
+
const svr = options.server || {}
|
|
51
|
+
for (const k of Object.keys(svr).sort()) {
|
|
52
|
+
server.push({ name: k, value: String(svr[k]) })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const authActive = null != options.auth
|
|
56
|
+
const auth = {
|
|
57
|
+
active: authActive,
|
|
58
|
+
prefix: authActive ? String(options.auth.prefix || '') : '',
|
|
59
|
+
secretname: secretnameDefault(slug),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const entities: Record<string, DescriptorEntity> = {}
|
|
63
|
+
const entdefs = config?.entity || {}
|
|
64
|
+
for (const ename of Object.keys(entdefs).sort()) {
|
|
65
|
+
const e = entdefs[ename] || {}
|
|
66
|
+
const fields: Record<string, { kind: string }> = {}
|
|
67
|
+
for (const f of e.fields || []) {
|
|
68
|
+
if (null != f && null != f.name) {
|
|
69
|
+
fields[f.name] = { kind: String(f.kind || f.type || '') }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const ops: Record<string, { points: DescriptorPoint[] }> = {}
|
|
73
|
+
const opdefs = e.op || {}
|
|
74
|
+
for (const opname of Object.keys(opdefs).sort()) {
|
|
75
|
+
const op = opdefs[opname] || {}
|
|
76
|
+
const points: DescriptorPoint[] = []
|
|
77
|
+
for (const p of op.points || []) {
|
|
78
|
+
if (null == p) { continue }
|
|
79
|
+
const point: DescriptorPoint = {
|
|
80
|
+
method: String(p.method || ''),
|
|
81
|
+
path: String(p.orig || p.path || ''),
|
|
82
|
+
params: (p.parts || []).filter((s: any) => 'string' === typeof s &&
|
|
83
|
+
s.startsWith(':')).map((s: string) => s.slice(1)),
|
|
84
|
+
}
|
|
85
|
+
if (null != p.select) { point.select = p.select }
|
|
86
|
+
points.push(point)
|
|
87
|
+
}
|
|
88
|
+
ops[opname] = { points }
|
|
89
|
+
}
|
|
90
|
+
entities[ename] = { fields, ops }
|
|
91
|
+
}
|
|
92
|
+
|
|
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
|
+
const features: {
|
|
108
|
+
name: string, active: boolean,
|
|
109
|
+
options?: Record<string, any>, transport?: string,
|
|
110
|
+
}[] = []
|
|
111
|
+
const fdefs = config?.feature || {}
|
|
112
|
+
const factive = activeFeatures || {}
|
|
113
|
+
for (const fname of Object.keys(fdefs).sort()) {
|
|
114
|
+
const fdef = fdefs[fname] || {}
|
|
115
|
+
const row: any = { name: fname, active: true === factive[fname]?.active }
|
|
116
|
+
if (null != fdef.options && 'object' === typeof fdef.options) {
|
|
117
|
+
row.options = fdef.options
|
|
118
|
+
}
|
|
119
|
+
if (null != fdef.transport && '' !== fdef.transport) {
|
|
120
|
+
row.transport = String(fdef.transport)
|
|
121
|
+
}
|
|
122
|
+
features.push(row)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const descriptor: Descriptor = {
|
|
126
|
+
station: 1,
|
|
127
|
+
name, slug,
|
|
128
|
+
envtoken: envtoken(slug),
|
|
129
|
+
version, target,
|
|
130
|
+
base: String(options.base || ''),
|
|
131
|
+
server, auth, entities, features,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { descriptor, warnings }
|
|
135
|
+
}
|
|
136
|
+
|
|
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
|
+
const UTF8 = new TextEncoder()
|
|
151
|
+
|
|
152
|
+
function bytecompare(a: string, b: string): number {
|
|
153
|
+
const ab = UTF8.encode(a), bb = UTF8.encode(b)
|
|
154
|
+
const n = ab.length < bb.length ? ab.length : bb.length
|
|
155
|
+
for (let i = 0; i < n; i++) {
|
|
156
|
+
if (ab[i] !== bb[i]) { return ab[i] - bb[i] }
|
|
157
|
+
}
|
|
158
|
+
return ab.length - bb.length
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function canonicalSerialize(value: any): string {
|
|
162
|
+
if (null === value || 'boolean' === typeof value || 'number' === typeof value) {
|
|
163
|
+
return JSON.stringify(value)
|
|
164
|
+
}
|
|
165
|
+
if ('string' === typeof value) {
|
|
166
|
+
return JSON.stringify(value)
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
return '[' + value.map(canonicalSerialize).join(',') + ']'
|
|
170
|
+
}
|
|
171
|
+
if ('object' === typeof value) {
|
|
172
|
+
const keys = Object.keys(value).filter((k) => undefined !== value[k])
|
|
173
|
+
// Bytewise sort: compare UTF-8 byte sequences, not UTF-16 code units.
|
|
174
|
+
keys.sort(bytecompare)
|
|
175
|
+
return '{' + keys.map((k) =>
|
|
176
|
+
JSON.stringify(k) + ':' + canonicalSerialize(value[k])).join(',') + '}'
|
|
177
|
+
}
|
|
178
|
+
return 'null'
|
|
179
|
+
}
|