@mikrojs/native 0.18.0 → 0.18.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.
- package/CMakeLists.txt +62 -1
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/result/native-result.node-shim.d.ts +3 -0
- package/dist/runtime/result/native-result.node-shim.d.ts.map +1 -0
- package/dist/runtime/result/native-result.node-shim.js +41 -0
- package/dist/runtime/result/native-result.node-shim.js.map +1 -0
- package/dist/runtime/result/types.d.ts +55 -0
- package/dist/runtime/result/types.d.ts.map +1 -0
- package/dist/runtime/result/types.js +2 -0
- package/dist/runtime/result/types.js.map +1 -0
- package/dist/runtime/schema/core.d.ts +115 -0
- package/dist/runtime/schema/core.d.ts.map +1 -0
- package/dist/runtime/schema/core.js +259 -0
- package/dist/runtime/schema/core.js.map +1 -0
- package/dist/runtime/schema/shared.d.ts +54 -0
- package/dist/runtime/schema/shared.d.ts.map +1 -0
- package/dist/runtime/schema/shared.js +489 -0
- package/dist/runtime/schema/shared.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/include/mikrojs/cbor_helpers.h +20 -0
- package/include/mikrojs/mem.h +11 -0
- package/include/mikrojs/mikrojs.h +2 -1
- package/include/mikrojs/ota_client.h +342 -0
- package/include/mikrojs/ota_config.h +100 -0
- package/include/mikrojs/ota_env.h +192 -0
- package/include/mikrojs/ota_js_hooks.h +71 -0
- package/include/mikrojs/ota_policy.h +131 -0
- package/include/mikrojs/ota_slots.h +47 -0
- package/include/mikrojs/sys_codec.h +61 -0
- package/package.json +7 -5
- package/prebuilds/darwin-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-arm64/mikrojs.napi.node +0 -0
- package/prebuilds/linux-x64/mikrojs.napi.node +0 -0
- package/runtime/internal.d.ts +22 -16
- package/runtime/kv/shared.ts +11 -5
- package/runtime/kv/types.ts +4 -4
- package/runtime/ota/client.ts +12 -51
- package/runtime/ota/config.ts +18 -0
- package/runtime/ota/ota.ts +28 -70
- package/runtime/ota/types.ts +220 -2
- package/runtime/schema/core.ts +539 -0
- package/runtime/schema/schema.ts +36 -314
- package/runtime/schema/shared.ts +494 -0
- package/runtime/schema/types.ts +84 -12
- package/scripts/bundle-runtime.js +33 -0
- package/scripts/gen-checkin-fixtures.js +323 -0
- package/src/builtins.cpp +7 -8
- package/src/fs.cpp +3 -0
- package/src/mem.cpp +38 -0
- package/src/mik_abort.cpp +8 -1
- package/src/mik_cbor.cpp +43 -5
- package/src/mik_inspect.cpp +128 -22
- package/src/mik_ota_client.cpp +1230 -0
- package/src/mik_ota_config.cpp +296 -0
- package/src/mik_ota_js_hooks.cpp +190 -0
- package/src/mik_ota_policy.cpp +419 -0
- package/src/mik_ota_slots.cpp +249 -0
- package/src/mik_repl.cpp +9 -3
- package/src/mik_result.cpp +3 -1
- package/src/mik_sys_codec.cpp +167 -0
- package/src/mikrojs.cpp +15 -0
- package/src/modules.cpp +32 -13
- package/runtime/ota/client-impl.ts +0 -590
- package/runtime/ota/policy.ts +0 -299
package/runtime/ota/ota.ts
CHANGED
|
@@ -1,77 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// `mikro/ota` — the low-level update surface, for an app that talks to its own
|
|
2
|
+
// registry. The built-in client (`mikro/ota/client`) covers the ordinary case
|
|
3
|
+
// and does not go through here.
|
|
4
|
+
//
|
|
5
|
+
// Every member is the C policy (src/mik_ota_policy.cpp): the retry budget, the
|
|
6
|
+
// trial gates and the staging session all live there, so this module and the
|
|
7
|
+
// built-in client cannot disagree about the crash-loop latch they share.
|
|
8
|
+
|
|
9
|
+
import {config} from 'mikro/ota/config'
|
|
10
|
+
import {
|
|
11
|
+
applyOffer,
|
|
12
|
+
bearer,
|
|
13
|
+
confirm,
|
|
14
|
+
parseOffer,
|
|
15
|
+
reconcile,
|
|
16
|
+
registry,
|
|
17
|
+
revert,
|
|
18
|
+
running,
|
|
19
|
+
} from 'native:mikro/ota_client'
|
|
4
20
|
|
|
5
|
-
import {createOta, type OtaStore} from './policy.js'
|
|
6
21
|
import type {Ota} from './types.js'
|
|
7
22
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
function put(key: string, value: string | number): void {
|
|
16
|
-
const r = sysSet(key, value)
|
|
17
|
-
// eslint-disable-next-line no-console
|
|
18
|
-
if (!r.ok) console.error(`ota: could not persist ${key}`, r.error)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const store: OtaStore = {
|
|
22
|
-
getUrl: () => {
|
|
23
|
-
const v = sysGet('ota.url')
|
|
24
|
-
return typeof v === 'string' ? v : undefined
|
|
25
|
-
},
|
|
26
|
-
setUrl: (url) => put('ota.url', url),
|
|
27
|
-
getAttempt: () => {
|
|
28
|
-
const v = sysGet('ota.att')
|
|
29
|
-
return typeof v === 'string' ? v : undefined
|
|
30
|
-
},
|
|
31
|
-
setAttempt: (checksum) => put('ota.att', checksum),
|
|
32
|
-
getTries: () => {
|
|
33
|
-
const v = sysGet('ota.tries')
|
|
34
|
-
return typeof v === 'number' ? v : 0
|
|
35
|
-
},
|
|
36
|
-
setTries: (n) => put('ota.tries', n),
|
|
37
|
-
getBad: () => {
|
|
38
|
-
const v = sysGet('ota.bad')
|
|
39
|
-
return typeof v === 'string' ? v : undefined
|
|
40
|
-
},
|
|
41
|
-
setBad: (checksum) => put('ota.bad', checksum),
|
|
42
|
-
getInFlight: () => sysGet('ota.inflight') === 1,
|
|
43
|
-
setInFlight: (value) => put('ota.inflight', value ? 1 : 0),
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Written as a pair to mik.sys by `mikro ota enroll`: the registry url and
|
|
47
|
-
// the device update key that authenticates against it.
|
|
48
|
-
function bearer(): string | undefined {
|
|
49
|
-
const v = sysGet('ota.updateKey')
|
|
50
|
-
return typeof v === 'string' ? v : undefined
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function registry(): string | undefined {
|
|
54
|
-
const v = sysGet('ota.registry')
|
|
55
|
-
return typeof v === 'string' ? v : undefined
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function readAppVersion(): string | undefined {
|
|
59
|
-
const r = readFile('/app/package.json', 'utf-8')
|
|
60
|
-
if (!r.ok) return undefined
|
|
61
|
-
try {
|
|
62
|
-
const pkg = JSON.parse(r.value) as {version?: unknown}
|
|
63
|
-
return typeof pkg.version === 'string' ? pkg.version : undefined
|
|
64
|
-
} catch {
|
|
65
|
-
return undefined
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const ota: Ota = createOta({
|
|
70
|
-
native,
|
|
71
|
-
store,
|
|
72
|
-
readAppVersion,
|
|
23
|
+
const ota: Ota = {
|
|
24
|
+
reconcile,
|
|
25
|
+
running,
|
|
26
|
+
parseOffer,
|
|
27
|
+
applyOffer,
|
|
28
|
+
confirm,
|
|
29
|
+
revert,
|
|
73
30
|
bearer,
|
|
74
31
|
registry,
|
|
75
|
-
|
|
32
|
+
config,
|
|
33
|
+
}
|
|
76
34
|
|
|
77
35
|
export {ota}
|
package/runtime/ota/types.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type {RequestError} from 'mikro/http/helpers'
|
|
2
|
+
|
|
3
|
+
import type {CborError} from '../cbor/types.js'
|
|
1
4
|
import type {Result} from '../result/types.js'
|
|
2
5
|
|
|
3
6
|
/** An update offered by a registry: what to fetch and how to verify it, nothing
|
|
@@ -114,6 +117,69 @@ export type ApplyOutcome =
|
|
|
114
117
|
* mismatch code, since it cannot tell a bad hash from bad bytes. */
|
|
115
118
|
export type OtaError = OtaWriteError | OtaBeginError | OtaInstallError | OtaDownloadError
|
|
116
119
|
|
|
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
|
+
/** The three config slots, mirroring the build's install slots: `current` is
|
|
139
|
+
* what the running build reads, `next` is staged with an offered build,
|
|
140
|
+
* `prev` is the rollback baseline while a trial is unresolved. */
|
|
141
|
+
export type ConfigSlot = 'current' | 'next' | 'prev'
|
|
142
|
+
|
|
143
|
+
/** A running-release config delivery on trial. `left` is the boot budget
|
|
144
|
+
* (each boot that reads config burns one); `read` records that the app has
|
|
145
|
+
* read the document since delivery. A completed check-in adopts the trial
|
|
146
|
+
* only once that is true, or an app that reads config only at boot could
|
|
147
|
+
* have a never-executed document adopted under it. */
|
|
148
|
+
export interface ConfigTrial {
|
|
149
|
+
left: number
|
|
150
|
+
read: boolean
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** A config document rolled back after a failed trial; reported on check-ins
|
|
154
|
+
* while it stands. `rev` names the failed document, and the client keeps
|
|
155
|
+
* echoing it as `configRev`, which is what stops the registry re-serving
|
|
156
|
+
* the same document until an operator changes the config. */
|
|
157
|
+
export interface ConfigErrorReport {
|
|
158
|
+
rev: string
|
|
159
|
+
message: string
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
declare global {
|
|
163
|
+
/**
|
|
164
|
+
* Merge your app's config type into this interface (next to the schema
|
|
165
|
+
* definition) to type `ota.config()` app-wide, with no type parameter at
|
|
166
|
+
* the call sites:
|
|
167
|
+
*
|
|
168
|
+
* ```ts
|
|
169
|
+
* declare global {
|
|
170
|
+
* interface OtaConfig extends InferRead<typeof ConfigSchema> {}
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*
|
|
174
|
+
* A global rather than a module augmentation because `mikro/ota` re-exports
|
|
175
|
+
* its types, and module augmentation does not merge through re-exports. An
|
|
176
|
+
* explicit `ota.config<T>()` still works and wins over the registration.
|
|
177
|
+
*/
|
|
178
|
+
interface OtaConfig {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type RegisteredConfig = keyof OtaConfig extends never ? unknown : OtaConfig
|
|
182
|
+
|
|
117
183
|
export interface Ota {
|
|
118
184
|
/** Report what happened to a previous update on this boot, and clear the report. */
|
|
119
185
|
reconcile(): InstallOutcome
|
|
@@ -122,8 +188,10 @@ export interface Ota {
|
|
|
122
188
|
/** Validate a registry value into an `Offer`, or `undefined` if unusable.
|
|
123
189
|
* `allowInsecure` (dev only) accepts an http build url instead of https. */
|
|
124
190
|
parseOffer(raw: unknown, opts?: {allowInsecure?: boolean}): Offer | undefined
|
|
125
|
-
/** Run the full update policy: skip checks
|
|
126
|
-
*
|
|
191
|
+
/** Run the full update policy: skip checks and the retry limit, download
|
|
192
|
+
* via the `download` callback, and verification. Compatibility is not
|
|
193
|
+
* re-checked: the registry selected this build for the reported firmware,
|
|
194
|
+
* and a mismatched archive fails its checksum or fails to load. */
|
|
127
195
|
applyOffer(
|
|
128
196
|
offer: Offer,
|
|
129
197
|
download: DownloadFn,
|
|
@@ -140,8 +208,158 @@ export interface Ota {
|
|
|
140
208
|
/** The registry url the device was enrolled against, written next to the
|
|
141
209
|
* update key at enrollment, or `undefined` on an un-enrolled device. */
|
|
142
210
|
registry(): string | undefined
|
|
211
|
+
/**
|
|
212
|
+
* The app's effective config: the running build's manifest defaults with the
|
|
213
|
+
* document the registry computed for this release spread over them, top level
|
|
214
|
+
* only. Always an object for a build that went through the tooling, so no
|
|
215
|
+
* `?? fallback` at the call sites; fields the schema gives no default are the
|
|
216
|
+
* ones that can be absent, and the read type marks exactly those optional.
|
|
217
|
+
*
|
|
218
|
+
* Reads current state on every call: it changes exactly when a check-in
|
|
219
|
+
* completes, and every call hands back a fresh object, so mutating one never
|
|
220
|
+
* reaches the cached defaults. The one thing held between calls is the last
|
|
221
|
+
* document that read successfully, which is served while, and only while, the
|
|
222
|
+
* store cannot answer. A read fails under heap pressure, and flipping a
|
|
223
|
+
* running app onto the defaults for a beat would re-configure its hardware
|
|
224
|
+
* mid-handshake. A cleared document removes the key and reads back as an
|
|
225
|
+
* honest absence, so a clear is never mistaken for a failure.
|
|
226
|
+
*
|
|
227
|
+
* The device never merges deeper than one level and never validates: every
|
|
228
|
+
* writer of the document validated it against this release's schema before
|
|
229
|
+
* writing, and the type comes from the same source definition
|
|
230
|
+
* (`ota.config<Config>()` with the schema's `InferRead`).
|
|
231
|
+
*
|
|
232
|
+
* Throws when there is nothing to serve at all: a build carrying no readable
|
|
233
|
+
* manifest and no stored document (deploy it with `mikro deploy`, or run
|
|
234
|
+
* `mikro dev`), or a store that failed before any read succeeded this runtime
|
|
235
|
+
* on a build whose manifest could not be parsed either. Both are transient or
|
|
236
|
+
* fixable states, never a value the app has to branch on.
|
|
237
|
+
*/
|
|
238
|
+
config<T = RegisteredConfig>(): T
|
|
143
239
|
}
|
|
144
240
|
|
|
145
241
|
/** The `mikro/ota` singleton. The runtime value is provided by the on-device
|
|
146
242
|
* builtin (or the sim stub); this declaration carries its type for hosts. */
|
|
147
243
|
export declare const ota: Ota
|
|
244
|
+
|
|
245
|
+
/* ── mikro/ota/client ──────────────────────────────────────────────────────
|
|
246
|
+
* The check-in client's surface. The implementation is C
|
|
247
|
+
* (src/mik_ota_client.cpp); these are the shapes it marshals across. */
|
|
248
|
+
|
|
249
|
+
export interface CheckOptions {
|
|
250
|
+
/** Budget for the check-in round trip. Default 10s. */
|
|
251
|
+
checkinTimeoutMs?: number
|
|
252
|
+
/** Budget for the build download. Separate from the check-in's because it is
|
|
253
|
+
* a total wallclock deadline that cancels the transfer mid-stream, and an
|
|
254
|
+
* image needs orders of magnitude more of it than a check-in body does.
|
|
255
|
+
* Default 5m. */
|
|
256
|
+
downloadTimeoutMs?: number
|
|
257
|
+
/** Require a completed check-in (via `ota.confirm()`, which the client fires
|
|
258
|
+
* itself) before an installed build is kept. Default true. */
|
|
259
|
+
requireConfirm?: boolean
|
|
260
|
+
/** Clean boots a trial may consume before an unconfirmed build reverts.
|
|
261
|
+
* A deep-sleep wake counts as a clean boot, so wake-cycle devices on flaky
|
|
262
|
+
* networks should raise this above the default 1. The same budget arms a
|
|
263
|
+
* delivered config document's trial. */
|
|
264
|
+
trialBoots?: number
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Runs after its round settles: after the check and any download, and before
|
|
268
|
+
* an auto-restart, so the network brought up in `beforeCheck` can go down.
|
|
269
|
+
*
|
|
270
|
+
* Whatever it returns is ignored, so it can call something that reports a
|
|
271
|
+
* Result without having to unwrap or discard it. A promise is awaited before
|
|
272
|
+
* the round is considered over. */
|
|
273
|
+
export type Teardown = () => unknown
|
|
274
|
+
|
|
275
|
+
/** What a `beforeCheck` hands back:
|
|
276
|
+
*
|
|
277
|
+
* - a **function**: the round runs, and that function runs after it
|
|
278
|
+
* - **nothing**: the round runs, with no teardown
|
|
279
|
+
* - an **`err`**: the round is skipped and retried at the failure interval,
|
|
280
|
+
* and no teardown runs, since unwinding a partial setup is the hook's own job
|
|
281
|
+
* - an **`ok`**: as its value, a teardown function or nothing
|
|
282
|
+
*
|
|
283
|
+
* Throwing has the same effect as returning an `err`. The bare-function form
|
|
284
|
+
* is there because wrapping a teardown in `ok()` is ceremony on the path that
|
|
285
|
+
* always succeeds; the Result form is what lets a failing setup hand its own
|
|
286
|
+
* error straight back. */
|
|
287
|
+
export type BeforeCheckResult = Teardown | void | Result<Teardown | void, unknown>
|
|
288
|
+
|
|
289
|
+
export interface WatchOptions extends CheckOptions {
|
|
290
|
+
/** Steady interval between rounds, end-of-round to start-of-next. Default
|
|
291
|
+
* 30m, floored at 30s: each round's TLS session leaves heap and socket
|
|
292
|
+
* residue that needs time to drain on small-heap devices, and the value
|
|
293
|
+
* may arrive from remote config, and the floor is what bounds the damage a
|
|
294
|
+
* mistyped document can do. */
|
|
295
|
+
checkinIntervalMs?: number
|
|
296
|
+
/** Delay before the first round. Default 5s. */
|
|
297
|
+
initialDelayMs?: number
|
|
298
|
+
/** Interval after a failed round, capped at `checkinIntervalMs`. Default 1m. */
|
|
299
|
+
retryAfterFailureMs?: number
|
|
300
|
+
/** Spread every scheduled sleep by ±10%, so a fleet that lost power together
|
|
301
|
+
* does not check in phase-locked forever. Default true; pass false for
|
|
302
|
+
* exact intervals (a demo watching for the update to land, a single
|
|
303
|
+
* device where the spread only delays it). */
|
|
304
|
+
jitter?: boolean
|
|
305
|
+
/** Bring the network up for one round. State shared with the teardown stays
|
|
306
|
+
* in this one scope. See {@link BeforeCheckResult} for what to hand back. */
|
|
307
|
+
beforeCheck?(): BeforeCheckResult | Promise<BeforeCheckResult>
|
|
308
|
+
/** Called after a completed round changes the effective config the running
|
|
309
|
+
* build reads: delivered or cleared by that round, or applied by this boot's
|
|
310
|
+
* install or rollback. Receives the new effective config. A watch loop is
|
|
311
|
+
* otherwise silent, so without this an app has to poll `ota.config()` to
|
|
312
|
+
* notice.
|
|
313
|
+
*
|
|
314
|
+
* Not called for a config staged alongside an offered build: that one
|
|
315
|
+
* applies at its trial boot, so the running app cannot read it yet. */
|
|
316
|
+
onConfig?(config: RegisteredConfig): void
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface Watcher {
|
|
320
|
+
/** Prevent future rounds and cancel the pending sleep. An in-flight round
|
|
321
|
+
* completes, but a build it stages no longer auto-restarts: it stays armed
|
|
322
|
+
* for the next natural reboot. */
|
|
323
|
+
stop(): void
|
|
324
|
+
/** Change the cadence without restarting the watcher, floored at 30s like
|
|
325
|
+
* {@link WatchOptions.checkinIntervalMs}. A wait already counting is re-timed
|
|
326
|
+
* from when it started, so a shorter interval brings the next round forward
|
|
327
|
+
* rather than waiting out the old one. The initial delay is left alone. */
|
|
328
|
+
setCheckinInterval(intervalMs: number): void
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Why an offered build was not armed. Each is the policy working as intended;
|
|
332
|
+
* the distinction is logged and returned because the app's next move differs. */
|
|
333
|
+
export type DeclineReason =
|
|
334
|
+
| 'trial-pending'
|
|
335
|
+
| 'current'
|
|
336
|
+
| 'abandoned'
|
|
337
|
+
| 'exhausted'
|
|
338
|
+
| 'download-failed'
|
|
339
|
+
| 'install-failed'
|
|
340
|
+
|
|
341
|
+
/** The check-in never completed. */
|
|
342
|
+
export type CheckError =
|
|
343
|
+
| RequestError
|
|
344
|
+
| CborError
|
|
345
|
+
| {name: 'Status'; status: number}
|
|
346
|
+
/** The response body was past the size the client will buffer. */
|
|
347
|
+
| {name: 'TooLarge'; message: string}
|
|
348
|
+
|
|
349
|
+
export type CheckResult =
|
|
350
|
+
/** Build downloaded, verified, and armed. The app restarts when ready. */
|
|
351
|
+
| {status: 'staged'; offer: Offer}
|
|
352
|
+
/** `configUpdated` reports that the running build's stored config changed
|
|
353
|
+
* since the app could last have read it: delivered or cleared this round,
|
|
354
|
+
* or applied by this boot's install or rollback. An app that read config
|
|
355
|
+
* early in the cycle knows to read it again. */
|
|
356
|
+
| {status: 'up-to-date'; configUpdated?: boolean}
|
|
357
|
+
/** An offer arrived but was not armed. */
|
|
358
|
+
| {status: 'not-staged'; reason: DeclineReason; error?: OtaError}
|
|
359
|
+
/** Transient: the check-in did not complete, so the running trial (if any)
|
|
360
|
+
* was not confirmed. */
|
|
361
|
+
| {status: 'failed'; error: CheckError}
|
|
362
|
+
/** The registry rejected the update key (HTTP 401). Permanent until
|
|
363
|
+
* re-enrollment over the cable. */
|
|
364
|
+
| {status: 'unauthorized'}
|
|
365
|
+
| {status: 'not-enrolled'}
|