@nestia/core 13.0.0 → 13.0.2

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.
@@ -58,7 +58,7 @@ function EncryptedBody(validator) {
58
58
  ? param({ headers: headers.get(), body, direction: "decode" })
59
59
  : param;
60
60
  // PARSE AND VALIDATE DATA
61
- const data = JSON.parse(decrypt(body, password.key, password.iv));
61
+ const data = decode(body, password.key, password.iv);
62
62
  const error = checker(data);
63
63
  if (error !== null)
64
64
  throw error;
@@ -66,10 +66,28 @@ function EncryptedBody(validator) {
66
66
  });
67
67
  })();
68
68
  }
69
- /** @internal */
70
- const decrypt = (body, key, iv) => {
69
+ /**
70
+ * Decrypt and JSON-parse the request body behind a single failure response.
71
+ *
72
+ * `AesPkcs5` is AES-CBC without an authentication tag, so a MITM attacker can
73
+ * forge ciphertexts. Splitting the two failure modes of a forged body — a bad
74
+ * PKCS#5 padding (decryption throws) versus valid padding whose plaintext is
75
+ * not JSON (`JSON.parse` throws) — into two different HTTP status codes turns
76
+ * this decorator into a padding oracle: one observable bit per request lets the
77
+ * attacker recover the plaintext byte by byte without the key
78
+ * (GHSA-pqj4-gvf7-6fq5).
79
+ *
80
+ * Both failure modes must therefore collapse into one indistinguishable
81
+ * `BadRequestException` (identical status, message, and body). Only the
82
+ * downstream type validation of an already-parsed body may report its own
83
+ * distinct error, because reaching it requires plaintext the attacker cannot
84
+ * forge without already knowing it.
85
+ *
86
+ * @internal
87
+ */
88
+ const decode = (body, key, iv) => {
71
89
  try {
72
- return AesPkcs5_1.AesPkcs5.decrypt(body, key, iv);
90
+ return JSON.parse(AesPkcs5_1.AesPkcs5.decrypt(body, key, iv));
73
91
  }
74
92
  catch (exp) {
75
93
  if (exp instanceof Error)
@@ -1 +1 @@
1
- {"version":3,"file":"EncryptedBody.js","sourceRoot":"","sources":["../../src/decorators/EncryptedBody.ts"],"names":[],"mappings":";;;;;;;;;;;;AACA,2DAAwD;AACxD,2CAIwB;AAKxB,kDAA+C;AAC/C,oEAAuE;AACvE,4DAAyD;AACzD,oEAAiE;AACjE,4DAAyD;AACzD,4EAAyE;AAEzE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,uBACE,SAAoC;IAEpC,MAAM,OAAO,GAAG,IAAA,6CAAqB,EAAC,eAAe,CAAC,CAAC,SAAS,CAAC,CAAC;IAClE,OAAO,IAAA,6BAAoB,EAAC,SAAe,aAAa,CACtD,QAAa,EACb,OAAyB;;YAEzB,MAAM,OAAO,GAAqC,OAAO;iBACtD,YAAY,EAAE;iBACd,UAAU,EAAE,CAAC;YAChB,IAAI,IAAA,6BAAa,EAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,KAAK;gBACxE,MAAM,IAAI,4BAAmB,CAAC,wCAAwC,CAAC,CAAC;YAE1E,MAAM,KAAK,GACT,OAAO,CAAC,WAAW,CAAC,2CAAuB,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACnE,IAAI,CAAC,KAAK;gBACR,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;YAEJ,gBAAgB;YAChB,MAAM,OAAO,GAAsC,IAAI,qBAAS,CAAC,GAAG,EAAE,CACpE,IAAA,qCAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CACnC,CAAC;YACF,MAAM,IAAI,GAAW,MAAM,IAAA,6BAAa,EAAC,OAAO,CAAC,CAAC;YAClD,MAAM,QAAQ,GACZ,OAAO,KAAK,KAAK,UAAU;gBACzB,CAAC,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;gBAC9D,CAAC,CAAC,KAAK,CAAC;YAEZ,0BAA0B;YAC1B,MAAM,IAAI,GAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;YACvE,MAAM,KAAK,GAAiB,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,IAAI;gBAAE,MAAM,KAAK,CAAC;YAChC,OAAO,IAAI,CAAC;QACd,CAAC;KAAA,CAAC,EAAE,CAAC;AACP,CAAC;AAED,gBAAgB;AAChB,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,GAAW,EAAE,EAAU,EAAU,EAAE;IAChE,IAAI,CAAC;QACH,OAAO,mBAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK;YACtB,MAAM,IAAI,4BAAmB,CAC3B,qFAAqF,CACtF,CAAC;;YACC,MAAM,GAAG,CAAC;IACjB,CAAC;AACH,CAAC,CAAC"}
1
+ {"version":3,"file":"EncryptedBody.js","sourceRoot":"","sources":["../../src/decorators/EncryptedBody.ts"],"names":[],"mappings":";;;;;;;;;;;;AACA,2DAAwD;AACxD,2CAIwB;AAKxB,kDAA+C;AAC/C,oEAAuE;AACvE,4DAAyD;AACzD,oEAAiE;AACjE,4DAAyD;AACzD,4EAAyE;AAEzE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,uBACE,SAAoC;IAEpC,MAAM,OAAO,GAAG,IAAA,6CAAqB,EAAC,eAAe,CAAC,CAAC,SAAS,CAAC,CAAC;IAClE,OAAO,IAAA,6BAAoB,EAAC,SAAe,aAAa,CACtD,QAAa,EACb,OAAyB;;YAEzB,MAAM,OAAO,GAAqC,OAAO;iBACtD,YAAY,EAAE;iBACd,UAAU,EAAE,CAAC;YAChB,IAAI,IAAA,6BAAa,EAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,KAAK;gBACxE,MAAM,IAAI,4BAAmB,CAAC,wCAAwC,CAAC,CAAC;YAE1E,MAAM,KAAK,GACT,OAAO,CAAC,WAAW,CAAC,2CAAuB,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACnE,IAAI,CAAC,KAAK;gBACR,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;YAEJ,gBAAgB;YAChB,MAAM,OAAO,GAAsC,IAAI,qBAAS,CAAC,GAAG,EAAE,CACpE,IAAA,qCAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CACnC,CAAC;YACF,MAAM,IAAI,GAAW,MAAM,IAAA,6BAAa,EAAC,OAAO,CAAC,CAAC;YAClD,MAAM,QAAQ,GACZ,OAAO,KAAK,KAAK,UAAU;gBACzB,CAAC,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;gBAC9D,CAAC,CAAC,KAAK,CAAC;YAEZ,0BAA0B;YAC1B,MAAM,IAAI,GAAQ,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;YAC1D,MAAM,KAAK,GAAiB,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,IAAI;gBAAE,MAAM,KAAK,CAAC;YAChC,OAAO,IAAI,CAAC;QACd,CAAC;KAAA,CAAC,EAAE,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,GAAW,EAAE,EAAU,EAAW,EAAE;IAChE,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,mBAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK;YACtB,MAAM,IAAI,4BAAmB,CAC3B,qFAAqF,CACtF,CAAC;;YACC,MAAM,GAAG,CAAC;IACjB,CAAC;AACH,CAAC,CAAC"}
@@ -8,6 +8,7 @@ import (
8
8
  "io"
9
9
  "os"
10
10
  "path/filepath"
11
+ "sync"
11
12
  "time"
12
13
 
13
14
  shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
@@ -68,23 +69,54 @@ func runBuild(args []string) int {
68
69
  return 2
69
70
  }
70
71
  if len(diags) > 0 {
72
+ if prog != nil {
73
+ prog.Close()
74
+ }
71
75
  driver.WritePrettyDiagnostics(stderr, diags, cwd)
72
76
  return 2
73
77
  }
74
- defer prog.Close()
75
- releaseTypiaRegistries := registerTypiaDefaultLibraryClassifier(prog)
76
- defer releaseTypiaRegistries()
77
78
  if profile {
78
79
  started = time.Now()
79
80
  }
80
81
  if diags := prog.Diagnostics(); len(diags) > 0 {
81
82
  profileBuildStep(profile, "diagnostics", started)
83
+ prog.Close()
82
84
  driver.WritePrettyDiagnostics(stderr, diags, cwd)
83
85
  return 2
84
86
  }
85
87
  profileBuildStep(profile, "diagnostics", started)
86
88
 
87
89
  shouldEmit := !prog.ParsedConfig.ParsedConfig.CompilerOptions.NoEmit.IsTrue()
90
+ if !shouldEmit {
91
+ // Plugin transformers are part of tsgo's emit pipeline. Reload with emit
92
+ // enabled so check/noEmit traverses the same source set, then discard every
93
+ // generated output below. The original program already proved the project's
94
+ // diagnostics; diagnosing this private reload would reject valid analysis-
95
+ // only options such as allowImportingTsExtensions.
96
+ prog.Close()
97
+ if profile {
98
+ started = time.Now()
99
+ }
100
+ prog, diags, err = driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
101
+ ForceEmit: true,
102
+ OutDir: *outDir,
103
+ })
104
+ profileBuildStep(profile, "load-transform-program", started)
105
+ if err != nil {
106
+ fmt.Fprintf(stderr, "ttsc-nestia build: %v\n", err)
107
+ return 2
108
+ }
109
+ if len(diags) > 0 {
110
+ if prog != nil {
111
+ prog.Close()
112
+ }
113
+ driver.WritePrettyDiagnostics(stderr, diags, cwd)
114
+ return 2
115
+ }
116
+ }
117
+ defer prog.Close()
118
+ releaseTypiaRegistries := registerTypiaDefaultLibraryClassifier(prog)
119
+ defer releaseTypiaRegistries()
88
120
  if !*quiet {
89
121
  fmt.Fprintf(
90
122
  stdout,
@@ -97,9 +129,6 @@ func runBuild(args []string) int {
97
129
  shouldEmit,
98
130
  )
99
131
  }
100
- if !shouldEmit {
101
- return 0
102
- }
103
132
 
104
133
  // AST-integration emit: typia's per-file transformer and @nestia/core's own
105
134
  // per-file transformer both run inside tsgo's emit pipeline (sharing the
@@ -132,10 +161,20 @@ func runBuild(args []string) int {
132
161
  transforms := append([]driver.PluginTransform{typiaTransform, coreTransform}, contributorTransforms...)
133
162
 
134
163
  emitted := []string{}
164
+ pending := []buildPendingOutput{}
165
+ var writeMu sync.Mutex
135
166
  writeFile := shimcompiler.WriteFile(func(fileName, text string, data *shimcompiler.WriteFileData) error {
136
167
  _ = data
137
- emitted = append(emitted, fileName)
138
- return driver.DefaultWriteFile(fileName, text)
168
+ if shouldEmit {
169
+ // TypeScript-Go emits declarations in parallel. Serialize the shared
170
+ // slices so buffering cannot lose an artifact or race while the
171
+ // declaration workers call this callback concurrently.
172
+ writeMu.Lock()
173
+ defer writeMu.Unlock()
174
+ emitted = append(emitted, fileName)
175
+ pending = append(pending, buildPendingOutput{FileName: fileName, Text: text})
176
+ }
177
+ return nil
139
178
  })
140
179
 
141
180
  // Declaration emit: ttsc delegates the whole emit of a transform-plugin
@@ -145,7 +184,7 @@ func runBuild(args []string) int {
145
184
  // core runtime transforms never change the public type surface, so the
146
185
  // declarations are taken from the pristine program — done before the JS
147
186
  // transform runs so it reads the un-mutated AST.
148
- if prog.ParsedConfig.ParsedConfig.CompilerOptions.Declaration.IsTrue() {
187
+ if shouldEmit && prog.ParsedConfig.ParsedConfig.CompilerOptions.Declaration.IsTrue() {
149
188
  if profile {
150
189
  started = time.Now()
151
190
  }
@@ -162,10 +201,6 @@ func runBuild(args []string) int {
162
201
  fmt.Fprintf(stderr, "ttsc-nestia build: emit failed: %v\n", err)
163
202
  return 3
164
203
  }
165
- if len(transformDiags) > 0 {
166
- WriteTypiaTransformDiagnostics(stderr, transformDiags, cwd)
167
- return 3
168
- }
169
204
  emitHasError := false
170
205
  for _, d := range eDiags {
171
206
  fmt.Fprintln(stderr, " -", d.String())
@@ -173,31 +208,48 @@ func runBuild(args []string) int {
173
208
  emitHasError = true
174
209
  }
175
210
  }
211
+ if len(transformDiags) > 0 {
212
+ WriteTypiaTransformDiagnostics(stderr, transformDiags, cwd)
213
+ return 3
214
+ }
176
215
  if emitHasError {
177
216
  return 3
178
217
  }
179
- if *manifestPath != "" {
180
- data, err := json.Marshal(emitted)
181
- if err != nil {
182
- fmt.Fprintf(stderr, "ttsc-nestia build: manifest marshal failed: %v\n", err)
183
- return 3
218
+ if shouldEmit {
219
+ for _, output := range pending {
220
+ if err := driver.DefaultWriteFile(output.FileName, output.Text); err != nil {
221
+ fmt.Fprintf(stderr, "ttsc-nestia build: emit write failed: %v\n", err)
222
+ return 3
223
+ }
184
224
  }
185
- if err := os.MkdirAll(filepath.Dir(*manifestPath), 0o755); err != nil {
186
- fmt.Fprintf(stderr, "ttsc-nestia build: manifest mkdir failed: %v\n", err)
187
- return 3
225
+ if *manifestPath != "" {
226
+ data, err := json.Marshal(emitted)
227
+ if err != nil {
228
+ fmt.Fprintf(stderr, "ttsc-nestia build: manifest marshal failed: %v\n", err)
229
+ return 3
230
+ }
231
+ if err := os.MkdirAll(filepath.Dir(*manifestPath), 0o755); err != nil {
232
+ fmt.Fprintf(stderr, "ttsc-nestia build: manifest mkdir failed: %v\n", err)
233
+ return 3
234
+ }
235
+ if err := os.WriteFile(*manifestPath, data, 0o644); err != nil {
236
+ fmt.Fprintf(stderr, "ttsc-nestia build: manifest write failed: %v\n", err)
237
+ return 3
238
+ }
188
239
  }
189
- if err := os.WriteFile(*manifestPath, data, 0o644); err != nil {
190
- fmt.Fprintf(stderr, "ttsc-nestia build: manifest write failed: %v\n", err)
191
- return 3
240
+ if !*quiet {
241
+ fmt.Fprintf(stdout, "// ttsc-nestia build: emitted=%d files\n", len(emitted))
192
242
  }
193
243
  }
194
- if !*quiet {
195
- fmt.Fprintf(stdout, "// ttsc-nestia build: emitted=%d files\n", len(emitted))
196
- }
197
244
  profileBuildStep(profile, "total", totalStarted)
198
245
  return 0
199
246
  }
200
247
 
248
+ type buildPendingOutput struct {
249
+ FileName string
250
+ Text string
251
+ }
252
+
201
253
  // emitDeclarations runs tsgo's standard declaration emitter for the program.
202
254
  // tsgo's MarkLinkedReferences pass can nil-panic on some cross-module reference
203
255
  // shapes (e.g. a nestia.config.ts that calls NestFactory.create, compiled by the
@@ -220,39 +272,7 @@ func emitDeclarations(prog *driver.Program, writeFile shimcompiler.WriteFile) {
220
272
  }
221
273
 
222
274
  func runCheck(args []string) int {
223
- fs := flag.NewFlagSet("check", flag.ContinueOnError)
224
- fs.SetOutput(stderr)
225
- tsconfigPath := fs.String("tsconfig", "tsconfig.json", "path to tsconfig.json")
226
- cwdOverride := fs.String("cwd", "", "override the working directory")
227
- pluginsJSON := fs.String("plugins-json", "", "ordered ttsc plugin payload")
228
- if err := fs.Parse(args); err != nil {
229
- return 2
230
- }
231
- if _, err := plugin.ParsePlan(*pluginsJSON); err != nil {
232
- fmt.Fprintf(stderr, "ttsc-nestia check: %v\n", err)
233
- return 2
234
- }
235
- cwd, ok := resolveCWD("ttsc-nestia check", *cwdOverride)
236
- if !ok {
237
- return 2
238
- }
239
- prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
240
- ForceNoEmit: true,
241
- })
242
- if err != nil {
243
- fmt.Fprintf(stderr, "ttsc-nestia check: %v\n", err)
244
- return 2
245
- }
246
- if len(diags) > 0 {
247
- driver.WritePrettyDiagnostics(stderr, diags, cwd)
248
- return 2
249
- }
250
- defer prog.Close()
251
- if diags := prog.Diagnostics(); len(diags) > 0 {
252
- driver.WritePrettyDiagnostics(stderr, diags, cwd)
253
- return 2
254
- }
255
- return 0
275
+ return runBuild(append([]string{"--noEmit"}, args...))
256
276
  }
257
277
 
258
278
  type Diagnostic struct {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestia/core",
3
- "version": "13.0.0",
3
+ "version": "13.0.2",
4
4
  "description": "Super-fast validation decorators of NestJS",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "homepage": "https://nestia.io",
47
47
  "dependencies": {
48
- "@typia/interface": "^14.0.1",
49
- "@typia/utils": "^14.0.1",
48
+ "@typia/interface": "^14.0.2",
49
+ "@typia/utils": "^14.0.2",
50
50
  "get-function-location": "^2.0.0",
51
51
  "glob": "^11.0.3",
52
52
  "path-parser": "^6.1.0",
@@ -54,9 +54,9 @@
54
54
  "reflect-metadata": ">=0.1.12",
55
55
  "rxjs": ">=6.0.3",
56
56
  "tgrid": "^1.1.0",
57
- "typia": "^14.0.1",
57
+ "typia": "^14.0.2",
58
58
  "ws": "^7.5.3",
59
- "@nestia/fetcher": "^13.0.0"
59
+ "@nestia/fetcher": "^13.0.2"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@modelcontextprotocol/sdk": "^1.18.0",
@@ -65,8 +65,8 @@
65
65
  "reflect-metadata": ">=0.1.12",
66
66
  "rxjs": ">=6.0.3",
67
67
  "ttsc": ">=0.19.2",
68
- "typia": "^14.0.1",
69
- "@nestia/fetcher": "^13.0.0"
68
+ "typia": "^14.0.2",
69
+ "@nestia/fetcher": "^13.0.2"
70
70
  },
71
71
  "peerDependenciesMeta": {
72
72
  "@modelcontextprotocol/sdk": {
@@ -69,17 +69,35 @@ export function EncryptedBody<T>(
69
69
  : param;
70
70
 
71
71
  // PARSE AND VALIDATE DATA
72
- const data: any = JSON.parse(decrypt(body, password.key, password.iv));
72
+ const data: any = decode(body, password.key, password.iv);
73
73
  const error: Error | null = checker(data);
74
74
  if (error !== null) throw error;
75
75
  return data;
76
76
  })();
77
77
  }
78
78
 
79
- /** @internal */
80
- const decrypt = (body: string, key: string, iv: string): string => {
79
+ /**
80
+ * Decrypt and JSON-parse the request body behind a single failure response.
81
+ *
82
+ * `AesPkcs5` is AES-CBC without an authentication tag, so a MITM attacker can
83
+ * forge ciphertexts. Splitting the two failure modes of a forged body — a bad
84
+ * PKCS#5 padding (decryption throws) versus valid padding whose plaintext is
85
+ * not JSON (`JSON.parse` throws) — into two different HTTP status codes turns
86
+ * this decorator into a padding oracle: one observable bit per request lets the
87
+ * attacker recover the plaintext byte by byte without the key
88
+ * (GHSA-pqj4-gvf7-6fq5).
89
+ *
90
+ * Both failure modes must therefore collapse into one indistinguishable
91
+ * `BadRequestException` (identical status, message, and body). Only the
92
+ * downstream type validation of an already-parsed body may report its own
93
+ * distinct error, because reaching it requires plaintext the attacker cannot
94
+ * forge without already knowing it.
95
+ *
96
+ * @internal
97
+ */
98
+ const decode = (body: string, key: string, iv: string): unknown => {
81
99
  try {
82
- return AesPkcs5.decrypt(body, key, iv);
100
+ return JSON.parse(AesPkcs5.decrypt(body, key, iv));
83
101
  } catch (exp) {
84
102
  if (exp instanceof Error)
85
103
  throw new BadRequestException(