@nasebanal/cli 0.3.0 → 0.5.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/README.md CHANGED
@@ -10,7 +10,7 @@ Contract-Driven Development (CDD) pipeline, alongside the web frontends.
10
10
 
11
11
  ```console
12
12
  $ nb account me get
13
- $ nb recorder records list
13
+ $ nb record records list
14
14
  $ nb target shares accept <id>
15
15
  $ nb account me tokens create --data '{"name":"ci"}'
16
16
  ```
@@ -45,7 +45,17 @@ export NB_TOKEN=nbpat_… # a PAT or service-account token (no browser ne
45
45
 
46
46
  # JUnit XML: vitest (--reporter=junit), pytest (--junitxml), Specmatic, Playwright (--reporter=junit)
47
47
  nb assurance report upload --file report/junit.xml --kind unit --tool pytest
48
- nb assurance report upload --file results/junit.xml --kind e2e --tool playwright --suite web
48
+ nb assurance report upload --file results/junit.xml --kind e2e --tool playwright --component web
49
+
50
+ # Two scans of one tool on the same components (ZAP's baseline and full scan): a label tells them apart
51
+ nb assurance report upload --file report/baseline-junit.xml --kind security --tool zap --component frontend --label baseline
52
+ nb assurance report upload --file report/full-scan-junit.xml --kind security --tool zap --component frontend --label full-scan
53
+
54
+ # The tool's own version is informational and optional
55
+ nb assurance report upload --file report/junit.xml --kind unit --tool pytest --tool-version 9.1.0
56
+
57
+ # A result may be about several components: repeat the flag (new names are created in the project; none = `default`)
58
+ nb assurance report upload --file results/junit.xml --kind e2e --tool playwright --component frontend --component backend
49
59
 
50
60
  # Specmatic: the JUnit XML, plus its HTML report for the API coverage (coverage_pct = operations exercised / operations in the spec)
51
61
  nb assurance report upload --file specmatic/junit/TEST-junit-jupiter.xml --kind contract --tool specmatic \
package/dist/apis.js CHANGED
@@ -1,7 +1,6 @@
1
1
  export const API_CATALOG = [
2
2
  { name: "account", spec: "account.yaml", summary: "User profiles, subscriptions, billing, and API tokens" },
3
- { name: "recorder", spec: "recorder.yaml", summary: "Records and tags (NASEBANAL Recorder)" },
3
+ { name: "record", spec: "recorder.yaml", summary: "Records and tags (NASEBANAL Recorder)" },
4
4
  { name: "target", spec: "target.yaml", summary: "Targets, cells, and shares (NASEBANAL Target)" },
5
5
  { name: "assurance", spec: "assurance.yaml", summary: "Monitors, projects and CI/CD test results (NASEBANAL Assurance)" },
6
- { name: "app-template", spec: "app-template.yaml", summary: "Reference API surface (App Template)" },
7
6
  ];
@@ -7,6 +7,20 @@
7
7
  * task exception, or a failure ratio above the allowed one, fails the run.
8
8
  */
9
9
  import { toRecords } from "./csv.js";
10
+ /**
11
+ * The CSV's column names changed between Locust versions: 1.x wrote "# requests" / "# failures" / "Median response
12
+ * time" (and named the aggregate row "Total"), 2.x writes "Request Count" / "Failure Count" / "Median Response Time"
13
+ * ("Aggregated"). Each value is read under every name it has had, the first present one winning.
14
+ */
15
+ const COLUMNS = {
16
+ requests: ["Request Count", "# requests"],
17
+ failures: ["Failure Count", "# failures"],
18
+ median: ["Median Response Time", "Median response time"],
19
+ average: ["Average Response Time", "Average response time"],
20
+ max: ["Max Response Time", "Max response time"],
21
+ };
22
+ const has = (row, names) => names.some((n) => n in row);
23
+ const read = (row, names) => num(row[names.find((n) => n in row) ?? ""]);
10
24
  function num(value) {
11
25
  if (value === undefined || value === "" || value === "N/A")
12
26
  return null;
@@ -16,21 +30,26 @@ function num(value) {
16
30
  const round = (n, digits = 2) => Math.round(n * 10 ** digits) / 10 ** digits;
17
31
  export function summariseLocust(files, maxFailRatio) {
18
32
  const stats = toRecords(files.stats);
19
- const total = stats.find((r) => r.Name === "Aggregated");
33
+ // The row of all requests together: "Aggregated" (2.x) or "Total" (1.x).
34
+ const total = stats.find((r) => r.Name === "Aggregated" || r.Name === "Total");
20
35
  if (!total)
21
- throw new Error('No "Aggregated" row in the stats CSV - is this a Locust *_stats.csv file?');
22
- const requests = num(total["Request Count"]) ?? 0;
23
- const failed = num(total["Failure Count"]) ?? 0;
36
+ throw new Error('No "Aggregated" (or "Total") row in the stats CSV - is this a Locust *_stats.csv file?');
37
+ // Without the counts the numbers below would silently be 0, a green result for a report nobody understood: refuse it.
38
+ if (!has(total, COLUMNS.requests) || !has(total, COLUMNS.failures)) {
39
+ throw new Error(`The stats CSV has no request / failure count column (looked for ${[...COLUMNS.requests, ...COLUMNS.failures].map((n) => `"${n}"`).join(", ")}; found: ${Object.keys(total).join(", ")}) - is it from a Locust version this nb does not know?`);
40
+ }
41
+ const requests = read(total, COLUMNS.requests) ?? 0;
42
+ const failed = read(total, COLUMNS.failures) ?? 0;
24
43
  const metrics = { requests, failures: failed };
25
44
  const put = (key, value) => {
26
45
  if (value !== null)
27
46
  metrics[key] = round(value);
28
47
  };
29
- put("avg_ms", num(total["Average Response Time"]));
30
- put("p50_ms", num(total["50%"]) ?? num(total["Median Response Time"]));
48
+ put("avg_ms", read(total, COLUMNS.average));
49
+ put("p50_ms", num(total["50%"]) ?? read(total, COLUMNS.median));
31
50
  put("p95_ms", num(total["95%"]));
32
51
  put("p99_ms", num(total["99%"]));
33
- put("max_ms", num(total["Max Response Time"]));
52
+ put("max_ms", read(total, COLUMNS.max));
34
53
  put("rps", num(total["Requests/s"]));
35
54
  const ratio = requests > 0 ? failed / requests : 0;
36
55
  metrics.error_rate_pct = round(ratio * 100);
@@ -127,7 +127,13 @@ function locustSibling(statsFile, suffix) {
127
127
  }
128
128
  export function parseReport(opts) {
129
129
  const text = readFileSync(opts.file, "utf8");
130
- const base = { kind: opts.kind, tool: opts.tool, ...(opts.suiteName ? { name: opts.suiteName } : {}) };
130
+ const base = {
131
+ kind: opts.kind,
132
+ tool: opts.tool,
133
+ ...(opts.components && opts.components.length > 0 ? { components: opts.components } : {}),
134
+ ...(opts.label ? { label: opts.label } : {}),
135
+ ...(opts.toolVersion ? { tool_version: opts.toolVersion } : {}),
136
+ };
131
137
  if (opts.format === "junit") {
132
138
  const j = parseJunit(text);
133
139
  const status = opts.status ?? (j.failed === 0 ? "passed" : "failed");
@@ -214,6 +220,49 @@ function statsFileIn(dir) {
214
220
  fail(`No *_stats.csv file in ${dir}`);
215
221
  return join(dir, files.sort()[files.length - 1]);
216
222
  }
223
+ /** At most this many components per result (the API's limit) and this many characters in a name. */
224
+ const MAX_COMPONENTS = 20;
225
+ const MAX_COMPONENT_LENGTH = 100;
226
+ /** The `--label`: trimmed, not empty, within the API's limit. */
227
+ function checkLabel(raw) {
228
+ if (raw === undefined)
229
+ return undefined;
230
+ const label = raw.trim();
231
+ if (label === "")
232
+ fail("--label must not be empty");
233
+ if (label.length > MAX_COMPONENT_LENGTH)
234
+ fail(`--label must be ${MAX_COMPONENT_LENGTH} characters or fewer`);
235
+ return label;
236
+ }
237
+ /** The `--tool-version`: trimmed, not empty, within the API's limit. */
238
+ function checkToolVersion(raw) {
239
+ if (raw === undefined)
240
+ return undefined;
241
+ const version = raw.trim();
242
+ if (version === "")
243
+ fail("--tool-version must not be empty");
244
+ if (version.length > 50)
245
+ fail("--tool-version must be 50 characters or fewer");
246
+ return version;
247
+ }
248
+ /** The `--component` names: trimmed, no empty one, no two alike ignoring case, within the API's limits. */
249
+ function checkComponents(raw) {
250
+ if (raw === undefined)
251
+ return undefined;
252
+ const out = [];
253
+ for (const value of raw) {
254
+ const name = value.trim();
255
+ if (name === "")
256
+ fail("--component must not be empty");
257
+ if (name.length > MAX_COMPONENT_LENGTH)
258
+ fail(`--component must be ${MAX_COMPONENT_LENGTH} characters or fewer: ${name.slice(0, 20)}…`);
259
+ if (!out.some((o) => o.toLowerCase() === name.toLowerCase()))
260
+ out.push(name);
261
+ }
262
+ if (out.length > MAX_COMPONENTS)
263
+ fail(`At most ${MAX_COMPONENTS} --component values`);
264
+ return out;
265
+ }
217
266
  function resolveFormat(opts, file) {
218
267
  if (opts.format !== undefined) {
219
268
  if (opts.format !== "junit" && opts.format !== "locust")
@@ -235,9 +284,11 @@ export function buildReportUploadCommand() {
235
284
  .option("--format <format>", "junit | locust (default: from the file name)")
236
285
  .option("--kind <kind>", `what the suite proves: ${SUITE_KINDS.join(" | ")} (locust: load)`)
237
286
  .option("--tool <label>", "the producing tool, a free label, e.g. vitest, pytest, playwright, specmatic, zap, trivy (locust: locust)")
238
- .option("--suite <name>", "tells apart several suites of one kind in a run (e.g. api, web)")
287
+ .option("--component <name>", "a component the result is for (e.g. frontend, backend), repeatable when it covers several (e.g. an end-to-end test: --component frontend --component backend); new names are created in the project; leave it out for the component `default`", (v, all = []) => [...all, v])
288
+ .option("--label <name>", "tells apart results that are otherwise the same, such as two scans of one tool on the same components (e.g. --label baseline, --label full-scan); part of the result's identity, so the same label again replaces the result; optional")
289
+ .option("--tool-version <version>", "the version of the tool that produced the report (e.g. 9.1.0): informational, shown next to the tool, handy when a tool's new version changes its layout; optional")
239
290
  .option("--metric <key=value>", "an extra number for the suite, repeatable (e.g. --metric high=0 --metric medium=3; for security: critical, high, medium, low, info)", (v, all = []) => [...all, v])
240
- .option("--api-version <label>", "the API version the result is for (e.g. v1, v2), part of the suite's identity next to its kind and name; leave it out for results that hold for every version, such as unit tests")
291
+ .option("--api-version <label>", "the API version the result is for (e.g. v1, v2), part of the result's identity next to its kind and components; leave it out for results that hold for every version, such as unit tests")
241
292
  .option("--project <name>", "project name (CI: the repository); found, or created if missing")
242
293
  .option("--repo <repo>", "repository recorded on a newly created project (CI: detected)")
243
294
  .option("--run-key <key>", "identity of the pipeline run (CI: detected); all jobs of a run share it")
@@ -272,7 +323,9 @@ export function buildReportUploadCommand() {
272
323
  format,
273
324
  kind,
274
325
  tool: checkedTool,
275
- suiteName: opts.suite,
326
+ components: checkComponents(opts.component),
327
+ label: checkLabel(opts.label),
328
+ toolVersion: checkToolVersion(opts.toolVersion),
276
329
  status: opts.status,
277
330
  maxFailRatio,
278
331
  file,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nasebanal/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "gcloud-style command-line interface for NASEBANAL APIs, generated from nb-api-specs OpenAPI contracts.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,7 +2,7 @@ openapi: 3.0.3
2
2
  info:
3
3
  title: NASEBANAL Account API
4
4
  description: Centralized user and billing management API for NASEBANAL applications. Manages user profiles, per-service subscriptions, Stripe billing, and Personal Access Tokens (PAT).
5
- version: 1.5.1
5
+ version: 1.6.0
6
6
  contact:
7
7
  name: NASEBANAL
8
8
  url: https://www.nasebanal.com
@@ -1,488 +0,0 @@
1
- openapi: 3.0.3
2
- info:
3
- title: NASEBANAL - App Template API
4
- description: |-
5
- A template API for NASEBANAL applications. Provides sample CRUD endpoints, user authentication via Auth0 JWT and Personal Access Tokens (PAT), and integration with the Account API.
6
-
7
- All responses use a uniform envelope: successful responses are `{ "success": true, "data": ... }` and failures are `{ "success": false, "error": { "code", "message" } }`. This mirrors `successResponse()` / `errorResponse()` in the implementation. The `success` discriminator is pinned with `enum` so both Specmatic stub generation and contract tests treat it as a fixed value per response class.
8
- version: 1.1.1
9
- contact:
10
- name: NASEBANAL
11
- url: https://www.nasebanal.com
12
- servers:
13
- - url: http://localhost:8789
14
- description: Local dev (wrangler dev)
15
- - url: https://api.app-template.nasebanal.com
16
- description: Production
17
- security:
18
- - BearerAuth: []
19
- paths:
20
- /api/v1/health:
21
- get:
22
- operationId: getHealth
23
- tags:
24
- - Health
25
- summary: Health check
26
- description: Returns the health status of the API.
27
- security: []
28
- responses:
29
- "200":
30
- description: API is healthy
31
- content:
32
- application/json:
33
- schema:
34
- type: object
35
- required:
36
- - success
37
- - data
38
- properties:
39
- success:
40
- type: boolean
41
- enum:
42
- - true
43
- data:
44
- type: object
45
- required:
46
- - status
47
- - timestamp
48
- properties:
49
- status:
50
- type: string
51
- timestamp:
52
- type: string
53
- format: date-time
54
- examples:
55
- valid:
56
- value:
57
- success: true
58
- data:
59
- status: ok
60
- timestamp: 2026-05-14T12:00:00Z
61
- /api/v1/me:
62
- get:
63
- operationId: getCurrentUser
64
- tags:
65
- - User
66
- summary: Get current user profile
67
- description: Returns the authenticated user's profile information from the Account API.
68
- responses:
69
- "200":
70
- description: User profile
71
- content:
72
- application/json:
73
- schema:
74
- $ref: "#/components/schemas/UserResponse"
75
- examples:
76
- valid:
77
- value:
78
- success: true
79
- data:
80
- id: 42
81
- auth0_sub: auth0|0123456789abcdef
82
- email: user@example.com
83
- name: Sample User
84
- language: ja
85
- plan: pro
86
- subscription_status: active
87
- subscription_expires_at: 2026-12-31T23:59:59Z
88
- "401":
89
- $ref: "#/components/responses/Unauthorized"
90
- /api/v1/items:
91
- get:
92
- operationId: listItems
93
- tags:
94
- - Items
95
- summary: List items
96
- description: Returns all items belonging to the authenticated user.
97
- responses:
98
- "200":
99
- description: List of items
100
- content:
101
- application/json:
102
- schema:
103
- type: object
104
- required:
105
- - success
106
- - data
107
- properties:
108
- success:
109
- type: boolean
110
- enum:
111
- - true
112
- data:
113
- type: object
114
- required:
115
- - data
116
- - total
117
- properties:
118
- data:
119
- type: array
120
- items:
121
- $ref: "#/components/schemas/Item"
122
- total:
123
- type: integer
124
- examples:
125
- valid:
126
- value:
127
- success: true
128
- data:
129
- data:
130
- - id: itm_a1b2c3d4
131
- user_id: 42
132
- name: My Item
133
- description: A sample item description
134
- created_at: 2026-05-14T12:00:00Z
135
- updated_at: 2026-05-14T12:00:00Z
136
- total: 1
137
- "401":
138
- $ref: "#/components/responses/Unauthorized"
139
- post:
140
- operationId: createItem
141
- tags:
142
- - Items
143
- summary: Create item
144
- description: Creates a new item for the authenticated user.
145
- requestBody:
146
- required: true
147
- content:
148
- application/json:
149
- schema:
150
- $ref: "#/components/schemas/CreateItemInput"
151
- examples:
152
- valid:
153
- value:
154
- name: My Item
155
- description: A sample item description
156
- responses:
157
- "201":
158
- description: Item created
159
- content:
160
- application/json:
161
- schema:
162
- $ref: "#/components/schemas/ItemResponse"
163
- examples:
164
- valid:
165
- value:
166
- success: true
167
- data:
168
- id: itm_a1b2c3d4
169
- user_id: 42
170
- name: My Item
171
- description: A sample item description
172
- created_at: 2026-05-14T12:00:00Z
173
- updated_at: 2026-05-14T12:00:00Z
174
- "401":
175
- $ref: "#/components/responses/Unauthorized"
176
- /api/v1/items/{id}:
177
- get:
178
- operationId: getItem
179
- tags:
180
- - Items
181
- summary: Get item
182
- description: Returns a single item by ID.
183
- parameters:
184
- - name: id
185
- in: path
186
- required: true
187
- schema:
188
- type: string
189
- examples:
190
- valid:
191
- value: itm_a1b2c3d4
192
- responses:
193
- "200":
194
- description: Item details
195
- content:
196
- application/json:
197
- schema:
198
- $ref: "#/components/schemas/ItemResponse"
199
- examples:
200
- valid:
201
- value:
202
- success: true
203
- data:
204
- id: itm_a1b2c3d4
205
- user_id: 42
206
- name: My Item
207
- description: A sample item description
208
- created_at: 2026-05-14T12:00:00Z
209
- updated_at: 2026-05-14T12:00:00Z
210
- "401":
211
- $ref: "#/components/responses/Unauthorized"
212
- "404":
213
- $ref: "#/components/responses/NotFound"
214
- put:
215
- operationId: updateItem
216
- tags:
217
- - Items
218
- summary: Update item
219
- description: Updates an existing item.
220
- parameters:
221
- - name: id
222
- in: path
223
- required: true
224
- schema:
225
- type: string
226
- examples:
227
- valid:
228
- value: itm_a1b2c3d4
229
- requestBody:
230
- required: true
231
- content:
232
- application/json:
233
- schema:
234
- $ref: "#/components/schemas/UpdateItemInput"
235
- examples:
236
- valid:
237
- value:
238
- name: Updated Item
239
- description: An updated item description
240
- responses:
241
- "200":
242
- description: Item updated
243
- content:
244
- application/json:
245
- schema:
246
- $ref: "#/components/schemas/ItemResponse"
247
- examples:
248
- valid:
249
- value:
250
- success: true
251
- data:
252
- id: itm_a1b2c3d4
253
- user_id: 42
254
- name: Updated Item
255
- description: An updated item description
256
- created_at: 2026-05-14T12:00:00Z
257
- updated_at: 2026-05-15T09:30:00Z
258
- "401":
259
- $ref: "#/components/responses/Unauthorized"
260
- "404":
261
- $ref: "#/components/responses/NotFound"
262
- delete:
263
- operationId: deleteItem
264
- tags:
265
- - Items
266
- summary: Delete item
267
- description: Deletes an item by ID.
268
- parameters:
269
- - name: id
270
- in: path
271
- required: true
272
- schema:
273
- type: string
274
- examples:
275
- valid:
276
- value: itm_a1b2c3d4
277
- responses:
278
- "200":
279
- description: Item deleted
280
- content:
281
- application/json:
282
- schema:
283
- $ref: "#/components/schemas/SuccessResponse"
284
- examples:
285
- valid:
286
- value:
287
- success: true
288
- data:
289
- success: true
290
- "401":
291
- $ref: "#/components/responses/Unauthorized"
292
- "404":
293
- $ref: "#/components/responses/NotFound"
294
- components:
295
- securitySchemes:
296
- BearerAuth:
297
- type: http
298
- scheme: bearer
299
- bearerFormat: JWT or PAT
300
- description: Use either an Auth0 JWT token or a Personal Access Token (PAT) starting with `nbpat_`.
301
- schemas:
302
- User:
303
- type: object
304
- required:
305
- - id
306
- - auth0_sub
307
- - email
308
- - name
309
- - language
310
- - plan
311
- properties:
312
- id:
313
- type: integer
314
- auth0_sub:
315
- type: string
316
- email:
317
- type: string
318
- format: email
319
- name:
320
- type: string
321
- language:
322
- type: string
323
- enum:
324
- - ja
325
- - en
326
- plan:
327
- type: string
328
- enum:
329
- - free
330
- - pro
331
- - family
332
- - enterprise
333
- subscription_status:
334
- type: string
335
- enum:
336
- - active
337
- - canceled
338
- - past_due
339
- - trialing
340
- - null
341
- nullable: true
342
- subscription_expires_at:
343
- type: string
344
- format: date-time
345
- nullable: true
346
- UserResponse:
347
- type: object
348
- required:
349
- - success
350
- - data
351
- properties:
352
- success:
353
- type: boolean
354
- enum:
355
- - true
356
- data:
357
- $ref: "#/components/schemas/User"
358
- Item:
359
- type: object
360
- required:
361
- - id
362
- - user_id
363
- - name
364
- - created_at
365
- - updated_at
366
- properties:
367
- id:
368
- type: string
369
- user_id:
370
- type: integer
371
- name:
372
- type: string
373
- description:
374
- type: string
375
- nullable: true
376
- created_at:
377
- type: string
378
- format: date-time
379
- updated_at:
380
- type: string
381
- format: date-time
382
- ItemResponse:
383
- type: object
384
- required:
385
- - success
386
- - data
387
- properties:
388
- success:
389
- type: boolean
390
- enum:
391
- - true
392
- data:
393
- $ref: "#/components/schemas/Item"
394
- CreateItemInput:
395
- type: object
396
- required:
397
- - name
398
- properties:
399
- name:
400
- type: string
401
- minLength: 1
402
- maxLength: 200
403
- description:
404
- type: string
405
- maxLength: 1000
406
- UpdateItemInput:
407
- type: object
408
- properties:
409
- name:
410
- type: string
411
- minLength: 1
412
- maxLength: 200
413
- description:
414
- type: string
415
- maxLength: 1000
416
- SuccessResponse:
417
- type: object
418
- required:
419
- - success
420
- - data
421
- properties:
422
- success:
423
- type: boolean
424
- enum:
425
- - true
426
- data:
427
- type: object
428
- required:
429
- - success
430
- properties:
431
- success:
432
- type: boolean
433
- enum:
434
- - true
435
- Error:
436
- type: object
437
- required:
438
- - success
439
- - error
440
- properties:
441
- success:
442
- type: boolean
443
- enum:
444
- - false
445
- error:
446
- type: object
447
- required:
448
- - code
449
- - message
450
- properties:
451
- code:
452
- type: string
453
- message:
454
- type: string
455
- responses:
456
- Unauthorized:
457
- description: Authentication required or invalid token
458
- content:
459
- application/json:
460
- schema:
461
- $ref: "#/components/schemas/Error"
462
- examples:
463
- unauthorized:
464
- value:
465
- success: false
466
- error:
467
- code: AUTH_ERROR
468
- message: No authentication token provided
469
- NotFound:
470
- description: Resource not found
471
- content:
472
- application/json:
473
- schema:
474
- $ref: "#/components/schemas/Error"
475
- examples:
476
- not_found:
477
- value:
478
- success: false
479
- error:
480
- code: NOT_FOUND
481
- message: Resource not found
482
- tags:
483
- - name: Health
484
- description: Health check endpoints
485
- - name: User
486
- description: User profile endpoints
487
- - name: Items
488
- description: Sample CRUD endpoints (replace with your domain)