@molecule/api-resource-firmware 1.0.0 → 1.0.1

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 (2) hide show
  1. package/README.md +484 -0
  2. package/package.json +14 -8
package/README.md ADDED
@@ -0,0 +1,484 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-04T01:49:12.273Z
7
+ -->
8
+
9
+ # @molecule/api-resource-firmware
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ `@molecule/api-resource-firmware` — owner-scoped firmware versions
16
+ and OTA rollouts.
17
+
18
+ - Versions follow a draft → published → deprecated lifecycle.
19
+ - Rollouts target an explicit `device_ids[]` list, a `fleet_id`, or
20
+ both. The intersection with owner-scoped devices is what actually
21
+ gets enrolled.
22
+ - Per-device `firmware_update_tasks` rows are materialized and a
23
+ matching `device_commands` row with `firmware_update` payload is
24
+ created. Best-effort realtime broadcasts go out on
25
+ `firmware:{deviceId}` and `commands:{deviceId}`.
26
+ - The device reports back via a token-authenticated status endpoint;
27
+ the resource updates the rollout's completed/failed counters and
28
+ bumps the device's `firmware_version` on success.
29
+
30
+ Extracted from the iot-device-manager flagship.
31
+
32
+ ## Quick Start
33
+
34
+ ```ts
35
+ import express from 'express'
36
+ import { createFirmwareRouter, type DeviceTokenMiddleware } from '@molecule/api-resource-firmware'
37
+
38
+ // Device-auth implementations vary per deployment (token, mTLS, JWT, …),
39
+ // so the status-report endpoint takes a caller-supplied authorizer. On
40
+ // success it MUST set `res.locals.deviceAuth = { deviceId, ownerId }`;
41
+ // on failure it responds 401.
42
+ const requireDeviceToken: DeviceTokenMiddleware = async (req, res, next) => {
43
+ const device = await verifyDeviceToken(req.header('authorization'))
44
+ if (!device) {
45
+ res.sendStatus(401)
46
+ return
47
+ }
48
+ res.locals.deviceAuth = { deviceId: device.id, ownerId: device.ownerId }
49
+ next()
50
+ }
51
+
52
+ const app = express()
53
+ app.use('/api/firmware', createFirmwareRouter({ requireDeviceToken }))
54
+ ```
55
+
56
+ ## Type
57
+
58
+ `resource`
59
+
60
+ ## Installation
61
+
62
+ ```bash
63
+ npm install @molecule/api-resource-firmware @molecule/api-bonds-default-express @molecule/api-database @molecule/api-i18n @molecule/api-logger @molecule/api-middleware-validation @molecule/api-realtime express zod
64
+ npm install -D @types/express
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### Interfaces
70
+
71
+ #### `FirmwareRolloutRow`
72
+
73
+ Database row representing a firmware rollout targeting a fleet or set of devices.
74
+
75
+ ```typescript
76
+ interface FirmwareRolloutRow {
77
+ id: string
78
+ owner_id: string
79
+ firmware_id: string
80
+ fleet_id: string | null
81
+ device_ids: unknown
82
+ strategy: RolloutStrategy
83
+ status: RolloutStatus
84
+ target_count: number
85
+ completed_count: number
86
+ failed_count: number
87
+ progress_percent: number
88
+ created_at: string | Date
89
+ updated_at: string | Date
90
+ }
91
+ ```
92
+
93
+ #### `FirmwareUpdateTaskRow`
94
+
95
+ Database row representing a single device's update task within a rollout.
96
+
97
+ ```typescript
98
+ interface FirmwareUpdateTaskRow {
99
+ id: string
100
+ rollout_id: string
101
+ firmware_id: string
102
+ device_id: string
103
+ status: RolloutTaskStatus
104
+ error_message: string | null
105
+ completed_at: string | Date | null
106
+ created_at: string | Date
107
+ updated_at: string | Date
108
+ }
109
+ ```
110
+
111
+ #### `FirmwareVersionRow`
112
+
113
+ Database row representing a firmware version artifact and its metadata.
114
+
115
+ ```typescript
116
+ interface FirmwareVersionRow {
117
+ id: string
118
+ owner_id: string
119
+ version: string
120
+ device_type: string
121
+ release_notes: string
122
+ download_url: string | null
123
+ checksum: string | null
124
+ file_size: number
125
+ status: FirmwareStatus
126
+ released_at: string | Date | null
127
+ created_at: string | Date
128
+ updated_at: string | Date
129
+ }
130
+ ```
131
+
132
+ ### Types
133
+
134
+ #### `DeviceTokenMiddleware`
135
+
136
+ Caller-supplied device-token authorization. Should set
137
+ `res.locals.deviceAuth = { deviceId, ownerId }` on success or 401 on
138
+ failure.
139
+
140
+ ```typescript
141
+ type DeviceTokenMiddleware = RequestHandler
142
+ ```
143
+
144
+ #### `FirmwareStatus`
145
+
146
+ Lifecycle state of a firmware version.
147
+
148
+ ```typescript
149
+ type FirmwareStatus = 'draft' | 'published' | 'deprecated'
150
+ ```
151
+
152
+ #### `RolloutStatus`
153
+
154
+ Current state of a firmware rollout operation.
155
+
156
+ ```typescript
157
+ type RolloutStatus = 'pending' | 'active' | 'completed' | 'failed' | 'canceled'
158
+ ```
159
+
160
+ #### `RolloutStrategy`
161
+
162
+ Deployment strategy used when rolling out a firmware version to devices.
163
+
164
+ ```typescript
165
+ type RolloutStrategy = 'immediate' | 'canary' | 'gradual'
166
+ ```
167
+
168
+ #### `RolloutTaskStatus`
169
+
170
+ Current state of an individual device update task within a rollout.
171
+
172
+ ```typescript
173
+ type RolloutTaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed'
174
+ ```
175
+
176
+ ### Functions
177
+
178
+ #### `createFirmwareForOwner(userId, input)`
179
+
180
+ Create a draft firmware version.
181
+
182
+ ```typescript
183
+ function createFirmwareForOwner(
184
+ userId: string,
185
+ input: {
186
+ version: string
187
+ device_type: string
188
+ release_notes?: string
189
+ download_url?: string | null
190
+ checksum?: string | null
191
+ file_size?: number
192
+ },
193
+ ): Promise<FirmwareVersionRow | null>
194
+ ```
195
+
196
+ #### `createFirmwareRouter(opts)`
197
+
198
+ Build the firmware router.
199
+
200
+ ```typescript
201
+ function createFirmwareRouter(opts: { requireDeviceToken: DeviceTokenMiddleware }): Router
202
+ ```
203
+
204
+ #### `createRolloutForOwner(userId, input)`
205
+
206
+ Create a rollout, materialize per-device tasks + `firmware_update`
207
+ device commands, and best-effort broadcast realtime notifications.
208
+
209
+ ```typescript
210
+ function createRolloutForOwner(
211
+ userId: string,
212
+ input: {
213
+ firmware_id: string
214
+ device_ids?: string[]
215
+ fleet_id?: string | null
216
+ strategy?: RolloutStrategy
217
+ },
218
+ ): Promise<
219
+ | { ok: true; rollout: FirmwareRolloutRow; targets: string[] }
220
+ | { ok: false; reason: 'not_found' | 'not_published' | 'no_targets' }
221
+ >
222
+ ```
223
+
224
+ #### `getFirmwareForOwner(userId, id)`
225
+
226
+ Owner-scoped firmware read — null when missing or not owned.
227
+
228
+ ```typescript
229
+ function getFirmwareForOwner(userId: string, id: string): Promise<FirmwareVersionRow | null>
230
+ ```
231
+
232
+ #### `isUuid(value)`
233
+
234
+ Whether a string is a syntactically valid UUID.
235
+
236
+ ```typescript
237
+ function isUuid(value: string): boolean
238
+ ```
239
+
240
+ #### `listFirmwareForOwner(userId, filters?)`
241
+
242
+ Owner-scoped firmware list.
243
+
244
+ ```typescript
245
+ function listFirmwareForOwner(
246
+ userId: string,
247
+ filters?: { device_type?: string; status?: string; page?: number; limit?: number },
248
+ ): Promise<FirmwareVersionRow[]>
249
+ ```
250
+
251
+ #### `listRolloutsForOwner(userId, filters?)`
252
+
253
+ Owner-scoped rollout list.
254
+
255
+ ```typescript
256
+ function listRolloutsForOwner(
257
+ userId: string,
258
+ filters?: { firmware_id?: string; status?: string; page?: number; limit?: number },
259
+ ): Promise<FirmwareRolloutRow[]>
260
+ ```
261
+
262
+ #### `publishFirmwareForOwner(userId, id)`
263
+
264
+ Flip a firmware version to published + stamp `released_at`.
265
+
266
+ ```typescript
267
+ function publishFirmwareForOwner(userId: string, id: string): Promise<FirmwareVersionRow | null>
268
+ ```
269
+
270
+ #### `recordRolloutDeviceStatus(opts)`
271
+
272
+ Record a per-device task status report. Updates the rollout's
273
+ completed/failed counters + progress percent; on success bumps the
274
+ device's `firmware_version` to the new release.
275
+
276
+ ```typescript
277
+ function recordRolloutDeviceStatus(opts: {
278
+ rolloutId: string
279
+ deviceId: string
280
+ ownerId: string
281
+ status: RolloutTaskStatus
282
+ errorMessage?: string | null
283
+ }): Promise<{ ok: true } | { ok: false; reason: 'task_not_found' }>
284
+ ```
285
+
286
+ #### `updateFirmwareForOwner(userId, id, patch)`
287
+
288
+ Patch a firmware version owned by the user.
289
+
290
+ ```typescript
291
+ function updateFirmwareForOwner(
292
+ userId: string,
293
+ id: string,
294
+ patch: Record<string, unknown>,
295
+ ): Promise<FirmwareVersionRow | null>
296
+ ```
297
+
298
+ ### Constants
299
+
300
+ #### `createFirmwareSchema`
301
+
302
+ Validator for creating a draft firmware version.
303
+
304
+ ```typescript
305
+ const createFirmwareSchema: z.ZodObject<
306
+ {
307
+ version: z.ZodString
308
+ device_type: z.ZodString
309
+ release_notes: z.ZodOptional<z.ZodString>
310
+ download_url: z.ZodOptional<z.ZodNullable<z.ZodString>>
311
+ checksum: z.ZodOptional<z.ZodNullable<z.ZodString>>
312
+ file_size: z.ZodOptional<z.ZodCoercedNumber<unknown>>
313
+ },
314
+ z.core.$strip
315
+ >
316
+ ```
317
+
318
+ #### `createRolloutSchema`
319
+
320
+ Validator for creating a rollout.
321
+
322
+ ```typescript
323
+ const createRolloutSchema: z.ZodObject<
324
+ {
325
+ firmware_id: z.ZodString
326
+ device_ids: z.ZodOptional<z.ZodArray<z.ZodString>>
327
+ fleet_id: z.ZodOptional<z.ZodNullable<z.ZodString>>
328
+ strategy: z.ZodOptional<
329
+ z.ZodEnum<{ immediate: 'immediate'; canary: 'canary'; gradual: 'gradual' }>
330
+ >
331
+ },
332
+ z.core.$strip
333
+ >
334
+ ```
335
+
336
+ #### `firmwareStatusSchema`
337
+
338
+ Allowed lifecycle states for a firmware version.
339
+
340
+ ```typescript
341
+ const firmwareStatusSchema: z.ZodEnum<{
342
+ draft: 'draft'
343
+ published: 'published'
344
+ deprecated: 'deprecated'
345
+ }>
346
+ ```
347
+
348
+ #### `listFirmwareQuerySchema`
349
+
350
+ Validator for the firmware-version list query params.
351
+
352
+ ```typescript
353
+ const listFirmwareQuerySchema: z.ZodObject<
354
+ {
355
+ device_type: z.ZodOptional<z.ZodString>
356
+ status: z.ZodOptional<
357
+ z.ZodEnum<{ draft: 'draft'; published: 'published'; deprecated: 'deprecated' }>
358
+ >
359
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>
360
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>
361
+ },
362
+ z.core.$strip
363
+ >
364
+ ```
365
+
366
+ #### `listRolloutsQuerySchema`
367
+
368
+ Validator for the rollout-list query params.
369
+
370
+ ```typescript
371
+ const listRolloutsQuerySchema: z.ZodObject<
372
+ {
373
+ firmware_id: z.ZodOptional<z.ZodString>
374
+ status: z.ZodOptional<
375
+ z.ZodEnum<{
376
+ pending: 'pending'
377
+ completed: 'completed'
378
+ failed: 'failed'
379
+ active: 'active'
380
+ canceled: 'canceled'
381
+ }>
382
+ >
383
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>
384
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>
385
+ },
386
+ z.core.$strip
387
+ >
388
+ ```
389
+
390
+ #### `rolloutDeviceStatusSchema`
391
+
392
+ Validator for a per-device rollout status report.
393
+
394
+ ```typescript
395
+ const rolloutDeviceStatusSchema: z.ZodObject<
396
+ {
397
+ status: z.ZodEnum<{
398
+ pending: 'pending'
399
+ in_progress: 'in_progress'
400
+ completed: 'completed'
401
+ failed: 'failed'
402
+ }>
403
+ error_message: z.ZodOptional<z.ZodNullable<z.ZodString>>
404
+ },
405
+ z.core.$strip
406
+ >
407
+ ```
408
+
409
+ #### `rolloutStatusSchema`
410
+
411
+ Allowed progress states for a firmware rollout.
412
+
413
+ ```typescript
414
+ const rolloutStatusSchema: z.ZodEnum<{
415
+ pending: 'pending'
416
+ completed: 'completed'
417
+ failed: 'failed'
418
+ active: 'active'
419
+ canceled: 'canceled'
420
+ }>
421
+ ```
422
+
423
+ #### `rolloutStrategySchema`
424
+
425
+ Allowed rollout delivery strategies.
426
+
427
+ ```typescript
428
+ const rolloutStrategySchema: z.ZodEnum<{
429
+ immediate: 'immediate'
430
+ canary: 'canary'
431
+ gradual: 'gradual'
432
+ }>
433
+ ```
434
+
435
+ #### `updateFirmwareSchema`
436
+
437
+ Validator for patching a firmware version (release notes, status, etc).
438
+
439
+ ```typescript
440
+ const updateFirmwareSchema: z.ZodObject<
441
+ {
442
+ release_notes: z.ZodOptional<z.ZodString>
443
+ download_url: z.ZodOptional<z.ZodNullable<z.ZodString>>
444
+ checksum: z.ZodOptional<z.ZodNullable<z.ZodString>>
445
+ file_size: z.ZodOptional<z.ZodCoercedNumber<unknown>>
446
+ status: z.ZodOptional<
447
+ z.ZodEnum<{ draft: 'draft'; published: 'published'; deprecated: 'deprecated' }>
448
+ >
449
+ },
450
+ z.core.$strip
451
+ >
452
+ ```
453
+
454
+ ## Injection Notes
455
+
456
+ ### Requirements
457
+
458
+ Peer dependencies:
459
+
460
+ - `@molecule/api-bonds-default-express` ^1.0.1
461
+ - `@molecule/api-database` ^1.0.1
462
+ - `@molecule/api-i18n` ^1.0.1
463
+ - `@molecule/api-logger` ^1.0.1
464
+ - `@molecule/api-middleware-validation` ^1.0.1
465
+ - `@molecule/api-realtime` ^1.0.1
466
+ - `express` ^5.0.0
467
+ - `zod` ^4.0.0
468
+
469
+ ### Runtime Dependencies
470
+
471
+ - `@molecule/api-bonds-default-express`
472
+ - `@molecule/api-database`
473
+ - `@molecule/api-i18n`
474
+ - `@molecule/api-logger`
475
+ - `@molecule/api-middleware-validation`
476
+ - `@molecule/api-realtime`
477
+ - `express`
478
+ - `zod`
479
+
480
+ The schema in `__setup__/firmware.sql` creates `firmware_versions`,
481
+ `firmware_rollouts`, and `firmware_update_tasks`. The resource also
482
+ reads/writes `iot_devices`, `fleets`, `fleet_memberships`,
483
+ `device_commands`, and `device_alerts` — those tables are owned by
484
+ the iot-device-manager template.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-resource-firmware",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Owner-scoped firmware versions + OTA rollouts: draft/publish/deprecate, per-device update tasks, realtime command dispatch, device-token-authenticated status reporting. Extracted from iot-device-manager flagship.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,8 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "README.md"
21
22
  ],
22
23
  "keywords": [
23
24
  "molecule",
@@ -28,6 +29,11 @@
28
29
  "resource"
29
30
  ],
30
31
  "license": "Apache-2.0",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/molecule-dev/molecule.git",
35
+ "directory": "packages/api/resources/firmware"
36
+ },
31
37
  "devDependencies": {
32
38
  "@types/express": "5.0.6",
33
39
  "@types/node": "26.1.2",
@@ -37,12 +43,12 @@
37
43
  "zod": "4.4.3"
38
44
  },
39
45
  "peerDependencies": {
40
- "@molecule/api-bonds-default-express": "^1.0.0",
41
- "@molecule/api-database": "^1.0.0",
42
- "@molecule/api-i18n": "^1.0.0",
43
- "@molecule/api-logger": "^1.0.0",
44
- "@molecule/api-middleware-validation": "^1.0.0",
45
- "@molecule/api-realtime": "^1.0.0",
46
+ "@molecule/api-bonds-default-express": "^1.0.1",
47
+ "@molecule/api-database": "^1.0.1",
48
+ "@molecule/api-i18n": "^1.0.1",
49
+ "@molecule/api-logger": "^1.0.1",
50
+ "@molecule/api-middleware-validation": "^1.0.1",
51
+ "@molecule/api-realtime": "^1.0.1",
46
52
  "express": "^5.0.0",
47
53
  "zod": "^4.0.0"
48
54
  }