@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.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/cmp/AgentGuideContent.js +1 -1
- package/dist/sdkgen.d.ts +2 -2
- package/dist/sdkgen.js +6 -3
- package/dist/sdkgen.js.map +1 -1
- package/dist/testkit.js +2 -2
- package/dist/testkit.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utility.d.ts +4 -1
- package/dist/utility.js +60 -0
- package/dist/utility.js.map +1 -1
- package/model/sdkgen.aon +10 -0
- package/package.json +1 -1
- package/project/.sdk/model/feature/feature-index.aon +20 -20
- package/project/.sdk/src/cmp/c/Config_c.ts +15 -0
- package/project/.sdk/src/cmp/c/Main_c.ts +15 -0
- package/project/.sdk/src/cmp/c/PrepareAuth_c.ts +371 -0
- package/project/.sdk/src/cmp/clojure/Config_clojure.ts +47 -1
- package/project/.sdk/src/cmp/clojure/Main_clojure.ts +11 -1
- package/project/.sdk/src/cmp/clojure/PrepareAuth_clojure.ts +374 -0
- package/project/.sdk/src/cmp/cpp/Config_cpp.ts +40 -0
- package/project/.sdk/src/cmp/cpp/Main_cpp.ts +17 -0
- package/project/.sdk/src/cmp/cpp/PrepareAuth_cpp.ts +441 -0
- package/project/.sdk/src/cmp/csharp/Config_csharp.ts +42 -1
- package/project/.sdk/src/cmp/csharp/Main_csharp.ts +18 -0
- package/project/.sdk/src/cmp/csharp/PrepareAuth_csharp.ts +392 -0
- package/project/.sdk/src/cmp/elixir/Config_elixir.ts +36 -1
- package/project/.sdk/src/cmp/elixir/Main_elixir.ts +15 -0
- package/project/.sdk/src/cmp/elixir/PrepareAuth_elixir.ts +418 -0
- package/project/.sdk/src/cmp/go/Config_go.ts +12 -1
- package/project/.sdk/src/cmp/go/Main_go.ts +14 -0
- package/project/.sdk/src/cmp/go/PrepareAuth_go.ts +322 -0
- package/project/.sdk/src/cmp/java/Config_java.ts +20 -1
- package/project/.sdk/src/cmp/java/Main_java.ts +14 -0
- package/project/.sdk/src/cmp/java/PrepareAuth_java.ts +298 -0
- package/project/.sdk/src/cmp/js/Config_js.ts +12 -1
- package/project/.sdk/src/cmp/js/Main_js.ts +6 -0
- package/project/.sdk/src/cmp/js/PrepareAuth_js.ts +244 -0
- package/project/.sdk/src/cmp/kotlin/Config_kotlin.ts +23 -1
- package/project/.sdk/src/cmp/kotlin/Main_kotlin.ts +20 -0
- package/project/.sdk/src/cmp/kotlin/PrepareAuth_kotlin.ts +440 -0
- package/project/.sdk/src/cmp/lua/Config_lua.ts +15 -1
- package/project/.sdk/src/cmp/lua/Main_lua.ts +11 -0
- package/project/.sdk/src/cmp/lua/PrepareAuth_lua.ts +364 -0
- package/project/.sdk/src/cmp/ocaml/Config_ocaml.ts +31 -1
- package/project/.sdk/src/cmp/ocaml/Main_ocaml.ts +19 -0
- package/project/.sdk/src/cmp/ocaml/PrepareAuth_ocaml.ts +454 -0
- package/project/.sdk/src/cmp/perl/Config_perl.ts +30 -0
- package/project/.sdk/src/cmp/perl/Main_perl.ts +14 -0
- package/project/.sdk/src/cmp/perl/PrepareAuth_perl.ts +387 -0
- package/project/.sdk/src/cmp/php/Config_php.ts +12 -1
- package/project/.sdk/src/cmp/php/Main_php.ts +7 -0
- package/project/.sdk/src/cmp/php/PrepareAuth_php.ts +325 -0
- package/project/.sdk/src/cmp/py/Config_py.ts +12 -1
- package/project/.sdk/src/cmp/py/Main_py.ts +12 -0
- package/project/.sdk/src/cmp/py/PrepareAuth_py.ts +389 -0
- package/project/.sdk/src/cmp/rb/Config_rb.ts +18 -1
- package/project/.sdk/src/cmp/rb/Main_rb.ts +10 -0
- package/project/.sdk/src/cmp/rb/PrepareAuth_rb.ts +305 -0
- package/project/.sdk/src/cmp/rust/Config_rust.ts +41 -1
- package/project/.sdk/src/cmp/rust/Main_rust.ts +16 -0
- package/project/.sdk/src/cmp/rust/PrepareAuth_rust.ts +410 -0
- package/project/.sdk/src/cmp/scala/Config_scala.ts +21 -1
- package/project/.sdk/src/cmp/scala/Main_scala.ts +14 -0
- package/project/.sdk/src/cmp/scala/PrepareAuth_scala.ts +415 -0
- package/project/.sdk/src/cmp/swift/Config_swift.ts +37 -1
- package/project/.sdk/src/cmp/swift/Main_swift.ts +10 -0
- package/project/.sdk/src/cmp/swift/PrepareAuth_swift.ts +439 -0
- package/project/.sdk/src/cmp/ts/Config_ts.ts +12 -1
- package/project/.sdk/src/cmp/ts/Main_ts.ts +5 -0
- package/project/.sdk/src/cmp/ts/PrepareAuth_ts.ts +237 -0
- package/project/.sdk/src/cmp/zig/Config_zig.ts +33 -1
- package/project/.sdk/src/cmp/zig/Main_zig.ts +16 -0
- package/project/.sdk/src/cmp/zig/PrepareAuth_zig.ts +545 -0
- package/project/.sdk/tm/clojure/src/sdk/core.clj +31 -20
- package/project/.sdk/tm/cpp/utility/pipeline.hpp +36 -40
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +11 -0
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +19 -32
- package/project/.sdk/tm/java/utility/MakeOptions.java +13 -1
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +14 -1
- package/project/.sdk/tm/kotlin/utility/Prepare.kt +13 -46
- package/project/.sdk/tm/ocaml/Makefile +11 -1
- package/project/.sdk/tm/ocaml/sdk_helpers.ml +15 -0
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +31 -25
- package/project/.sdk/tm/ocaml/test/feature/secrets/t_secrets.ml +125 -12
- package/project/.sdk/tm/ocaml/test/t_pipeline.ml +64 -6
- package/project/.sdk/tm/perl/utility/make_options.pm +10 -1
- package/project/.sdk/tm/php/utility/MakeOptions.php +8 -1
- package/project/.sdk/tm/rust/utility/make_options.rs +13 -1
- package/project/.sdk/tm/scala/utility/Make.scala +14 -1
- package/project/.sdk/tm/scala/utility/Prepare.scala +16 -40
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +30 -3
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/Prepare.swift +21 -39
- package/project/.sdk/tm/zig/core/utility.zig +37 -45
- package/project/.sdk/tm/zig/test/pipeline_test.zig +34 -0
- package/project/sdkgen-package.json +1 -1
- package/src/cmp/AgentGuideContent.ts +1 -1
- package/src/sdkgen.ts +4 -1
- package/src/testkit.ts +2 -2
- package/src/utility.ts +75 -0
- package/project/.sdk/tm/c/utility/prepare_auth.c +0 -65
- package/project/.sdk/tm/csharp/utility/PrepareAuth.cs +0 -54
- package/project/.sdk/tm/go/utility/prepare_auth.go +0 -59
- package/project/.sdk/tm/java/utility/PrepareAuth.java +0 -66
- package/project/.sdk/tm/js/src/utility/PrepareAuthUtility.js +0 -49
- package/project/.sdk/tm/lua/utility/prepare_auth.lua +0 -52
- package/project/.sdk/tm/perl/utility/prepare_auth.pm +0 -56
- package/project/.sdk/tm/php/utility/PrepareAuth.php +0 -45
- package/project/.sdk/tm/py/pkg/utility/prepare_auth.py +0 -46
- package/project/.sdk/tm/rb/utility/prepare_auth.rb +0 -35
- package/project/.sdk/tm/rust/utility/prepare_auth.rs +0 -65
- 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
|
-
|
|
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
|
+
}
|