velocious 1.0.467 → 1.0.469

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 (27) hide show
  1. package/README.md +5 -3
  2. package/build/configuration-types.js +9 -1
  3. package/build/configuration.js +6 -1
  4. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +168 -36
  5. package/build/error-reporting/request-details.js +308 -0
  6. package/build/frontend-model-controller.js +50 -2
  7. package/build/src/configuration-types.d.ts +23 -1
  8. package/build/src/configuration-types.d.ts.map +1 -1
  9. package/build/src/configuration-types.js +9 -2
  10. package/build/src/configuration.d.ts.map +1 -1
  11. package/build/src/configuration.js +7 -2
  12. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +91 -9
  13. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  14. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +149 -36
  15. package/build/src/error-reporting/request-details.d.ts +7 -0
  16. package/build/src/error-reporting/request-details.d.ts.map +1 -0
  17. package/build/src/error-reporting/request-details.js +278 -0
  18. package/build/src/frontend-model-controller.d.ts +16 -0
  19. package/build/src/frontend-model-controller.d.ts.map +1 -1
  20. package/build/src/frontend-model-controller.js +39 -3
  21. package/build/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +2 -2
  23. package/src/configuration-types.js +9 -1
  24. package/src/configuration.js +6 -1
  25. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +168 -36
  26. package/src/error-reporting/request-details.js +308 -0
  27. package/src/frontend-model-controller.js +50 -2
package/README.md CHANGED
@@ -591,16 +591,18 @@ const configuration = new Configuration({
591
591
 
592
592
  This opt-in is ignored in `production`; production frontend-model responses never include internal exception details.
593
593
 
594
- Backends can append client-safe metadata to frontend-model error responses with `configuration.addClientErrorPayloadReporter(...)`. Reporters receive the caught `error`, the current `request`, and a small `context` object, and should only return fields that are safe for clients to see. Frontend-model endpoint failures include `context.frontendModelEndpoint`, `action`, `commandType`, `model`, `requestId`, and `expectedError`. This is useful for attaching an error-reporting URL while keeping the normal production error message generic:
594
+ Backends can append client-safe metadata to frontend-model error responses with `configuration.addClientErrorPayloadReporter(...)`. Reporters receive the caught `error`, the current `request`, a safe `requestDetails` snapshot, and a small `context` object, and should only return fields that are safe for clients to see. Frontend-model endpoint failures include `context.frontendModelEndpoint`, `action`, `commandType`, `model`, `requestId`, and `expectedError`. This is useful for attaching an error-reporting URL while keeping the normal production error message generic:
595
595
 
596
596
  ```js
597
- configuration.addClientErrorPayloadReporter(async ({error, request, context}) => {
598
- const report = await reportErrorToService({error, request, context})
597
+ configuration.addClientErrorPayloadReporter(async ({error, requestDetails, context}) => {
598
+ const report = await reportErrorToService({error, requestDetails, context})
599
599
 
600
600
  return {bugReportUrl: report.url}
601
601
  })
602
602
  ```
603
603
 
604
+ `requestDetails` includes `httpMethod`, `path`, and a parsed `body` snapshot when available. The body snapshot redacts common secret keys, truncates large strings and arrays, summarizes uploaded files and buffers without bytes, and compacts oversized frontend-model batches while preserving `requestId`, `model`, `commandType` / `customPath`, and payload shape.
605
+
604
606
  For sqlite web databases, Velocious defaults to `https://sql.js.org/dist/<file>` for `sql.js` wasm loading. You can override wasm resolution per database config with `locateFile`:
605
607
 
606
608
  ```js
@@ -212,6 +212,13 @@
212
212
  * @typedef {Record<string, import("./frontend-models/query.js").FrontendModelTransportValue>} ClientErrorPayloadReporterPayload
213
213
  */
214
214
 
215
+ /**
216
+ * @typedef {object} ErrorRequestDetails
217
+ * @property {?} [body] - Sanitized parsed request body, when available.
218
+ * @property {string} httpMethod - Request HTTP method.
219
+ * @property {string} path - Request path.
220
+ */
221
+
215
222
  /**
216
223
  * @typedef {object} ClientErrorPayloadContext
217
224
  * @property {string} controller - Controller class name.
@@ -227,7 +234,8 @@
227
234
  * @typedef {function({
228
235
  * context: ClientErrorPayloadContext,
229
236
  * error: Error,
230
- * request: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default | undefined
237
+ * request: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default | undefined,
238
+ * requestDetails: ErrorRequestDetails | null
231
239
  * }): Promise<ClientErrorPayloadReporterPayload | void> | ClientErrorPayloadReporterPayload | void} ClientErrorPayloadReporterType
232
240
  */
233
241
 
@@ -18,6 +18,7 @@ import Ability from "./authorization/ability.js"
18
18
  import EventEmitter from "./utils/event-emitter.js"
19
19
  import VelociousWebsocketChannelSubscribers from "./http-server/websocket-channel-subscribers.js"
20
20
  import {CurrentConfigurationNotSetError, currentConfiguration, setCurrentConfiguration} from "./current-configuration.js"
21
+ import {requestDetails} from "./error-reporting/request-details.js"
21
22
  import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
22
23
  import PluginRoutes from "./routes/plugin-routes.js"
23
24
  import restArgsError from "./utils/rest-args-error.js"
@@ -2411,9 +2412,13 @@ export default class VelociousConfiguration {
2411
2412
  async clientErrorPayloadForError(args) {
2412
2413
  /** @type {import("./configuration-types.js").ClientErrorPayloadReporterPayload} */
2413
2414
  const payload = {}
2415
+ const details = requestDetails(args.request)
2414
2416
 
2415
2417
  for (const reporter of this._clientErrorPayloadReporters) {
2416
- const reporterPayload = await reporter(args)
2418
+ const reporterPayload = await reporter({
2419
+ ...args,
2420
+ requestDetails: details
2421
+ })
2417
2422
 
2418
2423
  if (reporterPayload && typeof reporterPayload === "object") {
2419
2424
  Object.assign(payload, reporterPayload)
@@ -29,7 +29,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
29
29
  /** @type {Map<string, string> | null} */
30
30
  _resourceMethodReturnTypes = null
31
31
 
32
- /** @type {Map<string, string[]> | null} */
32
+ /** @type {Map<string, Array<{name: string | null, type: string}>> | null} */
33
33
  _resourceMethodParameterTypes = null
34
34
 
35
35
  /**
@@ -288,7 +288,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
288
288
  }
289
289
  const collectionCommands = modelConfig.collectionCommands
290
290
  const memberCommands = modelConfig.memberCommands
291
- const commandMetadata = modelConfig.commandMetadata || {}
291
+ const declaredCommandMetadata = modelConfig.commandMetadata || {}
292
+ const commandMetadata = await this.commandMetadataWithResourceJsDoc({
293
+ commandMetadata: declaredCommandMetadata,
294
+ commandNames: [...Object.keys(collectionCommands), ...Object.keys(memberCommands)],
295
+ resourceClass
296
+ })
292
297
  const builtInCollectionCommandsAreDefault = builtInCollectionCommands.create === "create" && builtInCollectionCommands.index === "index"
293
298
  const builtInMemberCommandsAreDefault = builtInMemberCommands.attach === "attach"
294
299
  && builtInMemberCommands.destroy === "destroy"
@@ -472,12 +477,12 @@ export default class DbGenerateFrontendModels extends BaseCommand {
472
477
  fileContent += ` * @returns {Promise<${signature.returnType}>} - Command response.\n`
473
478
  fileContent += " */\n"
474
479
  fileContent += ` static async ${methodName}(${signature.parameters}) {\n`
475
- fileContent += " return await this.executeCustomCommand({\n"
480
+ fileContent += ` return /** @type {${signature.returnType}} */ (await this.executeCustomCommand({\n`
476
481
  fileContent += ` commandName: ${JSON.stringify(collectionCommands[methodName])},\n`
477
482
  fileContent += ` commandType: ${JSON.stringify(collectionCommands[methodName])},\n`
478
483
  fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(${signature.payloadArguments}),\n`
479
484
  fileContent += " resourcePath: this.resourcePath()\n"
480
- fileContent += " })\n"
485
+ fileContent += " }))\n"
481
486
  fileContent += " }\n"
482
487
  }
483
488
 
@@ -491,13 +496,13 @@ export default class DbGenerateFrontendModels extends BaseCommand {
491
496
  fileContent += ` * @returns {Promise<${signature.returnType}>} - Command response.\n`
492
497
  fileContent += " */\n"
493
498
  fileContent += ` async ${methodName}(${signature.parameters}) {\n`
494
- fileContent += ` return await ${className}.executeCustomCommand({\n`
499
+ fileContent += ` return /** @type {${signature.returnType}} */ (await ${className}.executeCustomCommand({\n`
495
500
  fileContent += ` commandName: ${JSON.stringify(memberCommands[methodName])},\n`
496
501
  fileContent += ` commandType: ${JSON.stringify(memberCommands[methodName])},\n`
497
502
  fileContent += " memberId: this.primaryKeyValue(),\n"
498
503
  fileContent += ` payload: ${className}.normalizeCustomCommandPayloadArguments(${signature.payloadArguments}),\n`
499
504
  fileContent += ` resourcePath: ${className}.resourcePath()\n`
500
- fileContent += " })\n"
505
+ fileContent += " }))\n"
501
506
  fileContent += " }\n"
502
507
  }
503
508
 
@@ -1406,11 +1411,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1406
1411
  * @returns {string} - A frontend-resolvable attribute type.
1407
1412
  */
1408
1413
  frontendResolvableAttributeJsDocType(jsDocType) {
1409
- const safeTypeIdentifiers = new Set([
1410
- "Array", "Date", "Exclude", "Extract", "FrontendModelAttributeValue", "FrontendModelTransportValue",
1411
- "Map", "NonNullable", "Omit", "Partial", "Pick", "Promise", "Readonly", "ReadonlyArray", "Record",
1412
- "Required", "ReturnType", "Set"
1413
- ])
1414
+ const safeTypeIdentifiers = this.frontendResolvableTypeIdentifiers()
1414
1415
  const referencedIdentifiers = jsDocType.match(/[A-Z][A-Za-z0-9_$]*/g) || []
1415
1416
 
1416
1417
  if (referencedIdentifiers.some((identifier) => !safeTypeIdentifiers.has(identifier))) {
@@ -1420,6 +1421,43 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1420
1421
  return jsDocType
1421
1422
  }
1422
1423
 
1424
+ /**
1425
+ * Capitalized identifiers a generated frontend model can resolve on its own
1426
+ * (primitives are lower-case and matched separately), so only framework-owned
1427
+ * and builtin generic types are listed.
1428
+ * @returns {Set<string>} - Frontend-resolvable type identifiers.
1429
+ */
1430
+ frontendResolvableTypeIdentifiers() {
1431
+ return new Set([
1432
+ "Array", "Date", "Exclude", "Extract", "FrontendModelAttributeValue", "FrontendModelTransportValue",
1433
+ "Map", "NonNullable", "Omit", "Partial", "Pick", "Promise", "Readonly", "ReadonlyArray", "Record",
1434
+ "Required", "ReturnType", "Set"
1435
+ ])
1436
+ }
1437
+
1438
+ /**
1439
+ * Rewrites a custom-command param/return JSDoc type so it resolves in the generated
1440
+ * frontend model: each model-class (or otherwise non-frontend-resolvable) identifier
1441
+ * becomes `any` in place, keeping the surrounding object fields typed. A command-result
1442
+ * field holding a model arrives as a serialized transport value, so the consumer hydrates
1443
+ * it with `Model.instantiateFromResponse(...)`. The word boundary avoids matching the
1444
+ * capitalized middle of a camelCase property name (e.g. `adjustedTotalCents`).
1445
+ * @param {string} jsDocType - Resolved (Promise-unwrapped) JSDoc type.
1446
+ * @returns {string} - A frontend-resolvable JSDoc type.
1447
+ */
1448
+ frontendResolvableCommandJsDocType(jsDocType) {
1449
+ const safeTypeIdentifiers = this.frontendResolvableTypeIdentifiers()
1450
+
1451
+ return jsDocType
1452
+ // A type that reaches into a backend source file via `import("...")` (optionally
1453
+ // `.Member` and `[]`) can't resolve from the generated frontend model and would type
1454
+ // a serialized result as a backend model instance, so collapse it to `any`.
1455
+ .replace(/import\(\s*["'][^"']*["']\s*\)(\s*\.\s*[A-Za-z_$][\w$]*)*(\s*\[\s*\])*/g, "any")
1456
+ // Remaining capitalized identifiers are model classes or otherwise non-resolvable
1457
+ // types; downgrade each in place so sibling scalar fields keep their real types.
1458
+ .replace(/\b[A-Z][A-Za-z0-9_$]*/g, (identifier) => safeTypeIdentifiers.has(identifier) ? identifier : "any")
1459
+ }
1460
+
1423
1461
  /**
1424
1462
  * Builds the JSDoc param block, parameter list, payload-argument expression, and
1425
1463
  * return type for a custom command method. With declared `args` each becomes a
@@ -1453,6 +1491,69 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1453
1491
  }
1454
1492
  }
1455
1493
 
1494
+ /**
1495
+ * Enriches custom-command metadata by deriving a command's typed args and return
1496
+ * type from the backend resource method's `@param`/`@returns` JSDoc when they are
1497
+ * not already declared in `resourceConfig`. Precedence: explicit `resourceConfig`
1498
+ * `{args, returnType}` wins, then the derived backend-method JSDoc, then the generic
1499
+ * default. Model-class identifiers in the derived types are downgraded to `any`
1500
+ * because the frontend receives a serialized record, not a model instance, which the
1501
+ * consumer hydrates with `Model.instantiateFromResponse(...)`.
1502
+ * @param {object} args - Arguments.
1503
+ * @param {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} args.commandMetadata - Declared per-command metadata.
1504
+ * @param {string[]} args.commandNames - Command method names to resolve.
1505
+ * @param {import("../../../../../configuration-types.js").FrontendModelResourceClassType | null | undefined} args.resourceClass - Resource class.
1506
+ * @returns {Promise<Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>>} - Enriched metadata.
1507
+ */
1508
+ async commandMetadataWithResourceJsDoc({commandMetadata, commandNames, resourceClass}) {
1509
+ if (!resourceClass) return commandMetadata
1510
+
1511
+ /** @type {Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>} */
1512
+ const enriched = {...commandMetadata}
1513
+
1514
+ for (const commandName of commandNames) {
1515
+ const declared = commandMetadata[commandName] || {args: [], returnType: null}
1516
+ const sourceClassName = this.methodOwnerClassName({methodName: commandName, targetClass: resourceClass})
1517
+
1518
+ if (!sourceClassName) {
1519
+ enriched[commandName] = declared
1520
+
1521
+ continue
1522
+ }
1523
+
1524
+ let returnType = declared.returnType
1525
+
1526
+ if (!returnType) {
1527
+ const jsDocReturnType = await this.resourceMethodReturnType({methodName: commandName, sourceClassName})
1528
+
1529
+ if (jsDocReturnType) {
1530
+ returnType = this.frontendResolvableCommandJsDocType(this.unwrappedPromiseJsDocType({jsDocType: jsDocReturnType}))
1531
+ }
1532
+ }
1533
+
1534
+ let args = declared.args
1535
+
1536
+ if (!args || args.length === 0) {
1537
+ const jsDocParameters = await this.resourceMethodParameters({methodName: commandName, sourceClassName})
1538
+ // Skip object-property tags (`@param {string} args.message`); only the
1539
+ // top-level parameters map to method arguments, otherwise the shared
1540
+ // `@param {object} args` + property style would emit `name(args, args)`.
1541
+ const topLevelParameters = (jsDocParameters || []).filter((parameter) => typeof parameter.name === "string" && !parameter.name.includes("."))
1542
+
1543
+ if (topLevelParameters.length > 0) {
1544
+ args = topLevelParameters.map((parameter) => ({
1545
+ name: /** @type {string} */ (parameter.name),
1546
+ type: this.frontendResolvableCommandJsDocType(parameter.type)
1547
+ }))
1548
+ }
1549
+ }
1550
+
1551
+ enriched[commandName] = {args: args || [], returnType: returnType || null}
1552
+ }
1553
+
1554
+ return enriched
1555
+ }
1556
+
1456
1557
  /**
1457
1558
  * Runs unwrapped promise js doc type.
1458
1559
  * @param {object} args - Arguments.
@@ -1534,26 +1635,39 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1534
1635
  * @returns {Promise<string | null>} - JSDoc parameter type when documented.
1535
1636
  */
1536
1637
  async resourceMethodParameterType({methodName, parameterIndex, sourceClassName}) {
1537
- const resourceMethodParameterTypes = await this.resourceMethodParameterTypes()
1538
- const parameterTypesKey = `${sourceClassName}.${methodName}`
1638
+ const parameters = await this.resourceMethodParameters({methodName, sourceClassName})
1539
1639
 
1540
- if (!resourceMethodParameterTypes.has(parameterTypesKey)) return null
1640
+ if (!parameters) return null
1641
+
1642
+ const parameter = parameters[parameterIndex]
1541
1643
 
1542
- const parameterTypes = resourceMethodParameterTypes.get(parameterTypesKey)
1644
+ if (parameter === undefined) return null
1543
1645
 
1544
- if (!parameterTypes) {
1545
- throw new Error(`Expected JSDoc parameter types for ${parameterTypesKey}`)
1646
+ if (parameter.type.length < 1) {
1647
+ throw new Error(`Expected non-empty JSDoc parameter type for ${sourceClassName}.${methodName} parameter ${parameterIndex}`)
1546
1648
  }
1547
1649
 
1548
- const parameterType = parameterTypes[parameterIndex]
1650
+ return parameter.type
1651
+ }
1652
+
1653
+ /**
1654
+ * Runs resource method parameters.
1655
+ * @param {{methodName: string, sourceClassName: string}} args - Arguments.
1656
+ * @returns {Promise<Array<{name: string | null, type: string}> | null>} - JSDoc parameters (name + type) when documented.
1657
+ */
1658
+ async resourceMethodParameters({methodName, sourceClassName}) {
1659
+ const resourceMethodParameterTypes = await this.resourceMethodParameterTypes()
1660
+ const parameterTypesKey = `${sourceClassName}.${methodName}`
1661
+
1662
+ if (!resourceMethodParameterTypes.has(parameterTypesKey)) return null
1549
1663
 
1550
- if (parameterType === undefined) return null
1664
+ const parameters = resourceMethodParameterTypes.get(parameterTypesKey)
1551
1665
 
1552
- if (parameterType.length < 1) {
1553
- throw new Error(`Expected non-empty JSDoc parameter type for ${parameterTypesKey} parameter ${parameterIndex}`)
1666
+ if (!parameters) {
1667
+ throw new Error(`Expected JSDoc parameters for ${parameterTypesKey}`)
1554
1668
  }
1555
1669
 
1556
- return parameterType
1670
+ return parameters
1557
1671
  }
1558
1672
 
1559
1673
  /**
@@ -1579,7 +1693,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1579
1693
 
1580
1694
  /**
1581
1695
  * Runs resource method parameter types.
1582
- * @returns {Promise<Map<string, string[]>>} - Resource method parameter types keyed by ClassName.methodName.
1696
+ * @returns {Promise<Map<string, Array<{name: string | null, type: string}>>>} - Resource method parameters keyed by ClassName.methodName.
1583
1697
  */
1584
1698
  async resourceMethodParameterTypes() {
1585
1699
  if (this._resourceMethodParameterTypes) return this._resourceMethodParameterTypes
@@ -1678,7 +1792,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1678
1792
 
1679
1793
  /**
1680
1794
  * Adds resource method parameter types from source.
1681
- * @param {{parameterTypes: Map<string, string[]>, sourceText: string}} args - Arguments.
1795
+ * @param {{parameterTypes: Map<string, Array<{name: string | null, type: string}>>, sourceText: string}} args - Arguments.
1682
1796
  * @returns {void}
1683
1797
  */
1684
1798
  addResourceMethodParameterTypesFromSource({parameterTypes, sourceText}) {
@@ -1710,10 +1824,10 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1710
1824
  if (!methodMatch) continue
1711
1825
 
1712
1826
  const methodName = methodMatch[1]
1713
- const jsDocParameterTypes = this.jsDocParameterTypes(jsDocMatch[1])
1827
+ const jsDocParameters = this.jsDocParameters(jsDocMatch[1])
1714
1828
 
1715
- if (jsDocParameterTypes.length > 0) {
1716
- parameterTypes.set(`${className}.${methodName}`, jsDocParameterTypes)
1829
+ if (jsDocParameters.length > 0) {
1830
+ parameterTypes.set(`${className}.${methodName}`, jsDocParameters)
1717
1831
  }
1718
1832
  }
1719
1833
 
@@ -1738,7 +1852,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1738
1852
  throw new Error(`Could not parse JSDoc return type from: ${jsDocText}`)
1739
1853
  }
1740
1854
 
1741
- const returnType = jsDocText.slice(typeOpenIndex + 1, typeCloseIndex).trim()
1855
+ const returnType = this.normalizeJsDocType(jsDocText.slice(typeOpenIndex + 1, typeCloseIndex))
1742
1856
 
1743
1857
  if (returnType.length < 1) {
1744
1858
  throw new Error(`Expected non-empty JSDoc return type in: ${jsDocText}`)
@@ -1748,12 +1862,24 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1748
1862
  }
1749
1863
 
1750
1864
  /**
1751
- * Runs js doc parameter types.
1865
+ * Collapses a JSDoc type spanning multiple comment lines into a single line so it can
1866
+ * be emitted into an inline type-assertion cast. A multiline backend return type keeps
1867
+ * its leading continuation asterisks in the captured substring, which are invalid inside
1868
+ * an inline cast and make TypeScript read the asserted type as `undefined`.
1869
+ * @param {string} jsDocType - Raw captured JSDoc type, possibly multiline.
1870
+ * @returns {string} - Single-line JSDoc type.
1871
+ */
1872
+ normalizeJsDocType(jsDocType) {
1873
+ return jsDocType.replace(/\s*\n\s*\*?[ \t]*/g, " ").trim()
1874
+ }
1875
+
1876
+ /**
1877
+ * Runs js doc parameters.
1752
1878
  * @param {string} jsDocText - JSDoc text inside comment markers.
1753
- * @returns {string[]} - JSDoc parameter types in declaration order.
1879
+ * @returns {Array<{name: string | null, type: string}>} - JSDoc parameters (name + type) in declaration order.
1754
1880
  */
1755
- jsDocParameterTypes(jsDocText) {
1756
- const parameterTypes = []
1881
+ jsDocParameters(jsDocText) {
1882
+ const parameters = []
1757
1883
  const paramRegex = /@param\s*\{/g
1758
1884
  let _paramMatch
1759
1885
 
@@ -1765,17 +1891,23 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1765
1891
  throw new Error(`Could not parse JSDoc parameter type from: ${jsDocText}`)
1766
1892
  }
1767
1893
 
1768
- const parameterType = jsDocText.slice(typeOpenIndex + 1, typeCloseIndex).trim()
1894
+ const type = this.normalizeJsDocType(jsDocText.slice(typeOpenIndex + 1, typeCloseIndex))
1769
1895
 
1770
- if (parameterType.length < 1) {
1896
+ if (type.length < 1) {
1771
1897
  throw new Error(`Expected non-empty JSDoc parameter type in: ${jsDocText}`)
1772
1898
  }
1773
1899
 
1774
- parameterTypes.push(parameterType)
1900
+ // After the closing brace the parameter name follows (optionally bracketed
1901
+ // for `@param {type} [name]`). Capture the leading name token — including any
1902
+ // dotted path so object-property tags like `@param {string} args.message` stay
1903
+ // distinguishable from the top-level `@param {object} args` parameter.
1904
+ const nameMatch = jsDocText.slice(typeCloseIndex + 1).match(/^\s*\[?\s*([A-Za-z_$][\w$.]*)/)
1905
+
1906
+ parameters.push({name: nameMatch ? nameMatch[1] : null, type})
1775
1907
  paramRegex.lastIndex = typeCloseIndex + 1
1776
1908
  }
1777
1909
 
1778
- return parameterTypes
1910
+ return parameters
1779
1911
  }
1780
1912
 
1781
1913
  /**