@mikrojs/native 0.19.0 → 0.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/CMakeLists.txt CHANGED
@@ -61,6 +61,7 @@ set(MIKROJS_CORE_SOURCES
61
61
  src/mik_ota_js_hooks.cpp
62
62
  src/mik_ota_client.cpp
63
63
  src/mik_sys_codec.cpp
64
+ src/mik_watchdog.cpp
64
65
  )
65
66
 
66
67
  add_library(mikrojs STATIC
@@ -91,7 +92,7 @@ endif()
91
92
  include(cmake/mikrojs_bytecode.cmake)
92
93
  mikrojs_generate_bytecode(
93
94
  RUNTIME_DIR "${CMAKE_CURRENT_SOURCE_DIR}/runtime"
94
- MODULES abort cbor env result schema fs i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators ota ota/client ota/config pin pwm reader sleep spi sntp stdio stream sys test uart udp
95
+ MODULES abort cbor env result schema fs i2c kv/nvs kv/rtc kv/shared module neopixel observable observable/lazy observable/operators ota ota/client ota/config pin pwm reader sleep spi sntp stdio stream sys test uart udp watchdog
95
96
  MODULE_PREFIX "mikro"
96
97
  SYMBOL_PREFIX "mikro"
97
98
  TARGET gen_bytecode
@@ -222,6 +223,7 @@ if(BUILD_TESTING)
222
223
  test/ota_wire_fixtures_test.cpp
223
224
  test/schema_conformance_test.cpp
224
225
  test/sys_codec_test.cpp
226
+ test/watchdog_test.cpp
225
227
  )
226
228
 
227
229
  # ── Check-in wire fixtures ───────────────────────────────────────
@@ -54,8 +54,18 @@ typedef struct MIKConfig {
54
54
  char log_dir[64];
55
55
  uint32_t log_max_size;
56
56
  MIKLogFlush log_flush;
57
+ /* Watchdog budgets in ms, 0 = disabled. Mirror of the `watchdog` key in
58
+ * the host-side TS config. blocking: one event-loop turn holding the
59
+ * loop; feed: gap between watchdog.feed() calls; awake: time since boot. */
60
+ int blocking_timeout_ms;
61
+ int feed_timeout_ms;
62
+ int awake_timeout_ms;
57
63
  } MIKConfig;
58
64
 
65
+ /* Shipped blocking default. The ESP32 side static_asserts this against the
66
+ * hardware TWDT timeout so the two cannot drift out of order. */
67
+ #define MIK_WATCHDOG_BLOCKING_DEFAULT_MS 30000
68
+
59
69
  void MIK_DefaultConfig(MIKConfig* config);
60
70
  int MIK_LoadConfig(const char* base_path, MIKConfig* config);
61
71
  void MIK_SetConfig(MIKRuntime* mik_rt, const MIKConfig* config);
@@ -400,6 +410,17 @@ void MIK_ProtocolClose(void);
400
410
  * and must detach before freeing. */
401
411
  void MIK_ProtocolAttach(MIKRuntime* mik_rt);
402
412
 
413
+ /* Record what `mik_rt` was handed at boot, reported on MSG_READY. Call once,
414
+ * from the boot path, at the point the app's entry is about to be evaluated:
415
+ * the figures then describe the floor an app starts from rather than whatever
416
+ * happens to be free when a client connects. Later calls are ignored, so a
417
+ * supervisor that swaps runtimes can't overwrite the boot reading with a
418
+ * per-test one. MIK_ProtocolAttach calls this itself, which covers embedders
419
+ * that never call it; a boot path with a test supervisor should call it
420
+ * explicitly, before the supervisor allocates, so both modes report the same
421
+ * figure. */
422
+ void MIK_CaptureBootMemory(MIKRuntime* mik_rt);
423
+
403
424
  /* Unbind the current runtime. Call before MIK_FreeRuntime when swapping
404
425
  * runtimes mid-session. The session remains open. */
405
426
  void MIK_ProtocolDetach(void);
@@ -51,6 +51,18 @@ public:
51
51
 
52
52
  using MIKOtaDownloadFn = std::function<bool(MIKOtaUpdate& update, MIKOtaError* out_err)>;
53
53
 
54
+ /* Why the last offered build was not taken, in wire-string form, held in the
55
+ * store until a completed check-in delivers it. Persisted rather than held in
56
+ * memory (unlike the built-in client's copy) because an own-transport decline
57
+ * happens after that wake's check-in, so it must survive a deep sleep to make
58
+ * the next report. Field caps match the registry's checkin validation, which
59
+ * rejects a body whose lastDecline oversteps them. */
60
+ struct MIKOtaDeclineRecord {
61
+ char checksum[65] = {};
62
+ char reason[65] = {};
63
+ char detail[257] = {};
64
+ };
65
+
54
66
  class MIKOtaStore {
55
67
  public:
56
68
  explicit MIKOtaStore(const MIKOtaEnv* env) : env_(env) {}
@@ -75,6 +87,11 @@ public:
75
87
  bool GetInFlight() const;
76
88
  void SetInFlight(bool in_flight);
77
89
 
90
+ /* False when no record stands. */
91
+ bool GetDecline(MIKOtaDeclineRecord* out) const;
92
+ void SetDecline(const MIKOtaDeclineRecord& record);
93
+ void ClearDecline();
94
+
78
95
  private:
79
96
  const MIKOtaEnv* env_;
80
97
  };
@@ -19,6 +19,9 @@ typedef struct MIKPlatform {
19
19
  * deep sleep (hosts), where callers fall back to restart(). */
20
20
  void (*deep_sleep_us)(uint64_t us);
21
21
  void (*yield)(void);
22
+ /** Feed the hardware task watchdog for the calling task. NULL where the
23
+ * platform has none (hosts) or the watchdog is compiled out. */
24
+ void (*feed_watchdog)(void);
22
25
  size_t (*get_free_system_mem)(void);
23
26
  size_t (*get_min_free_system_mem)(void); /* All-time low watermark */
24
27
  size_t (*get_total_system_mem)(void);
@@ -80,6 +80,21 @@ struct MIKRuntime {
80
80
  * the host can still deploy/clean/--recover during the grace window,
81
81
  * then platform->restart() fires from MIK_Loop. */
82
82
  int64_t restart_at_us;
83
+ /* Watchdog state (mik_watchdog.cpp). All per-runtime, no atomics needed.
84
+ * blocking_start_us == 0 means "stamp on the next interrupt poll"; the
85
+ * handler switches itself off when it fires and back on at mik__blocking_begin. */
86
+ int64_t blocking_start_us;
87
+ bool blocking_armed;
88
+ /* Set when the limit is hit, consumed by the error dump that reports it. */
89
+ bool blocking_tripped;
90
+ /* Feed deadline: last feed()/re-stamp; feed_armed clears when it fires. */
91
+ int64_t feed_last_us;
92
+ bool feed_armed;
93
+ /* Awake budget: deploy-pause time credited against get_boot_us(). */
94
+ int64_t awake_pause_offset_us;
95
+ bool awake_armed;
96
+ /* Boot-clock stamp of the current pause; 0 = not paused. */
97
+ int64_t pause_start_us;
83
98
  const char* fs_base_path;
84
99
  const char* fs_root; /* Sandbox root for mikrojs/fs operations (separate from module resolution) */
85
100
  size_t fs_limit; /* Max bytes for fs_root (0 = unlimited) */
@@ -296,6 +311,31 @@ JSModuleDef* mik__udp_init(JSContext* ctx);
296
311
  JSModuleDef* mik__observable_init(JSContext* ctx);
297
312
  void mik__observable_dispatch_free(struct MIKRuntime* mik_rt);
298
313
 
314
+ /* Watchdog (mik_watchdog.cpp). */
315
+ /* Start a fresh blocking budget: top of each MIK_Loop pass, eval entry
316
+ * points, REPL eval, and on return from a deliberately blocking native
317
+ * (lightSleep). One store; the interrupt handler stamps on its next poll. */
318
+ void mik__blocking_begin(struct MIKRuntime* mik_rt);
319
+ /* JS_SetInterruptHandler callback; opaque is the MIKRuntime*. */
320
+ int mik__watchdog_interrupt(JSRuntime* rt, void* opaque);
321
+ /* If the blocking deadline was hit, log the watchdog line and clear the
322
+ * flag. Returns whether it did; callers print the error trace after. */
323
+ bool mik__watchdog_report_blocking(struct MIKRuntime* mik_rt);
324
+ /* Switch feed/awake on from mik_rt->config. Called once the config is set. */
325
+ void mik__watchdog_arm(struct MIKRuntime* mik_rt);
326
+ /* Feed and awake checks. Runs at the top of MIK_Loop after the restart_at_us
327
+ * branch; a feed miss throws an uncatchable error into mik_rt->ctx, on an
328
+ * awake overrun calls MIK_Stop directly. */
329
+ void mik__watchdog_check(struct MIKRuntime* mik_rt);
330
+ /* On return from lightSleep: fresh blocking budget and feed window. */
331
+ void mik__watchdog_wake(struct MIKRuntime* mik_rt);
332
+ /* Deploy/REPL pause bookkeeping: re-stamp the feed clock and credit the
333
+ * paused span to the awake budget. */
334
+ void mik__watchdog_pause_begin(struct MIKRuntime* mik_rt);
335
+ void mik__watchdog_pause_end(struct MIKRuntime* mik_rt);
336
+ /* Registers native:mikro/watchdog (feed) on ctx. */
337
+ void mik__watchdog_init(JSContext* ctx);
338
+
299
339
  bool mik__repl_is_evaluating(void);
300
340
 
301
341
  /* REPL protocol mode (mik_repl.cpp) — used by mik_console.cpp, mik_stdio.cpp */
@@ -68,6 +68,11 @@ void mik_assert(const struct AssertionInfo info);
68
68
  }
69
69
 
70
70
  void mik_call_handler(JSContext* ctx, JSValue func, int argc, JSValue* argv);
71
+ /* Print one always-visible error line the way console.error does: an error
72
+ * frame in protocol mode, stderr otherwise. platform->log is filtered per
73
+ * tag on the device and silent by default, so lines that precede a reboot
74
+ * (watchdog lines, panic actions) go through here instead. */
75
+ void mik__print_error_line(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
71
76
  void mik_dump_error(JSContext* ctx);
72
77
  void mik_dump_error1(JSContext* ctx, JSValue exception_val);
73
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikrojs/native",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Mikro.js C++ runtime library and Node.js native addon",
5
5
  "keywords": [
6
6
  "esp32",
@@ -80,21 +80,22 @@
80
80
  "./runtime/test/types": "./runtime/test/types.ts",
81
81
  "./runtime/uart/types": "./runtime/uart/types.ts",
82
82
  "./runtime/udp/types": "./runtime/udp/types.ts",
83
+ "./runtime/watchdog/types": "./runtime/watchdog/types.ts",
83
84
  "./runtime/wifi/types": "./runtime/wifi/types.ts"
84
85
  },
85
86
  "dependencies": {
86
87
  "cmake-js": "^8.0.0",
87
88
  "node-addon-api": "^8.7.0",
88
89
  "node-gyp-build": "^4.8.4",
89
- "@mikrojs/quickjs": "0.19.0"
90
+ "@mikrojs/quickjs": "0.20.0"
90
91
  },
91
92
  "devDependencies": {
92
93
  "@swc/core": "^1.15.30",
93
94
  "@types/node": "^24.12.2",
94
95
  "esbuild": "^0.28.0",
95
96
  "terser": "^5.46.2",
96
- "@mikrojs/registry": "0.19.0",
97
- "@mikrojs/schema": "0.19.0"
97
+ "@mikrojs/registry": "0.20.0",
98
+ "@mikrojs/schema": "0.20.0"
98
99
  },
99
100
  "engines": {
100
101
  "node": ">=24.0.0"
@@ -170,6 +170,10 @@ declare module 'native:mikro/pin' {
170
170
  export function analogReadMillivolts(pin: number, attenuation: number): Result<number, PinError>
171
171
  }
172
172
 
173
+ declare module 'native:mikro/watchdog' {
174
+ export function feed(): void
175
+ }
176
+
173
177
  declare module 'native:mikro/sleep' {
174
178
  import type {DeepWakeupSources, LightWakeupSources} from '@mikrojs/native/runtime/sleep/types'
175
179
 
@@ -580,12 +584,8 @@ declare module 'native:mikro/ota_client' {
580
584
 
581
585
  // The mikro/ota policy surface.
582
586
  export const reconcile: Ota['reconcile']
583
- export const running: Ota['running']
584
- export const parseOffer: Ota['parseOffer']
585
587
  export const applyOffer: Ota['applyOffer']
586
- export const parseConfig: Ota['parseConfig']
587
- export const applyConfig: Ota['applyConfig']
588
- export const configState: Ota['configState']
588
+ export const decline: Ota['decline']
589
589
  export const report: Ota['report']
590
590
  export const settle: Ota['settle']
591
591
  export const confirm: Ota['confirm']
@@ -8,18 +8,14 @@
8
8
 
9
9
  import {config} from 'mikro/ota/config'
10
10
  import {
11
- applyConfig,
12
11
  applyOffer,
13
12
  bearer,
14
- configState,
15
13
  confirm,
16
- parseConfig,
17
- parseOffer,
14
+ decline,
18
15
  reconcile,
19
16
  registry,
20
17
  report,
21
18
  revert,
22
- running,
23
19
  settle,
24
20
  } from 'native:mikro/ota_client'
25
21
 
@@ -27,17 +23,13 @@ import type {Ota} from './types.js'
27
23
 
28
24
  const ota: Ota = {
29
25
  reconcile,
30
- running,
31
- parseOffer,
32
26
  applyOffer,
33
27
  confirm,
34
28
  revert,
35
29
  bearer,
36
30
  registry,
37
31
  config,
38
- parseConfig,
39
- applyConfig,
40
- configState,
32
+ decline,
41
33
  report,
42
34
  settle,
43
35
  }
@@ -6,7 +6,8 @@ import type {Result} from '../result/types.js'
6
6
  /** An update offered by a registry: what to fetch and how to verify it, nothing
7
7
  * more. Whether this device should take it is the registry's decision, made
8
8
  * against the firmware and bytecode version the device reports at check-in and
9
- * the whole set of builds it holds. Validate untrusted input with `parseOffer`. */
9
+ * the whole set of builds it holds. `settle()` validates the untrusted
10
+ * response fields into one. */
10
11
  export interface Offer {
11
12
  /** https URL of the .tgz build */
12
13
  url: string
@@ -117,24 +118,6 @@ export type ApplyOutcome =
117
118
  * mismatch code, since it cannot tell a bad hash from bad bytes. */
118
119
  export type OtaError = OtaWriteError | OtaBeginError | OtaInstallError | OtaDownloadError
119
120
 
120
- /** One stored config document: the complete effective config computed and
121
- * validated by the registry (or by the CLI at cable-seed time), the opaque
122
- * token that identifies it, and the release version it was computed for.
123
- * The device stores and returns it without understanding it: validation is
124
- * the writer's job, and the registry already ships the code. */
125
- export interface StoredConfig {
126
- /** Opaque registry-issued token echoed as `configRev` on check-ins. The
127
- * registry serves its document whenever the echo differs from its own
128
- * current rev, so a document it does not recognize is replaced. */
129
- rev?: string
130
- /** The release version this document was computed for. A document stamped
131
- * for another version is ignored by `ota.config()`. */
132
- version: string
133
- /** The served document: the deviation overlay the read resolves over the
134
- * build's manifest defaults. */
135
- doc?: unknown
136
- }
137
-
138
121
  /** The three config slots, mirroring the build's install slots: `current` is
139
122
  * what the running build reads, `next` is staged with an offered build,
140
123
  * `prev` is the rollback baseline while a trial is unresolved. */
@@ -166,28 +149,13 @@ export type ConfigWrite =
166
149
  * included), or it was a clear with nothing to clear. */
167
150
  | 'unchanged'
168
151
  /** Nothing was written, and the reason is transient: the store could not
169
- * answer, or the running version could not be read. Keep echoing the rev
170
- * from `configState()` so the document is served again, rather than the rev
171
- * of the document that did not land. */
152
+ * answer, or the running version could not be read. The next `report()`
153
+ * still echoes the held rev, so the document is served again, rather than
154
+ * the rev of the document that did not land. */
172
155
  | 'failed'
173
- /** Not a usable config document. Validate with `parseConfig` first to find
174
- * out before the delivery, and log what the wire actually carried. */
156
+ /** Not a usable config document; log what the wire actually carried. */
175
157
  | 'invalid'
176
158
 
177
- /** What a check-in body owes the registry about config, for a client that
178
- * builds its own. */
179
- export interface ConfigState {
180
- /** The rev to send as `configRev`: the registry serves its document whenever
181
- * this differs from its own current rev. Absent when the device holds no
182
- * document. After a rolled-back trial this is the FAILED document's rev,
183
- * which is what stops the registry serving it again. */
184
- rev?: string
185
- /** A document that failed its trial and was rolled back, reported until it
186
- * is replaced. Send it as `configError`, or an operator has no way to see
187
- * that the document they published took the device down. */
188
- error?: ConfigErrorReport
189
- }
190
-
191
159
  /** A config document rolled back after a failed trial; reported on check-ins
192
160
  * while it stands. `rev` names the failed document, and the client keeps
193
161
  * echoing it as `configRev`, which is what stops the registry re-serving
@@ -197,6 +165,19 @@ export interface ConfigErrorReport {
197
165
  message: string
198
166
  }
199
167
 
168
+ /** Why the last offered build was not taken. Without it a registry that waits
169
+ * for `running.checksum` to turn into the offered checksum cannot tell a
170
+ * device still working through a download from one that has permanently
171
+ * stopped trying. */
172
+ export interface DeclineReport {
173
+ /** The declined build, 64 lowercase hex characters. */
174
+ checksum: string
175
+ /** A module outcome ({@link DeclineReason}) or the app's own word for a
176
+ * reason only it can see, at most 64 characters. */
177
+ reason: DeclineReason | (string & {})
178
+ detail?: string
179
+ }
180
+
200
181
  /** The check-in body a device owes its registry, as `ota.report()` builds it.
201
182
  * Field shapes match the wire, so a client (or the proxy behind it) can
202
183
  * forward fields verbatim into `POST /api/v1/checkin`. */
@@ -217,10 +198,17 @@ export interface CheckinReport {
217
198
  /** Why a previous install failed. Present after a `reconcile()` that found
218
199
  * one, until a `settle()` marks it delivered. */
219
200
  lastInstall?: Diagnostic
220
- /** The held document's rev to echo; see {@link ConfigState.rev}. */
201
+ /** The held document's rev to echo: the registry serves its document
202
+ * whenever this differs from its own current rev. After a rolled-back
203
+ * trial this is the FAILED document's rev, which is what stops the
204
+ * registry serving it again. */
221
205
  configRev?: string
222
- /** A rolled-back document to report; see {@link ConfigState.error}. */
206
+ /** A rolled-back document to report; see {@link ConfigErrorReport}. */
223
207
  configError?: ConfigErrorReport
208
+ /** Why the last offered build was not taken. Present after `applyOffer`
209
+ * declined one or the app recorded its own via `decline()`, until a
210
+ * `settle()` marks it delivered. */
211
+ lastDecline?: DeclineReport
224
212
  }
225
213
 
226
214
  /** What `ota.settle()` took from a completed check-in's response. */
@@ -260,15 +248,14 @@ export type RegisteredConfig = keyof OtaConfig extends never ? unknown : OtaConf
260
248
  export interface Ota {
261
249
  /** Report what happened to a previous update on this boot, and clear the report. */
262
250
  reconcile(): InstallOutcome
263
- /** The build currently executing, read from the live app. */
264
- running(): RunningBuild
265
- /** Validate a registry value into an `Offer`, or `undefined` if unusable.
266
- * `allowInsecure` (dev only) accepts an http build url instead of https. */
267
- parseOffer(raw: unknown, opts?: {allowInsecure?: boolean}): Offer | undefined
268
251
  /** Run the full update policy: skip checks and the retry limit, download
269
252
  * via the `download` callback, and verification. Compatibility is not
270
253
  * re-checked: the registry selected this build for the reported firmware,
271
- * and a mismatched archive fails its checksum or fails to load. */
254
+ * and a mismatched archive fails its checksum or fails to load.
255
+ *
256
+ * An offer it does not stage is recorded for the registry the way
257
+ * `decline()` records one, except `'current'` and `'trial-pending'`,
258
+ * which are not declines. */
272
259
  applyOffer(
273
260
  offer: Offer,
274
261
  download: DownloadFn,
@@ -318,50 +305,29 @@ export interface Ota {
318
305
  */
319
306
  config<T = RegisteredConfig>(): T
320
307
  /**
321
- * Validate a config document a client received over its own transport, or
322
- * `undefined` when it cannot be used. What `parseOffer` is to an offer.
323
- *
324
- * A usable document is an object with a non-empty `version` (the release it
325
- * was computed for, which decides where `applyConfig` puts it), an optional
326
- * `rev` short enough to echo intact, and a `doc` that is an object or absent.
327
- * An absent `doc` is the clear, not a malformed document.
328
- *
329
- * Whether the document survives CBOR is settled by `applyConfig`, which is
330
- * where the stored bytes are made.
331
- */
332
- parseConfig(raw: unknown): StoredConfig | undefined
333
- /**
334
- * Store a config document, for a client that received one over its own
335
- * transport. The built-in client covers the ordinary case and does not go
336
- * through here.
337
- *
338
- * The `version` stamp decides where it lands: stamped for the running
339
- * release it is applied, and the document it replaces is kept as the
340
- * rollback baseline; stamped for another it is staged for the build it
341
- * names, to apply when that build installs. The return value says which
342
- * happened, and says when nothing did.
308
+ * Record why an offered build was not taken, for the next `report()` to
309
+ * carry as `lastDecline` and a later `settle()` to mark delivered — the
310
+ * same lifecycle as `lastInstall`. `applyOffer` records its own outcomes,
311
+ * so this is for a reason only the app can see: its own download budget
312
+ * spent, a build its policy refuses. Without a recorded decline the
313
+ * registry cannot tell a device still working through a download from one
314
+ * that has stopped, and shows the update pending forever.
343
315
  *
344
- * A delivery to the running release arms a trial. Each boot whose first
345
- * `config()` read serves the document burns one of `trialBoots`, and the
346
- * budget spent with no `confirm()` in between restores the previous
347
- * document. On a wake-cycle device every wake is a boot, so raise
348
- * `trialBoots` above the default when a check-in can plausibly fail a few
349
- * cycles in a row.
350
- */
351
- applyConfig(config: StoredConfig, options?: {trialBoots?: number}): ConfigWrite
352
- /**
353
- * What the device owes its registry about config: the rev to echo, and a
354
- * rolled-back document to report. A client that builds its own check-in body
355
- * needs both. Without the echo the registry re-serves the same document on
356
- * every check-in; without the report a document that took the device down is
357
- * re-served forever and nobody is told.
316
+ * Persisted on the device, so it survives a deep sleep: on a wake-cycle
317
+ * device the decline always lands after that wake's check-in, and the next
318
+ * wake's `report()` is its first chance to travel. Recording again
319
+ * overwrites; only the last decline stands. Throws a `TypeError` on a
320
+ * checksum that is not 64 lowercase hex characters or a `reason` past 64
321
+ * characters, since the registry would reject the whole check-in body;
322
+ * `detail` is free text and is cut to 256 characters instead.
358
323
  */
359
- configState(): ConfigState
324
+ decline(checksum: string, reason: DeclineReason | (string & {}), detail?: string): void
360
325
  /**
361
326
  * The check-in body the device owes its registry, assembled: identity,
362
- * `running()`, the name pair, free storage, the pending `lastInstall`
363
- * report, and what `configState()` resolves. One call instead of gathering
364
- * the fields by hand, and the same facts the built-in client sends.
327
+ * the running build, the name pair, free storage, the pending `lastInstall`
328
+ * report, a recorded `lastDecline`, and the config echo. One
329
+ * call instead of gathering the fields by hand, and the same facts the
330
+ * built-in client sends.
365
331
  *
366
332
  * Call `reconcile()` first, on every boot: it is what surfaces the
367
333
  * `lastInstall` report this reads (`settle()` marks it delivered).
@@ -370,9 +336,12 @@ export interface Ota {
370
336
  /**
371
337
  * Take a COMPLETED check-in's response, whole: confirm the running trial
372
338
  * and a read config document's trial (exactly `confirm()`), adopt a
373
- * delivered name pair, store a delivered config document (exactly
374
- * `applyConfig`, with `trialBoots`), and validate the offer fields (exactly
375
- * `parseOffer(raw)`, with `allowInsecure`). An empty or null response is
339
+ * delivered name pair, store a delivered config document, and validate the
340
+ * offer fields (`allowInsecure`, dev only, accepts an http build url). A
341
+ * stored document arms a trial of `trialBoots` boots: each boot whose first
342
+ * `config()` read serves it burns one, and the budget spent with no
343
+ * `confirm()` in between restores the previous document, so raise it above
344
+ * the default on a wake-cycle device. An empty or null response is
376
345
  * the registry's quiet round: nothing to deliver, and the confirm still
377
346
  * happens, which is the point of calling. A response of any other shape
378
347
  * never decoded: a captive portal handed back HTML, or a proxy sent an