@voxgig/sdkgen 4.19.0 → 4.20.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 (112) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/cmp/AgentGuideContent.js +1 -1
  3. package/dist/sdkgen.d.ts +2 -2
  4. package/dist/sdkgen.js +6 -3
  5. package/dist/sdkgen.js.map +1 -1
  6. package/dist/testkit.js +2 -2
  7. package/dist/testkit.js.map +1 -1
  8. package/dist/tsconfig.tsbuildinfo +1 -1
  9. package/dist/utility.d.ts +4 -1
  10. package/dist/utility.js +60 -0
  11. package/dist/utility.js.map +1 -1
  12. package/model/sdkgen.aon +10 -0
  13. package/package.json +1 -1
  14. package/project/.sdk/model/feature/feature-index.aon +20 -20
  15. package/project/.sdk/src/cmp/c/Config_c.ts +15 -0
  16. package/project/.sdk/src/cmp/c/Main_c.ts +15 -0
  17. package/project/.sdk/src/cmp/c/PrepareAuth_c.ts +371 -0
  18. package/project/.sdk/src/cmp/clojure/Config_clojure.ts +47 -1
  19. package/project/.sdk/src/cmp/clojure/Main_clojure.ts +11 -1
  20. package/project/.sdk/src/cmp/clojure/PrepareAuth_clojure.ts +374 -0
  21. package/project/.sdk/src/cmp/cpp/Config_cpp.ts +40 -0
  22. package/project/.sdk/src/cmp/cpp/Main_cpp.ts +17 -0
  23. package/project/.sdk/src/cmp/cpp/PrepareAuth_cpp.ts +441 -0
  24. package/project/.sdk/src/cmp/csharp/Config_csharp.ts +42 -1
  25. package/project/.sdk/src/cmp/csharp/Main_csharp.ts +18 -0
  26. package/project/.sdk/src/cmp/csharp/PrepareAuth_csharp.ts +392 -0
  27. package/project/.sdk/src/cmp/elixir/Config_elixir.ts +36 -1
  28. package/project/.sdk/src/cmp/elixir/Main_elixir.ts +15 -0
  29. package/project/.sdk/src/cmp/elixir/PrepareAuth_elixir.ts +418 -0
  30. package/project/.sdk/src/cmp/go/Config_go.ts +12 -1
  31. package/project/.sdk/src/cmp/go/Main_go.ts +14 -0
  32. package/project/.sdk/src/cmp/go/PrepareAuth_go.ts +322 -0
  33. package/project/.sdk/src/cmp/java/Config_java.ts +20 -1
  34. package/project/.sdk/src/cmp/java/Main_java.ts +14 -0
  35. package/project/.sdk/src/cmp/java/PrepareAuth_java.ts +298 -0
  36. package/project/.sdk/src/cmp/js/Config_js.ts +12 -1
  37. package/project/.sdk/src/cmp/js/Main_js.ts +6 -0
  38. package/project/.sdk/src/cmp/js/PrepareAuth_js.ts +244 -0
  39. package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +23 -1
  40. package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +20 -0
  41. package/project/.sdk/src/cmp/kotlin/PrepareAuth_kotlin.ts +440 -0
  42. package/project/.sdk/src/cmp/lua/Config_lua.ts +15 -1
  43. package/project/.sdk/src/cmp/lua/Main_lua.ts +11 -0
  44. package/project/.sdk/src/cmp/lua/PrepareAuth_lua.ts +364 -0
  45. package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +31 -1
  46. package/project/.sdk/src/cmp/ocaml/Main_ocaml.ts +19 -0
  47. package/project/.sdk/src/cmp/ocaml/PrepareAuth_ocaml.ts +454 -0
  48. package/project/.sdk/src/cmp/perl/Config_perl.ts +30 -0
  49. package/project/.sdk/src/cmp/perl/Main_perl.ts +14 -0
  50. package/project/.sdk/src/cmp/perl/PrepareAuth_perl.ts +387 -0
  51. package/project/.sdk/src/cmp/php/Config_php.ts +12 -1
  52. package/project/.sdk/src/cmp/php/Main_php.ts +7 -0
  53. package/project/.sdk/src/cmp/php/PrepareAuth_php.ts +325 -0
  54. package/project/.sdk/src/cmp/py/Config_py.ts +12 -1
  55. package/project/.sdk/src/cmp/py/Main_py.ts +12 -0
  56. package/project/.sdk/src/cmp/py/PrepareAuth_py.ts +389 -0
  57. package/project/.sdk/src/cmp/rb/Config_rb.ts +18 -1
  58. package/project/.sdk/src/cmp/rb/Main_rb.ts +10 -0
  59. package/project/.sdk/src/cmp/rb/PrepareAuth_rb.ts +305 -0
  60. package/project/.sdk/src/cmp/rust/Config_rust.ts +41 -1
  61. package/project/.sdk/src/cmp/rust/Main_rust.ts +16 -0
  62. package/project/.sdk/src/cmp/rust/PrepareAuth_rust.ts +410 -0
  63. package/project/.sdk/src/cmp/scala/Config_scala.ts +21 -1
  64. package/project/.sdk/src/cmp/scala/Main_scala.ts +14 -0
  65. package/project/.sdk/src/cmp/scala/PrepareAuth_scala.ts +415 -0
  66. package/project/.sdk/src/cmp/swift/Config_swift.ts +37 -1
  67. package/project/.sdk/src/cmp/swift/Main_swift.ts +10 -0
  68. package/project/.sdk/src/cmp/swift/PrepareAuth_swift.ts +439 -0
  69. package/project/.sdk/src/cmp/ts/Config_ts.ts +12 -1
  70. package/project/.sdk/src/cmp/ts/Main_ts.ts +5 -0
  71. package/project/.sdk/src/cmp/ts/PrepareAuth_ts.ts +237 -0
  72. package/project/.sdk/src/cmp/zig/Config_zig.ts +33 -1
  73. package/project/.sdk/src/cmp/zig/Main_zig.ts +16 -0
  74. package/project/.sdk/src/cmp/zig/PrepareAuth_zig.ts +545 -0
  75. package/project/.sdk/tm/clojure/src/sdk/core.clj +31 -20
  76. package/project/.sdk/tm/cpp/utility/pipeline.hpp +36 -40
  77. package/project/.sdk/tm/csharp/utility/MakeOptions.cs +11 -0
  78. package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -32
  79. package/project/.sdk/tm/java/utility/MakeOptions.java +13 -1
  80. package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +14 -1
  81. package/project/.sdk/tm/kotlin/utility/Prepare.kt +13 -46
  82. package/project/.sdk/tm/ocaml/Makefile +11 -1
  83. package/project/.sdk/tm/ocaml/sdk_helpers.ml +15 -0
  84. package/project/.sdk/tm/ocaml/sdk_runtime.ml +31 -25
  85. package/project/.sdk/tm/ocaml/test/feature/secrets/t_secrets.ml +125 -12
  86. package/project/.sdk/tm/ocaml/test/t_pipeline.ml +64 -6
  87. package/project/.sdk/tm/perl/utility/make_options.pm +10 -1
  88. package/project/.sdk/tm/php/utility/MakeOptions.php +8 -1
  89. package/project/.sdk/tm/rust/utility/make_options.rs +13 -1
  90. package/project/.sdk/tm/scala/utility/Make.scala +14 -1
  91. package/project/.sdk/tm/scala/utility/Prepare.scala +16 -40
  92. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +30 -3
  93. package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +21 -39
  94. package/project/.sdk/tm/zig/core/utility.zig +37 -45
  95. package/project/.sdk/tm/zig/test/pipeline_test.zig +34 -0
  96. package/project/sdkgen-package.json +1 -1
  97. package/src/cmp/AgentGuideContent.ts +1 -1
  98. package/src/sdkgen.ts +4 -1
  99. package/src/testkit.ts +2 -2
  100. package/src/utility.ts +75 -0
  101. package/project/.sdk/tm/c/utility/prepare_auth.c +0 -65
  102. package/project/.sdk/tm/csharp/utility/PrepareAuth.cs +0 -54
  103. package/project/.sdk/tm/go/utility/prepare_auth.go +0 -59
  104. package/project/.sdk/tm/java/utility/PrepareAuth.java +0 -66
  105. package/project/.sdk/tm/js/src/utility/PrepareAuthUtility.js +0 -49
  106. package/project/.sdk/tm/lua/utility/prepare_auth.lua +0 -52
  107. package/project/.sdk/tm/perl/utility/prepare_auth.pm +0 -56
  108. package/project/.sdk/tm/php/utility/PrepareAuth.php +0 -45
  109. package/project/.sdk/tm/py/pkg/utility/prepare_auth.py +0 -46
  110. package/project/.sdk/tm/rb/utility/prepare_auth.rb +0 -35
  111. package/project/.sdk/tm/rust/utility/prepare_auth.rs +0 -65
  112. package/project/.sdk/tm/ts/src/utility/PrepareAuthUtility.ts +0 -78
@@ -0,0 +1,322 @@
1
+
2
+ import {
3
+ Content,
4
+ File,
5
+ Folder,
6
+ cmp,
7
+ goModule,
8
+ isHttpBasicAuth,
9
+ resolveAuthIn,
10
+ resolveAuthName,
11
+ resolveAuthPrefix,
12
+ } from '@voxgig/sdkgen'
13
+
14
+
15
+ import {
16
+ KIT,
17
+ getModelPath,
18
+ } from '@voxgig/apidef'
19
+
20
+
21
+ // WHERE THE CREDENTIAL GOES IS A FACT ABOUT THE API, so it is generated
22
+ // rather than templated. This is the go peer of PrepareAuth_ts; read that
23
+ // one first, it carries the full account of the defect.
24
+ //
25
+ // This was a static file at `tm/go/utility/prepare_auth.go` that hardcoded
26
+ //
27
+ // const headerAuth = "authorization"
28
+ //
29
+ // apidef has always resolved the scheme's `in` and `name` into
30
+ // `main.kit.info.security` — joplin's says `in: "query", name: "token"` —
31
+ // and generation dropped both, so the SDK sent a header the API does not
32
+ // read and never sent the query parameter it does.
33
+ //
34
+ // A template cannot fix this, because the three placements need three
35
+ // different bodies and a template has to pick one. A component emits the
36
+ // branch this API actually uses and nothing else — no dead query code in a
37
+ // bearer-token SDK, and no runtime `if` on a value fixed at generation
38
+ // time. Go makes that stricter than most targets: an unused local is a
39
+ // COMPILE ERROR, so a file that resolved `authPrefix` and then dropped it
40
+ // for a query placement would not build at all.
41
+ const PrepareAuth = cmp(async function PrepareAuth(props: any) {
42
+ const { target } = props
43
+ const { model } = props.ctx$
44
+
45
+ // The generated source cannot use the `GOMODULE` / `github.com/voxgig/struct`
46
+ // placeholders: those are rewritten by Main's `Copy({from:'tm/go'})`, and
47
+ // this file never travels through that Copy. The real import paths go in
48
+ // here, the same way EntityTypes_go resolves them.
49
+ const gomodule = goModule(model, target.name)
50
+
51
+ const active = isAuthActive_go(model)
52
+ const where = resolveAuthIn(model)
53
+ const name = resolveAuthName(model)
54
+ const prefix = resolveAuthPrefix(model)
55
+ const basic = isHttpBasicAuth(model)
56
+
57
+ // FOLDER NESTING. Main_go opens NO folder around this call: go's layout is
58
+ // flat at the target root (core/, utility/, feature/, entity/, test/), the
59
+ // template this replaces lived at `tm/go/utility/prepare_auth.go`, and
60
+ // `Copy({from:'tm/go'})` lands it at `<root>/utility/`. So the `utility`
61
+ // folder is opened HERE, exactly as EntityTypes_go opens `entity`.
62
+ //
63
+ // The call site matters as much: Config is inside `Folder({name:'core'})`,
64
+ // and putting PrepareAuth beside it would write `core/utility/prepare_auth.go`
65
+ // — a second `package utility` nobody imports, while the registrar keeps
66
+ // calling whatever `utility/` still holds. Main_go calls this at ROOT level.
67
+ Folder({ name: 'utility' }, () => {
68
+ File({ name: 'prepare_auth.' + target.ext }, () => {
69
+ Content(render({ gomodule, active, where, name, prefix, basic }))
70
+ })
71
+ })
72
+ })
73
+
74
+
75
+ type AuthSpec = {
76
+ gomodule: string
77
+ active: boolean
78
+ where: string
79
+ name: string
80
+ prefix: string
81
+ basic: boolean
82
+ }
83
+
84
+
85
+ function render(spec: AuthSpec): string {
86
+
87
+ // NO AUTH AT ALL. A public API's SDK gets a prepareAuth that is honest
88
+ // about it rather than one that deletes a header nobody set. `vs` is left
89
+ // out of the imports deliberately — go rejects an unused import.
90
+ if (!spec.active) {
91
+ return `package utility
92
+
93
+ import (
94
+ "${spec.gomodule}/core"
95
+ )
96
+
97
+ // This API declares no authentication, so there is no credential to place.
98
+ // The function stays in the pipeline because makeSpec calls it
99
+ // unconditionally.
100
+ func prepareAuthUtil(ctx *core.Context) (*core.Spec, error) {
101
+ spec := ctx.Spec
102
+ if spec == nil {
103
+ return nil, ctx.MakeError("auth_no_spec",
104
+ "Expected context spec property to be defined.")
105
+ }
106
+
107
+ return spec, nil
108
+ }
109
+ `
110
+ }
111
+
112
+ // HTTP Basic is header-only by definition: the scheme is
113
+ // `Authorization: Basic base64(user:pass)`. It cannot be expressed as a
114
+ // query parameter or a cookie, so the branch is emitted only where it can
115
+ // mean something — and base64 is imported only then.
116
+ const withBasic = spec.basic && 'header' === spec.where
117
+
118
+ const head = `package utility
119
+
120
+ import (
121
+ ${withBasic ? ` "encoding/base64"
122
+
123
+ ` : ''} vs "${spec.gomodule}/utility/struct"
124
+
125
+ "${spec.gomodule}/core"
126
+ )
127
+
128
+ const credName = "${gostr(credLiteral(spec.where, spec.name))}"
129
+ ${'cookie' === spec.where ? `const cookieHeader = "cookie"
130
+ ` : ''}const optionApikey = "apikey"
131
+ ${withBasic ? `const optionSecret = "secret"
132
+ ` : ''}const notFound = "__NOTFOUND__"
133
+
134
+ func prepareAuthUtil(ctx *core.Context) (*core.Spec, error) {
135
+ spec := ctx.Spec
136
+ if spec == nil {
137
+ return nil, ctx.MakeError("auth_no_spec",
138
+ "Expected context spec property to be defined.")
139
+ }
140
+
141
+ ${bagName(spec.where)} := spec.${bagField(spec.where)}
142
+ options := ctx.Client.OptionsMap()
143
+
144
+ // Public APIs that need no auth omit the options.auth block entirely.
145
+ if options["auth"] == nil {
146
+ ${clear(spec.where)}
147
+ return spec, nil
148
+ }
149
+
150
+ apikey := vs.GetProp(options, optionApikey, notFound)
151
+
152
+ skip := false
153
+ if apikey == nil {
154
+ skip = true
155
+ } else if apikeyStr, ok := apikey.(string); ok &&
156
+ (apikeyStr == notFound || apikeyStr == "") {
157
+ skip = true
158
+ }
159
+ `
160
+
161
+ const basicBlock = !withBasic ? '' : `
162
+ // True HTTP Basic Auth needs TWO credentials, base64-joined - a single
163
+ // token in the header (the branch below) can never authenticate against
164
+ // an API that actually checks \`Authorization: Basic base64(user:pass)\`.
165
+ if basicAuth, _ := vs.GetPath(options, []any{"auth", "basic"}).(bool); basicAuth {
166
+ secret := vs.GetProp(options, optionSecret, notFound)
167
+
168
+ noSecret := false
169
+ if secret == nil {
170
+ noSecret = true
171
+ } else if secretStr, ok := secret.(string); ok &&
172
+ (secretStr == notFound || secretStr == "") {
173
+ noSecret = true
174
+ }
175
+
176
+ if skip || noSecret {
177
+ ${clear(spec.where)}
178
+ } else {
179
+ apikeyVal, _ := apikey.(string)
180
+ secretVal, _ := secret.(string)
181
+ b64 := base64.StdEncoding.EncodeToString([]byte(apikeyVal + ":" + secretVal))
182
+
183
+ basicPrefix := ""
184
+ if ap := vs.GetPath(options, []any{"auth", "prefix"}); ap != nil {
185
+ basicPrefix, _ = ap.(string)
186
+ }
187
+ // Empty prefix (raw apiKey credential) must not add a leading space.
188
+ if basicPrefix == "" {
189
+ ${bagName(spec.where)}[credName] = b64
190
+ } else {
191
+ ${bagName(spec.where)}[credName] = basicPrefix + " " + b64
192
+ }
193
+ }
194
+
195
+ return spec, nil
196
+ }
197
+ `
198
+
199
+ return head + basicBlock + `
200
+ if skip {
201
+ ${clear(spec.where)}
202
+ } else {
203
+ ${place(spec.where)}
204
+ }
205
+
206
+ return spec, nil
207
+ }
208
+ `
209
+ }
210
+
211
+
212
+ // The credential's key, as it goes into the bag.
213
+ //
214
+ // A HEADER name is lowercased. HTTP header names are case-insensitive
215
+ // (RFC 9110 5.1) and go's `req.Header.Set` canonicalises on the wire, so
216
+ // nothing changes over the socket — but this SDK's own header map is keyed
217
+ // in lowercase throughout (`content-type`, and the `authorization` that the
218
+ // generated pipeline/feature/secrets tests assert on), and the shipped
219
+ // `prepare_auth.go` said `"authorization"`. Emitting the resolver's
220
+ // title-cased default here would have left every header SDK's own test
221
+ // suite failing on a purely cosmetic difference.
222
+ //
223
+ // A QUERY parameter and a COOKIE name are case-SENSITIVE, so those go in
224
+ // verbatim: `?token=` is not `?Token=`.
225
+ function credLiteral(where: string, name: string): string {
226
+ return 'header' === where ? String(name).toLowerCase() : String(name)
227
+ }
228
+
229
+
230
+ // The bag the credential lands in, per placement. Cookies ride the header
231
+ // bag because a cookie IS a header.
232
+ function bagName(where: string): string {
233
+ return 'query' === where ? 'query' : 'headers'
234
+ }
235
+
236
+
237
+ function bagField(where: string): string {
238
+ return 'query' === where ? 'Query' : 'Headers'
239
+ }
240
+
241
+
242
+ function clear(where: string): string {
243
+ return `delete(${bagName(where)}, credName)`
244
+ }
245
+
246
+
247
+ function place(where: string): string {
248
+ if ('query' === where) {
249
+ // NO PREFIX IN A QUERY STRING. `?token=Bearer%20abc` is not a thing any
250
+ // API reads; the prefix is a header convention and is dropped here
251
+ // deliberately rather than silently concatenated.
252
+ return ` apikeyVal := ""
253
+ if av, ok := apikey.(string); ok {
254
+ apikeyVal = av
255
+ }
256
+ query[credName] = apikeyVal`
257
+ }
258
+
259
+ if ('cookie' === where) {
260
+ // Append, never replace: the cookie header may already carry pairs this
261
+ // SDK did not set, and clobbering it would drop them.
262
+ return ` apikeyVal := ""
263
+ if av, ok := apikey.(string); ok {
264
+ apikeyVal = av
265
+ }
266
+ pair := credName + "=" + apikeyVal
267
+ existing := ""
268
+ if ec, ok := headers[cookieHeader].(string); ok {
269
+ existing = ec
270
+ }
271
+ if existing == "" {
272
+ headers[cookieHeader] = pair
273
+ } else {
274
+ headers[cookieHeader] = existing + "; " + pair
275
+ }`
276
+ }
277
+
278
+ return ` authPrefix := ""
279
+ if ap := vs.GetPath(options, []any{"auth", "prefix"}); ap != nil {
280
+ authPrefix, _ = ap.(string)
281
+ }
282
+ apikeyVal := ""
283
+ if av, ok := apikey.(string); ok {
284
+ apikeyVal = av
285
+ }
286
+ // Empty prefix (raw apiKey credential) must not add a leading space.
287
+ if authPrefix == "" {
288
+ headers[credName] = apikeyVal
289
+ } else {
290
+ headers[credName] = authPrefix + " " + apikeyVal
291
+ }`
292
+ }
293
+
294
+
295
+ function gostr(s: string): string {
296
+ return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
297
+ }
298
+
299
+
300
+ export {
301
+ PrepareAuth
302
+ }
303
+
304
+
305
+ // NOT `isAuthActive`, AND THE DIFFERENCE IS LOAD-BEARING. That helper is
306
+ // also false whenever the SPEC declares no security scheme
307
+ // (`main.kit.info.auth: false`) — a statement about the DEFINITION, not a
308
+ // ban on ever sending a credential. `optspec` still declares `apikey` and
309
+ // makeOptions fills `options.auth` from its defaults, so the runtime
310
+ // `options.auth == null` guard never fired and those SDKs have always sent
311
+ // the credential. Gating the body on `isAuthActive` does not trim dead
312
+ // code, it removes working authentication — which
313
+ // `go: auth null suppresses the credential` catches, and which takes the
314
+ // secrets feature down with it.
315
+ //
316
+ // `main.kit.config.auth.active: false` is the project saying "no credential,
317
+ // ever", and it is the only signal that can be honoured before runtime.
318
+ function isAuthActive_go(model: any): boolean {
319
+ const auth = getModelPath(model, `main.${KIT}.config.auth`,
320
+ { only_active: false, required: false })
321
+ return !(null != auth && false === auth.active)
322
+ }
@@ -6,6 +6,9 @@ import {
6
6
  configDefinition,
7
7
  each,
8
8
  isAuthActive,
9
+ isHttpBasicAuth,
10
+ resolveAuthIn,
11
+ resolveAuthName,
9
12
  resolveAuthPrefix,
10
13
  targetFeatures,
11
14
  } from '@voxgig/sdkgen'
@@ -53,6 +56,14 @@ const Config = cmp(async function Config(props: any) {
53
56
  const authActive = isAuthActive(model)
54
57
  // config.auth.prefix override -> spec-derived info.security.prefix -> 'Bearer'
55
58
  const authPrefix = resolveAuthPrefix(model)
59
+ const authBasic = isHttpBasicAuth(model)
60
+ // `in` and `name` travel with the prefix now. They were resolved by
61
+ // apidef all along and dropped here, so an apiKey-in-query API got an
62
+ // Authorization header it does not read. Emitted only when they differ
63
+ // from the defaults, so a header/Authorization SDK is byte-identical to
64
+ // what it generated before.
65
+ const authIn = resolveAuthIn(model)
66
+ const authName = resolveAuthName(model)
56
67
 
57
68
  let baseUrl = ''
58
69
  try { baseUrl = getModelPath(model, `main.${KIT}.info.servers.0.url`) } catch (_e) { }
@@ -174,7 +185,15 @@ const Config = cmp(async function Config(props: any) {
174
185
  }
175
186
 
176
187
  if (authActive) {
177
- options.auth = { prefix: authPrefix }
188
+ const auth: Record<string, any> = { prefix: authPrefix }
189
+ // `basic` joins it for the same reason: the generated prepareAuth only
190
+ // emits the base64(user:pass) branch for a spec-declared HTTP Basic
191
+ // scheme, and that branch reads this option at runtime — without it the
192
+ // branch could never fire.
193
+ if (authBasic) { auth.basic = true }
194
+ if ('header' !== authIn) { auth.in = authIn }
195
+ if ('Authorization' !== authName) { auth.name = authName }
196
+ options.auth = auth
178
197
  }
179
198
  options.headers = headers
180
199
  options.entity = optionsEntity
@@ -28,6 +28,7 @@ import { MainEntity } from './MainEntity_java'
28
28
  import { EntityBase } from './EntityBase_java'
29
29
  import { EntityTypes } from './EntityTypes_java'
30
30
  import { SdkError } from './SdkError_java'
31
+ import { PrepareAuth } from './PrepareAuth_java'
31
32
  import { javaPackage } from './utility_java'
32
33
 
33
34
 
@@ -72,6 +73,19 @@ const Main = cmp(async function Main(props: any) {
72
73
  // Shared entity runtime (entity/EntityBase.java).
73
74
  EntityBase({ target })
74
75
 
76
+ // utility/PrepareAuth.java. WHERE the credential goes (header, query
77
+ // parameter or cookie) and UNDER WHAT NAME are facts about the API, so
78
+ // the file is generated from the model rather than copied from tm/ —
79
+ // which is why tm/java/utility/PrepareAuth.java was deleted: the blanket
80
+ // Copy above and this component would otherwise both claim that path and
81
+ // jostraca refuses a duplicate output path.
82
+ //
83
+ // Called HERE, at Main's top level, and NOT inside the `core` Folder
84
+ // below. The java target is flat — utility/ is a SIBLING of core/, not a
85
+ // child — and the component opens `utility` itself, exactly as
86
+ // EntityBase opens `entity`.
87
+ PrepareAuth({ target })
88
+
75
89
  // Generate the client class and config in core/.
76
90
  Folder({ name: 'core' }, () => {
77
91
 
@@ -0,0 +1,298 @@
1
+
2
+ import {
3
+ Content,
4
+ File,
5
+ Folder,
6
+ cmp,
7
+ isAuthSuppressed,
8
+ isHttpBasicAuth,
9
+ resolveAuthIn,
10
+ resolveAuthName,
11
+ } from '@voxgig/sdkgen'
12
+
13
+
14
+ import { javaPackage } from './utility_java'
15
+
16
+
17
+ // WHERE THE CREDENTIAL GOES IS A FACT ABOUT THE API, so it is generated
18
+ // rather than templated.
19
+ //
20
+ // This was a static file in `tm/java/utility/` that hardcoded
21
+ // `authorization` and a header. apidef has always resolved the scheme's
22
+ // `in` and `name` into `main.kit.info.security` — joplin's says
23
+ // `in: "query", name: "token"` — and generation dropped both. The result
24
+ // was an SDK that sent a header the API does not read and never sent the
25
+ // query parameter it does, so it could not authenticate at all. Four
26
+ // repos in the cedar fleet shipped that way: joplin (`token`), pipedrive
27
+ // (`api_token`), trello (`key`), lm-umbrella (`apiKey`).
28
+ //
29
+ // A template cannot fix this, because the three placements need three
30
+ // different bodies and a template has to pick one. A component emits the
31
+ // branch this API actually uses and nothing else — no dead query code in
32
+ // a bearer-token SDK, and no runtime `if` on a value that is fixed at
33
+ // generation time. The ts port (PrepareAuth_ts) is the donor.
34
+ const PrepareAuth = cmp(async function PrepareAuth(props: any) {
35
+ const { target } = props
36
+ const { model } = props.ctx$
37
+
38
+ const javapackage = javaPackage(model)
39
+
40
+ // `!isAuthSuppressed`, NOT `isAuthActive`. The latter is also false when
41
+ // the SPEC merely declares no security scheme (`main.kit.info.auth:
42
+ // false`), and those SDKs still carry a credential: optspec always
43
+ // declares `apikey` and makeOptions fills `options.auth` from its
44
+ // defaults, so the runtime guard never fired and they have always sent
45
+ // it. Only an explicit `main.kit.config.auth.active: false` means "no
46
+ // credential, ever", which is what isAuthSuppressed reads.
47
+ const active = !isAuthSuppressed(model)
48
+ const where = resolveAuthIn(model)
49
+ const name = resolveAuthName(model)
50
+ const basic = isHttpBasicAuth(model)
51
+
52
+ // FOLDER NESTING. The java target is FLAT: Main opens no `src` folder at
53
+ // all, and the blanket `Copy({ from: 'tm/java' })` lands core/, utility/,
54
+ // feature/ and test/ straight at the target root (pom.xml declares
55
+ // `sourceDirectory` as the project base and includes `utility/**`). So
56
+ // `utility` is opened HERE — the same shape EntityBase_java uses for
57
+ // `entity` — and this component must be called from Main's TOP level,
58
+ // NOT from inside its `Folder({ name: 'core' })`. Nested there it would
59
+ // write core/utility/PrepareAuth.java, which javac would compile into
60
+ // the wrong package while Register.java kept calling the stale
61
+ // utility/PrepareAuth.java.
62
+ Folder({ name: 'utility' }, () => {
63
+ File({ name: 'PrepareAuth.' + target.ext }, () => {
64
+ Content(render({ javapackage, active, where, name, basic }))
65
+ })
66
+ })
67
+ })
68
+
69
+
70
+ function render(spec: {
71
+ javapackage: string, active: boolean, where: string, name: string,
72
+ basic: boolean
73
+ }): string {
74
+ const jp = spec.javapackage
75
+
76
+ // NO AUTH AT ALL. A public API's SDK gets a prepareAuth that is honest
77
+ // about it rather than one that deletes a header nobody set.
78
+ if (!spec.active) {
79
+ return `package ${jp}.utility;
80
+
81
+ import ${jp}.core.Context;
82
+ import ${jp}.core.Spec;
83
+
84
+ // This API declares no authentication, so there is no credential to place.
85
+ // The class stays in the pipeline because Register wires prepareAuth
86
+ // unconditionally and MakeSpec calls it on every request.
87
+ final class PrepareAuth {
88
+
89
+ private PrepareAuth() {}
90
+
91
+ static Spec prepareAuth(Context ctx) {
92
+ Spec spec = ctx.spec;
93
+ if (spec == null) {
94
+ throw ctx.makeError("auth_no_spec",
95
+ "Expected context spec property to be defined.");
96
+ }
97
+
98
+ return spec;
99
+ }
100
+ }
101
+ `
102
+ }
103
+
104
+ const header = 'header' === spec.where
105
+ const query = 'query' === spec.where
106
+ const cookie = 'cookie' === spec.where
107
+
108
+ // HTTP Basic is header-only by definition: the scheme is
109
+ // `Authorization: Basic base64(user:pass)`. It cannot be expressed as a
110
+ // query parameter or a cookie, so the branch is emitted only where it
111
+ // can mean something.
112
+ const basicBranch = spec.basic && header
113
+
114
+ // The bag the credential lands in, per placement. Cookies ride the
115
+ // header bag because a cookie IS a header.
116
+ const bag = query ? 'query' : 'headers'
117
+
118
+ const imports = [
119
+ ...(basicBranch ? [
120
+ 'import java.nio.charset.StandardCharsets;',
121
+ 'import java.util.Base64;',
122
+ ] : []),
123
+ // `List.of("auth", "prefix")` is the only use of java.util.List, and a
124
+ // prefix is a HEADER convention — the query and cookie branches drop it,
125
+ // so neither needs the import.
126
+ ...(header ? ['import java.util.List;'] : []),
127
+ 'import java.util.Map;',
128
+ ].join('\n')
129
+
130
+ const consts = [
131
+ ` static final String CRED_NAME = "${javastr(credName(spec.where, spec.name))}";`,
132
+ ...(cookie ? [' static final String COOKIE_HEADER = "cookie";'] : []),
133
+ ' static final String OPTION_APIKEY = "apikey";',
134
+ ...(basicBranch ? [' static final String OPTION_SECRET = "secret";'] : []),
135
+ ' static final String NOT_FOUND = "__NOTFOUND__";',
136
+ ].join('\n')
137
+
138
+ const basicBlock = basicBranch ? `
139
+ // True HTTP Basic Auth needs TWO credentials, base64-joined - a single
140
+ // token in the header (the branch below) can never authenticate against
141
+ // an API that actually checks \`Authorization: Basic base64(user:pass)\`.
142
+ if (Boolean.TRUE.equals(Struct.getpath(options, List.of("auth", "basic")))) {
143
+ Object secret = Struct.getprop(options, OPTION_SECRET, NOT_FOUND);
144
+ boolean noApikey = !(apikey instanceof String)
145
+ || NOT_FOUND.equals(apikey) || "".equals(apikey);
146
+ boolean noSecret = !(secret instanceof String)
147
+ || NOT_FOUND.equals(secret) || "".equals(secret);
148
+
149
+ if (noApikey || noSecret) {
150
+ headers.remove(CRED_NAME);
151
+ }
152
+ else {
153
+ String basicPrefix = "";
154
+ Object bp = Struct.getpath(options, List.of("auth", "prefix"));
155
+ if (bp instanceof String) {
156
+ basicPrefix = (String) bp;
157
+ }
158
+ String b64 = Base64.getEncoder().encodeToString(
159
+ ((String) apikey + ":" + (String) secret).getBytes(StandardCharsets.UTF_8));
160
+ if ("".equals(basicPrefix)) {
161
+ headers.put(CRED_NAME, b64);
162
+ }
163
+ else {
164
+ headers.put(CRED_NAME, basicPrefix + " " + b64);
165
+ }
166
+ }
167
+
168
+ return spec;
169
+ }
170
+ ` : ''
171
+
172
+ return `package ${jp}.utility;
173
+
174
+ ${imports}
175
+
176
+ import ${jp}.core.Context;
177
+ import ${jp}.core.Spec;
178
+ import ${jp}.utility.struct.Struct;
179
+
180
+ final class PrepareAuth {
181
+
182
+ private PrepareAuth() {}
183
+
184
+ ${consts}
185
+
186
+ static Spec prepareAuth(Context ctx) {
187
+ Spec spec = ctx.spec;
188
+ if (spec == null) {
189
+ throw ctx.makeError("auth_no_spec",
190
+ "Expected context spec property to be defined.");
191
+ }
192
+
193
+ Map<String, Object> ${bag} = spec.${bag};
194
+ Map<String, Object> options = ctx.client.optionsMap();
195
+
196
+ // Public APIs that need no auth omit the options.auth block entirely.
197
+ if (options.get("auth") == null) {
198
+ ${clear(spec.where)}
199
+ return spec;
200
+ }
201
+
202
+ Object apikey = Struct.getprop(options, OPTION_APIKEY, NOT_FOUND);
203
+ ${basicBlock}
204
+ boolean skip = false;
205
+ if (apikey == null) {
206
+ skip = true;
207
+ }
208
+ else if (apikey instanceof String
209
+ && (NOT_FOUND.equals(apikey) || "".equals(apikey))) {
210
+ skip = true;
211
+ }
212
+
213
+ if (skip) {
214
+ ${clear(spec.where)}
215
+ }
216
+ else {
217
+ ${place(spec.where)}
218
+ }
219
+
220
+ return spec;
221
+ }
222
+ }
223
+ `
224
+ }
225
+
226
+
227
+ // THE CREDENTIAL NAME AS THIS TARGET SPELLS IT.
228
+ //
229
+ // java's header map is a plain case-sensitive LinkedHashMap and the whole
230
+ // generated SDK — Fetcher, SecretsFeature, and the SDK's own PipelineTest —
231
+ // spells the default header `authorization`, exactly as the deleted
232
+ // tm/java/utility/PrepareAuth.java did. Keep that spelling when the model
233
+ // resolves the DEFAULT, so a header-based java SDK behaves byte-for-byte as
234
+ // it did before; a spec that names a header explicitly gets that name
235
+ // verbatim, as do query parameters and cookies.
236
+ // EVERY header name, not just the default. java's map is a case-sensitive
237
+ // LinkedHashMap and the generated SDK spells headers lower-case throughout
238
+ // — Fetcher, SecretsFeature and PipelineTest all do. Special-casing only
239
+ // the literal 'Authorization' left a spec-named header like `X-API-Key`
240
+ // capitalised here while the other nineteen targets lower-cased it, so the
241
+ // same model produced a header java could not find and the rest could.
242
+ // Query parameters and cookies stay verbatim: those ARE case-sensitive.
243
+ function credName(where: string, name: string): string {
244
+ return 'header' === where ? name.toLowerCase() : name
245
+ }
246
+
247
+
248
+ function clear(where: string): string {
249
+ return 'query' === where ? 'query.remove(CRED_NAME);' : 'headers.remove(CRED_NAME);'
250
+ }
251
+
252
+
253
+ function place(where: string): string {
254
+ if ('query' === where) {
255
+ // NO PREFIX IN A QUERY STRING. `?token=Bearer%20abc` is not a thing any
256
+ // API reads; the prefix is a header convention and is dropped here
257
+ // deliberately rather than silently concatenated.
258
+ return ` String apikeyVal = apikey instanceof String ? (String) apikey : "";
259
+ // NO PREFIX IN A QUERY STRING: ?name=Bearer%20abc is not a thing any
260
+ // API reads, so the auth.prefix option is dropped here deliberately.
261
+ query.put(CRED_NAME, apikeyVal);`
262
+ }
263
+
264
+ if ('cookie' === where) {
265
+ return ` String apikeyVal = apikey instanceof String ? (String) apikey : "";
266
+ // A cookie IS a header, but the request may already carry others, so
267
+ // the pair is APPENDED rather than replacing the whole cookie header.
268
+ // No prefix, for the same reason a query parameter carries none.
269
+ Object existing = Struct.getprop(headers, COOKIE_HEADER, "");
270
+ String cookie = existing instanceof String ? (String) existing : "";
271
+ String pair = CRED_NAME + "=" + apikeyVal;
272
+ headers.put(COOKIE_HEADER, "".equals(cookie) ? pair : cookie + "; " + pair);`
273
+ }
274
+
275
+ return ` String authPrefix = "";
276
+ Object ap = Struct.getpath(options, List.of("auth", "prefix"));
277
+ if (ap instanceof String) {
278
+ authPrefix = (String) ap;
279
+ }
280
+ String apikeyVal = apikey instanceof String ? (String) apikey : "";
281
+ // Empty prefix (raw apiKey credential) must not add a leading space.
282
+ if ("".equals(authPrefix)) {
283
+ headers.put(CRED_NAME, apikeyVal);
284
+ }
285
+ else {
286
+ headers.put(CRED_NAME, authPrefix + " " + apikeyVal);
287
+ }`
288
+ }
289
+
290
+
291
+ function javastr(s: string): string {
292
+ return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')
293
+ }
294
+
295
+
296
+ export {
297
+ PrepareAuth
298
+ }