@voxgig/sdkgen 4.17.5 → 4.18.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 (61) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideContent.js +30 -0
  3. package/dist/cmp/AgentGuideContent.js.map +1 -1
  4. package/dist/cmp/FeatureDocs.d.ts +6 -1
  5. package/dist/cmp/FeatureDocs.js +27 -0
  6. package/dist/cmp/FeatureDocs.js.map +1 -1
  7. package/dist/cmp/ReadmeRefFeatures.js +23 -13
  8. package/dist/cmp/ReadmeRefFeatures.js.map +1 -1
  9. package/dist/helpers/applicability.js +7 -0
  10. package/dist/helpers/applicability.js.map +1 -1
  11. package/dist/helpers/canonSpec.d.ts +12 -0
  12. package/dist/helpers/canonSpec.js +259 -0
  13. package/dist/helpers/canonSpec.js.map +1 -0
  14. package/dist/helpers/optspec.d.ts +4 -0
  15. package/dist/helpers/optspec.js +152 -0
  16. package/dist/helpers/optspec.js.map +1 -0
  17. package/dist/sdkgen.d.ts +3 -1
  18. package/dist/sdkgen.js +12 -3
  19. package/dist/sdkgen.js.map +1 -1
  20. package/dist/tsconfig.tsbuildinfo +1 -1
  21. package/model/sdkgen.aon +125 -0
  22. package/package.json +1 -1
  23. package/project/.sdk/model/feature/audit.aon +15 -0
  24. package/project/.sdk/model/feature/cache.aon +13 -0
  25. package/project/.sdk/model/feature/clienttrack.aon +21 -0
  26. package/project/.sdk/model/feature/cost.aon +15 -0
  27. package/project/.sdk/model/feature/debug.aon +15 -0
  28. package/project/.sdk/model/feature/feature-index.aon +1 -0
  29. package/project/.sdk/model/feature/idempotency.aon +13 -0
  30. package/project/.sdk/model/feature/log.aon +13 -0
  31. package/project/.sdk/model/feature/metrics.aon +13 -0
  32. package/project/.sdk/model/feature/netsim.aon +14 -0
  33. package/project/.sdk/model/feature/paging.aon +15 -0
  34. package/project/.sdk/model/feature/proxy.aon +13 -0
  35. package/project/.sdk/model/feature/ratelimit.aon +15 -0
  36. package/project/.sdk/model/feature/retry.aon +15 -0
  37. package/project/.sdk/model/feature/streaming.aon +15 -0
  38. package/project/.sdk/model/feature/telemetry.aon +19 -0
  39. package/project/.sdk/model/feature/test.aon +13 -0
  40. package/project/.sdk/model/feature/timeout.aon +15 -0
  41. package/project/.sdk/model/feature/validate.aon +73 -0
  42. package/project/.sdk/model/target/js.aon +1 -1
  43. package/project/.sdk/model/target/ts.aon +1 -1
  44. package/project/.sdk/src/cmp/js/Main_js.ts +2 -0
  45. package/project/.sdk/src/cmp/js/Schema_js.ts +66 -0
  46. package/project/.sdk/src/cmp/scala/Main_scala.ts +30 -0
  47. package/project/.sdk/src/cmp/ts/Main_ts.ts +2 -0
  48. package/project/.sdk/src/cmp/ts/Schema_ts.ts +66 -0
  49. package/project/.sdk/tm/js/src/feature/validate/ValidateFeature.js +296 -0
  50. package/project/.sdk/tm/js/src/utility/MakeOptionsUtility.js +26 -61
  51. package/project/.sdk/tm/ts/src/feature/validate/ValidateFeature.ts +298 -0
  52. package/project/.sdk/tm/ts/src/utility/MakeOptionsUtility.ts +24 -61
  53. package/project/.sdk/tm/ts/test/feature/secrets/Secrets.test.ts +37 -8
  54. package/project/sdkgen-package.json +3 -2
  55. package/src/cmp/AgentGuideContent.ts +30 -0
  56. package/src/cmp/FeatureDocs.ts +39 -0
  57. package/src/cmp/ReadmeRefFeatures.ts +24 -13
  58. package/src/helpers/applicability.ts +8 -0
  59. package/src/helpers/canonSpec.ts +301 -0
  60. package/src/helpers/optspec.ts +180 -0
  61. package/src/sdkgen.ts +9 -0
@@ -0,0 +1,66 @@
1
+ import {
2
+ Content,
3
+ File,
4
+ cmp,
5
+ entitySpecMap,
6
+ optionSpec,
7
+ } from '@voxgig/sdkgen'
8
+
9
+
10
+ import {
11
+ Model,
12
+ } from '@voxgig/apidef'
13
+
14
+
15
+ // THE GENERATED SCHEMA MODULE: the model's schemas, as data the SDK can run.
16
+ //
17
+ // Two exports, both struct.validate specs:
18
+ //
19
+ // OPTSPEC what makeOptions validates the caller's options against —
20
+ // the standard options from `main.kit.optspec` plus one entry
21
+ // per feature this target carries, built by helpers/optspec.
22
+ //
23
+ // ENTITYSPEC per entity, `{ data, op: { <opname> } }` — the record shape
24
+ // and each operation's request shape, mapped from the field
25
+ // type sentinels the model already carries (helpers/canonSpec).
26
+ // Emitted as an EMPTY MAP unless the `validate` feature is
27
+ // active, because nothing else reads it and every byte here is
28
+ // a byte in the consumer's package.
29
+ //
30
+ // A JSON OBJECT LITERAL, not a parsed string: JSON is a subset of the
31
+ // language's own literal syntax, so the values land as data with no parse
32
+ // step, and the backticks the sentinels carry (`\`$STRING\``) survive because
33
+ // JSON.stringify quotes and escapes them. The config emitter has to choose
34
+ // between a literal and a parsed blob because the config can be megabytes;
35
+ // the spec is bounded by the model's field count, so it does not.
36
+ const Schema = cmp(async function Schema(props: any) {
37
+ const ctx$ = props.ctx$
38
+ const target = props.target
39
+
40
+ const model: Model = ctx$.model
41
+
42
+ const optspec = optionSpec(model, target.name)
43
+ const entityspec = entitySpecMap(model, target.name) || {}
44
+
45
+ File({ name: 'Schema.' + target.ext }, () => {
46
+ Content(`// ${model.const.Name} ${target.Name} SDK: generated schemas. Do not edit.
47
+ //
48
+ // Generated from the model: \`main.kit.optspec\` and each feature's
49
+ // \`config.options\` for OPTSPEC; entity \`fields[].type\` for ENTITYSPEC.
50
+
51
+ const OPTSPEC = ${JSON.stringify(optspec, null, 2)}
52
+
53
+ const ENTITYSPEC = ${JSON.stringify(entityspec, null, 2)}
54
+
55
+ module.exports = {
56
+ OPTSPEC,
57
+ ENTITYSPEC,
58
+ }
59
+ `)
60
+ })
61
+ })
62
+
63
+
64
+ export {
65
+ Schema
66
+ }
@@ -86,6 +86,35 @@ const Main = cmp(async function Main(props: any) {
86
86
  }
87
87
  }
88
88
 
89
+ // THE FEATURE THE MODEL NEVER MENTIONS AT ALL.
90
+ //
91
+ // inactivePluginExcludes above walks the model's DECLARED features and skips
92
+ // the active ones, which covers `secrets: { active: false }`. It cannot cover
93
+ // the case that actually shipped: a model that does not mention secrets
94
+ // anywhere. Then `main.kit.feature` has no `secrets` key, the loop never sees
95
+ // it, nothing is excluded, and all nine provider clients are copied and
96
+ // compiled — which is what `vocabulary.off.notrimmed` reports, and it failed
97
+ // on 12 of the 68 cedar SDKs.
98
+ //
99
+ // PRECISE, NOT THE WHOLE TREE. The first cut of this excluded
100
+ // `feature/<inactive>/` wholesale and broke the build: SecretsFeature.scala
101
+ // ships to every scala SDK regardless (this target's feature trim is off) and
102
+ // imports `com.voxgig.sekreto` and `voxgig.plugin`, so removing the vendored
103
+ // cores left it uncompilable — "value voxgig is not a member of com".
104
+ //
105
+ // What may go is exactly the nine provider clients under sekreto/plugins.
106
+ // Httpjson.scala and Sigv4.scala live there too and must STAY: both belong to
107
+ // no group and are shared, which model/feature/secrets.aon documents at
108
+ // length after a cloud-only trim once deleted Sigv4 and scalac failed with
109
+ // four "Not found: uriescape".
110
+ const SHARED_SEKRETO_PLUGINS = ['Httpjson.scala', 'Sigv4.scala']
111
+ const pluginDirExcludes: RegExp[] = []
112
+ if (null == (feature as any).secrets) {
113
+ pluginDirExcludes.push(new RegExp(
114
+ '(^|/)feature/secrets/sekreto/plugins/(?!' +
115
+ SHARED_SEKRETO_PLUGINS.map((f) => esc(f)).join('|') + ')[^/]+$'))
116
+ }
117
+
89
118
  Package({ target })
90
119
 
91
120
  Gitignore({})
@@ -119,6 +148,7 @@ const Main = cmp(async function Main(props: any) {
119
148
  TEST_CONTROL_EXCLUDE,
120
149
  ...pluginExcludes(model),
121
150
  ...inactivePluginExcludes,
151
+ ...pluginDirExcludes,
122
152
  ],
123
153
  replace: {
124
154
  ...props.ctx$.stdrep,
@@ -24,6 +24,7 @@ import {
24
24
 
25
25
  import { Package } from './Package_ts'
26
26
  import { Config } from './Config_ts'
27
+ import { Schema } from './Schema_ts'
27
28
  import { Gitignore } from './Gitignore_ts'
28
29
  import { MainEntity } from './MainEntity_ts'
29
30
  import { EntityBase } from './EntityBase_ts'
@@ -192,6 +193,7 @@ if (fres instanceof Promise) { await fres }
192
193
  })
193
194
 
194
195
  Config({ target })
196
+ Schema({ target })
195
197
  EntityBase({ target })
196
198
  EntityTypes({ target })
197
199
 
@@ -0,0 +1,66 @@
1
+ import {
2
+ Content,
3
+ File,
4
+ cmp,
5
+ entitySpecMap,
6
+ optionSpec,
7
+ } from '@voxgig/sdkgen'
8
+
9
+
10
+ import {
11
+ Model,
12
+ } from '@voxgig/apidef'
13
+
14
+
15
+ // THE GENERATED SCHEMA MODULE: the model's schemas, as data the SDK can run.
16
+ //
17
+ // Two exports, both struct.validate specs:
18
+ //
19
+ // OPTSPEC what makeOptions validates the caller's options against —
20
+ // the standard options from `main.kit.optspec` plus one entry
21
+ // per feature this target carries, built by helpers/optspec.
22
+ //
23
+ // ENTITYSPEC per entity, `{ data, op: { <opname> } }` — the record shape
24
+ // and each operation's request shape, mapped from the field
25
+ // type sentinels the model already carries (helpers/canonSpec).
26
+ // Emitted as an EMPTY MAP unless the `validate` feature is
27
+ // active, because nothing else reads it and every byte here is
28
+ // a byte in the consumer's package.
29
+ //
30
+ // A JSON OBJECT LITERAL, not a parsed string: JSON is a subset of the
31
+ // language's own literal syntax, so the values land as data with no parse
32
+ // step, and the backticks the sentinels carry (`\`$STRING\``) survive because
33
+ // JSON.stringify quotes and escapes them. The config emitter has to choose
34
+ // between a literal and a parsed blob because the config can be megabytes;
35
+ // the spec is bounded by the model's field count, so it does not.
36
+ const Schema = cmp(async function Schema(props: any) {
37
+ const ctx$ = props.ctx$
38
+ const target = props.target
39
+
40
+ const model: Model = ctx$.model
41
+
42
+ const optspec = optionSpec(model, target.name)
43
+ const entityspec = entitySpecMap(model, target.name) || {}
44
+
45
+ File({ name: 'Schema.' + target.ext }, () => {
46
+ Content(`// ${model.const.Name} ${target.Name} SDK: generated schemas. Do not edit.
47
+ //
48
+ // Generated from the model: \`main.kit.optspec\` and each feature's
49
+ // \`config.options\` for OPTSPEC; entity \`fields[].type\` for ENTITYSPEC.
50
+
51
+ const OPTSPEC = ${JSON.stringify(optspec, null, 2)}
52
+
53
+ const ENTITYSPEC = ${JSON.stringify(entityspec, null, 2)}
54
+
55
+ export {
56
+ OPTSPEC,
57
+ ENTITYSPEC,
58
+ }
59
+ `)
60
+ })
61
+ })
62
+
63
+
64
+ export {
65
+ Schema
66
+ }
@@ -0,0 +1,296 @@
1
+
2
+ const { ENTITYSPEC } = require('../../Schema')
3
+
4
+ const { BaseFeature } = require('../base/BaseFeature')
5
+
6
+
7
+ // Payload validation against the model's own field types.
8
+ //
9
+ // The specs are NOT written here and not written in the model either: every
10
+ // entity field already carries a canonical type sentinel (`$STRING`,
11
+ // `$INTEGER`, the `$ONE` union for an OpenAPI multi-type), which is the same
12
+ // vocabulary struct.validate speaks. The generator maps them once
13
+ // (helpers/canonSpec) and emits `ENTITYSPEC` beside this file, so a field
14
+ // whose type changes in the API spec changes what this feature enforces with
15
+ // no edit anywhere.
16
+ //
17
+ // WHAT IS CHECKED
18
+ // outbound (PreSpec) the payload the caller asked to send, against
19
+ // `spec.op[<opname>]` — the operation's request shape,
20
+ // which is the SAME partiality policy that decides what
21
+ // the generated `<Name>CreateData` type requires.
22
+ // inbound (PreDone) each record the operation returned, against
23
+ // `spec.data` — the entity's own field types.
24
+ //
25
+ // WHAT IS NOT. The model carries no array element types, no nested object
26
+ // schemas, no enums, formats or bounds (see canonSpec's note), so this checks
27
+ // the shape the model knows and nothing more. It is a guard against the
28
+ // mistakes the model CAN see — a number where a string belongs, a required
29
+ // field left out, a misspelled key under `strict` — not a substitute for the
30
+ // server's own validation.
31
+ class ValidateFeature extends BaseFeature {
32
+ version = '0.0.1'
33
+ name = 'validate'
34
+ active = true
35
+
36
+ _client
37
+ _options = {}
38
+ _spec = {}
39
+
40
+ _request = true
41
+ _response = false
42
+ _mode = 'throw'
43
+
44
+
45
+ init(ctx, options) {
46
+ this._client = ctx.client
47
+ this._options = options || {}
48
+ this.active = options.active
49
+
50
+ // DEFAULTS ARE APPLIED HERE, not by the option spec. The model's
51
+ // `config.options` documents them and types them; it does not inject
52
+ // them, because each feature entry in the spec is optional and struct
53
+ // fills in nothing through an optional union. So every feature resolves
54
+ // its own — and a `mode` left undefined here once meant `'throw' !==
55
+ // undefined`, which silently turned every rejection into a no-op.
56
+ this._request = false !== this._options.request
57
+ this._response = true === this._options.response
58
+
59
+ // FAIL CLOSED. Only the exact string 'report' selects report mode, so a
60
+ // typo (`mode: 'thow'`) still rejects rather than silently turning
61
+ // enforcement off — the failure nobody would notice. The option spec
62
+ // rejects the typo outright; this is what happens if it ever does not.
63
+ this._mode = 'report' === this._options.mode ? 'report' : 'throw'
64
+
65
+ // `strict` is applied ONCE, here, by rebuilding the spec tree without the
66
+ // `$OPEN` markers — rather than per call, which would clone a spec for
67
+ // every request an SDK ever makes.
68
+ this._spec = true === this._options.strict ? close(ENTITYSPEC) : ENTITYSPEC
69
+ }
70
+
71
+
72
+ // Outbound. `makeSpec` short-circuits on an `ctx.out.spec` that is already
73
+ // set, so assigning the error here rejects the operation before the request
74
+ // is built — the same seam rbac uses one stage earlier.
75
+ PreSpec(ctx) {
76
+ if (!this.active || !this._request) {
77
+ return
78
+ }
79
+
80
+ const opname = (ctx.op && ctx.op.name) || ''
81
+ const spec = this._entitySpec(ctx)
82
+ const opspec = spec && spec.op ? spec.op[opname] : null
83
+
84
+ if (null == opspec) {
85
+ return
86
+ }
87
+
88
+ const errs = this._check(ctx, this._payload(ctx, opname), opspec, 'request')
89
+ if (0 === errs.length || 'report' === this._mode) {
90
+ return
91
+ }
92
+
93
+ const err = ctx.error('validate_failed',
94
+ 'Invalid ' + opname + ' request for entity "' + entname(ctx) + '": ' +
95
+ errs.join('; '))
96
+ ctx.out.spec = err
97
+ return err
98
+ }
99
+
100
+
101
+ // Inbound. PreDone rather than PreResult: the records are extracted from
102
+ // the response body by `makeResult`, which runs between the two, so at
103
+ // PreResult there is nothing to check but the envelope.
104
+ //
105
+ // HOOK ORDER MATTERS HERE, and the default order is not the one you want.
106
+ // PreDone hooks fire in feature ADD order, which defaults to `test` first
107
+ // and then names sorted — and `validate` sorts last, after `audit`,
108
+ // `cost`, `debug`, `metrics` and `telemetry`. Those observers therefore
109
+ // record the operation as a success before this hook has looked at it.
110
+ // Activating features as an ORDERED ARRAY fixes it:
111
+ //
112
+ // feature: [{ name: 'validate', active: true, response: true },
113
+ // { name: 'metrics', active: true }]
114
+ //
115
+ // What this feature can fix from here, it does: the result is marked
116
+ // failed and its records are cleared, so the entity absorbs nothing.
117
+ PreDone(ctx) {
118
+ if (!this.active || !this._response) {
119
+ return
120
+ }
121
+
122
+ const spec = this._entitySpec(ctx)
123
+ if (null == spec || null == spec.data) {
124
+ return
125
+ }
126
+
127
+ const resdata = ctx.result && ctx.result.resdata
128
+ if (null == resdata) {
129
+ return
130
+ }
131
+
132
+ // A list op returns many records and a load returns one; both are checked
133
+ // against the same record spec, because they are the same entity.
134
+ const records = Array.isArray(resdata) ? resdata : [resdata]
135
+ const errs = []
136
+ for (const record of records) {
137
+ if (null == record) {
138
+ continue
139
+ }
140
+
141
+ // A NON-OBJECT IS A FAILURE, not something to skip. A load that
142
+ // answered `42` where the entity's spec wants a record used to pass
143
+ // this feature silently, which is the one outcome a validator must
144
+ // never produce. struct rejects it with the field it could not find.
145
+ for (const e of this._check(ctx, unwrap(record), spec.data, 'response')) {
146
+ errs.push(e)
147
+ }
148
+ }
149
+
150
+ if (0 === errs.length || 'report' === this._mode) {
151
+ return
152
+ }
153
+
154
+ const err = ctx.error('validate_failed',
155
+ 'Invalid response for entity "' + entname(ctx) + '": ' + errs.join('; '))
156
+
157
+ // BOTH, and `ok` is the load-bearing half: `done` returns `resdata`
158
+ // whenever `result.ok` is true and never looks at `err`, so setting the
159
+ // error alone handed the caller the very records that failed the spec.
160
+ ctx.result.ok = false
161
+ ctx.result.err = err
162
+
163
+ // AND THE DATA GOES. The load/update fragments copy `result.resdata`
164
+ // into the entity's own state on any non-null value, BEFORE `done`
165
+ // raises — so rejecting the operation while leaving the records in place
166
+ // left the caller holding an entity populated from a payload this
167
+ // feature had just declared invalid. Clearing it is the only half of
168
+ // that this feature owns; see the note on hook order below.
169
+ ctx.result.resdata = undefined
170
+
171
+ return err
172
+ }
173
+
174
+
175
+ // The payload an operation is about to send.
176
+ //
177
+ // TWO SLOTS, AND THE OP PICKS. A body op (create/update/patch) carries the
178
+ // caller's argument in `reqdata` over the entity's `data`; a match op
179
+ // (load/list/remove) carries it in `reqmatch` over `match`. That is what
180
+ // the Entity*Op fragments pass to makeContext and what makePoint reads
181
+ // (`getprop(ctx, 'req' + op.input)`) — so reading `reqdata` for every op
182
+ // checked a `load({ id })` against the entity's STALE stored match and
183
+ // rejected it for the id the caller had just supplied.
184
+ _payload(ctx, opname) {
185
+ const body = 'create' === opname || 'update' === opname || 'patch' === opname
186
+
187
+ const base = body ? ctx.data : ctx.match
188
+ const req = body ? ctx.reqdata : ctx.reqmatch
189
+
190
+ const out = { ...(base || {}), ...(req || {}) }
191
+
192
+ // `$action` SELECTS A CUSTOM ENDPOINT; it is not a field of the record.
193
+ // makePoint reads it off this same argument and the request transformer
194
+ // drops it before the body is built, so a spec built from the API's own
195
+ // fields will never name it — and under `strict` every custom-action
196
+ // call would be rejected for the one key that made it reachable.
197
+ delete out.$action
198
+
199
+ return out
200
+ }
201
+
202
+
203
+ _entitySpec(ctx) {
204
+ return this._spec[entname(ctx)]
205
+ }
206
+
207
+
208
+ // One validate call. Errors are COLLECTED, never thrown: struct throws on
209
+ // the first failure unless given an `errs` array, and a caller fixing a
210
+ // payload wants every problem with it, not the first one.
211
+ _check(ctx, data, spec, direction) {
212
+ const struct = ctx.utility.struct
213
+ const errs = []
214
+
215
+ try {
216
+ struct.validate(data, spec, { errs })
217
+ }
218
+ catch (e) {
219
+ // A spec this port cannot run at all (rather than a payload that fails
220
+ // it) must not take the operation down with it: report it like any
221
+ // other failure and let `mode` decide.
222
+ errs.push(e && e.message ? e.message : String(e))
223
+ }
224
+
225
+ if (0 < errs.length && 'function' === typeof this._options.onInvalid) {
226
+ try {
227
+ this._options.onInvalid({
228
+ entity: entname(ctx),
229
+ op: (ctx.op && ctx.op.name) || '',
230
+ direction,
231
+ errs,
232
+ data,
233
+ })
234
+ }
235
+ catch (_e) { }
236
+ }
237
+
238
+ return errs
239
+ }
240
+ }
241
+
242
+
243
+ // A RESULT RECORD AS DATA.
244
+ //
245
+ // `makeResult` turns every record of a LIST into an entity instance
246
+ // (`entity.make()` then `ent.data(entry)`), so what reaches PreDone for a
247
+ // list is wrappers, not records — and a wrapper checked against a field spec
248
+ // fails on every required field while its actual data goes unchecked. A load
249
+ // returns the record itself, so this has to handle both.
250
+ function unwrap(record) {
251
+ if (null != record && 'function' === typeof record.data) {
252
+ const data = record.data()
253
+ if (null != data) {
254
+ return data
255
+ }
256
+ }
257
+ return record
258
+ }
259
+
260
+
261
+ function entname(ctx) {
262
+ return (ctx.entity && ctx.entity.name) || (ctx.op && ctx.op.entity) || ''
263
+ }
264
+
265
+
266
+ // The spec tree with every `$OPEN` marker removed, so an undeclared key is an
267
+ // error rather than a pass. Rebuilt rather than mutated: ENTITYSPEC is a
268
+ // module constant shared by every client in the process.
269
+ function close(node) {
270
+ if (Array.isArray(node)) {
271
+ return node.map((n) => close(n))
272
+ }
273
+
274
+ if (null == node || 'object' !== typeof node) {
275
+ return node
276
+ }
277
+
278
+ const out = {}
279
+ for (const key of Object.keys(node)) {
280
+ if (OPEN === key) {
281
+ continue
282
+ }
283
+ out[key] = close(node[key])
284
+ }
285
+
286
+ return out
287
+ }
288
+
289
+
290
+ // Built rather than written, so the backticks cannot be lost in an edit.
291
+ const OPEN = String.fromCharCode(96) + '$OPEN' + String.fromCharCode(96)
292
+
293
+
294
+ module.exports = {
295
+ ValidateFeature
296
+ }
@@ -1,4 +1,7 @@
1
1
 
2
+ const { OPTSPEC } = require('../Schema')
3
+
4
+
2
5
  function makeOptions(ctx) {
3
6
  const utility = ctx.utility
4
7
  const options = ctx.options
@@ -57,67 +60,19 @@ function makeOptions(ctx) {
57
60
  let config = ctx.config || {}
58
61
  let cfgopts = config.options || {}
59
62
 
60
- // Standard SDK option values.
61
- const optspec = {
62
- apikey: '',
63
- secret: '',
64
- base: 'http://localhost:8000',
65
- prefix: '',
66
- suffix: '',
67
- auth: {
68
- prefix: '',
69
- basic: false
70
- },
71
- headers: {
72
- '`$CHILD`': '`$STRING`'
73
- },
74
- allow: {
75
- method: 'GET,PUT,POST,PATCH,DELETE,OPTIONS',
76
- op: 'create,update,load,list,remove,command,direct,graphql'
77
- },
78
- entity: {
79
- '`$CHILD`': {
80
- '`$OPEN`': true,
81
- active: false,
82
- alias: {}
83
- }
84
- },
85
- feature: {
86
- '`$CHILD`': {
87
- '`$OPEN`': true,
88
- active: false,
89
- }
90
- },
91
- utility: {},
92
- // Feature INSTANCES supplied at construction (the station adopt
93
- // path): consumed by the constructor's featureAdd loop, so they are
94
- // class instances, not data - `$ANY` accepts them verbatim. Without
95
- // this entry the seam is dead: the constructor reads
96
- // options.extend, but validate rejected the key.
97
- extend: '`$ANY`',
98
- system: {
99
- fetch: undefined
100
- },
101
- test: {
102
- active: false,
103
- entity: {
104
- '`$OPEN`': true,
105
- }
106
- },
107
- clean: {
108
- keys: 'key,token,id'
109
- },
110
- // Server-variable values for a templated base URL (OpenAPI server
111
- // variables): `{name}` placeholders in `base` are substituted from
112
- // this map at construction. Spec defaults arrive via the generated
113
- // Config; user values override them.
114
- server: {
115
- '`$CHILD`': ''
116
- }
117
- }
118
-
119
- // JavaScript specific option values.
120
- optspec.system.fetch = opts.system?.fetch || global.fetch
63
+ // THE OPTION SPEC IS GENERATED, NOT WRITTEN HERE.
64
+ //
65
+ // `Spec.OPTSPEC` is built from the model: `main.kit.optspec` for the
66
+ // standard options, plus one entry per feature this target carries, taken
67
+ // from that feature's own `config.options` / `config.optspec`. Editing this
68
+ // file to add an option would put it back where it was — one of twenty
69
+ // hand-maintained copies of a schema nothing cross-checked — so add it to
70
+ // the model instead and every ported target validates it.
71
+ //
72
+ // NOT MUTATED. It is a module-level constant shared by every client this
73
+ // process constructs; the platform default below is applied to the RESULT,
74
+ // never to the spec.
75
+ const optspec = OPTSPEC
121
76
 
122
77
  // Clone the config side before merging: `config` is a module-level
123
78
  // singleton in ts/js, and merge would otherwise use its nested maps as
@@ -127,6 +82,16 @@ function makeOptions(ctx) {
127
82
 
128
83
  opts = validate(opts, optspec)
129
84
 
85
+ // The platform fetch, supplied AFTER validate rather than as a spec
86
+ // default. `system.fetch` is declared `$ANY`, which passes a caller's own
87
+ // fetch through untouched but inserts nothing when the key is absent — and
88
+ // the spec is shared, so writing the default into it (as this did while the
89
+ // spec was a per-call literal) would hand one client's fetch to the next.
90
+ opts.system = opts.system || {}
91
+ if (null == opts.system.fetch) {
92
+ opts.system.fetch = global.fetch
93
+ }
94
+
130
95
  // Restore the suppression the optspec default would otherwise erase.
131
96
  if (authsuppressed) {
132
97
  opts.auth = null