@mikrojs/firmware 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.
@@ -28,6 +28,7 @@
28
28
 
29
29
  #include "mikrojs/app_store.h"
30
30
  #include "mikrojs/mikrojs.h"
31
+ #include "mikrojs/ota_env.h"
31
32
  #include "mikrojs/platform.h"
32
33
  #include "mikrojs_esp32.h"
33
34
  #include "quickjs.h"
@@ -1004,49 +1005,45 @@ bool valid_checksum(const char* s) {
1004
1005
  return i == 64;
1005
1006
  }
1006
1007
 
1007
- // stageBegin(checksum, size) -> { ok, resumeOffset } | { ok:false, error }
1008
- JSValue js_stage_begin(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
1009
- if (argc < 2) return err_obj(ctx, "missing checksum/size");
1010
- const char* checksum = JS_ToCString(ctx, argv[0]);
1011
- if (!checksum) {
1012
- // JS_ToCString throws on a value with a throwing toString. Clear it
1013
- // rather than strand it on the context for some unrelated later
1014
- // operation to surface, the same way the size path below does.
1015
- JS_FreeValue(ctx, JS_GetException(ctx));
1016
- return err_obj(ctx, "checksum not a string");
1008
+ // ── Staging cores ────────────────────────────────────────────────────────────
1009
+ // The install machinery, with no JSContext in sight. Two callers sit on top: the
1010
+ // native:mikro/ota JS bindings below, and the MIKOtaEnv install ops the native
1011
+ // OTA client drives (mik__ota_fill_install_ops). One implementation, so the two
1012
+ // can never drift on something as unforgiving as install safety.
1013
+
1014
+ struct CoreErr {
1015
+ const char* msg = nullptr;
1016
+ OtaErr kind = OtaErr::kTransient;
1017
+ };
1018
+
1019
+ bool core_fail(CoreErr* e, const char* msg, OtaErr kind = OtaErr::kTransient) {
1020
+ if (e) {
1021
+ e->msg = msg;
1022
+ e->kind = kind;
1017
1023
  }
1024
+ return false;
1025
+ }
1026
+
1027
+ bool core_stage_begin(const char* checksum, int64_t size, int64_t* out_resume, CoreErr* e) {
1028
+ if (!checksum) return core_fail(e, "checksum not a string");
1018
1029
  if (!valid_checksum(checksum)) {
1019
- JS_FreeCString(ctx, checksum);
1020
1030
  // kTransient, not the default: the build is fine, the offer is
1021
1031
  // malformed, so this must not count against the build's retry budget.
1022
- return err_obj_kind(ctx, "checksum must be 64 lowercase hex characters",
1023
- OtaErr::kTransient);
1024
- }
1025
- int64_t size = 0;
1026
- if (JS_ToInt64(ctx, &size, argv[1]) || size <= 0) {
1027
- JS_FreeCString(ctx, checksum);
1028
- // JS_ToInt64 throws on a value with a throwing valueOf. We report through
1029
- // the result object, so clear it rather than strand it on the context for
1030
- // some unrelated later operation to surface.
1031
- JS_FreeValue(ctx, JS_GetException(ctx));
1032
- return err_obj(ctx, "invalid size");
1032
+ return core_fail(e, "checksum must be 64 lowercase hex characters", OtaErr::kTransient);
1033
1033
  }
1034
- // Reject an impossible size here, while it is still int64: stgSize is u32,
1035
- // so a bogus offer above 4 GB would otherwise truncate into a small cap.
1034
+ if (size <= 0) return core_fail(e, "invalid size");
1035
+ // Reject an impossible size while it is still int64: stgSize is u32, so a
1036
+ // bogus offer above 4 GB would otherwise truncate into a small cap.
1036
1037
  long fs_total = 0;
1037
1038
  long fs_free = 0;
1038
1039
  if (fs_space(&fs_total, &fs_free) && size > fs_total) {
1039
- JS_FreeCString(ctx, checksum);
1040
- return err_obj(ctx, "offered build is larger than the filesystem");
1040
+ return core_fail(e, "offered build is larger than the filesystem");
1041
1041
  }
1042
1042
 
1043
1043
  stage_close(); // flush any prior staging file before stat/unlink below
1044
1044
 
1045
1045
  nvs_handle_t h;
1046
- if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) {
1047
- JS_FreeCString(ctx, checksum);
1048
- return err_obj(ctx, "nvs open failed");
1049
- }
1046
+ if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return core_fail(e, "nvs open failed");
1050
1047
 
1051
1048
  // Resume only if the in-progress staging file is for the same checksum+size
1052
1049
  // and hasn't already grown past the declared size.
@@ -1064,35 +1061,25 @@ JSValue js_stage_begin(JSContext* ctx, JSValueConst, int argc, JSValueConst* arg
1064
1061
  nvs_commit(h);
1065
1062
  }
1066
1063
  nvs_close(h);
1067
- JS_FreeCString(ctx, checksum);
1068
1064
 
1069
1065
  g_stage_cap = (uint32_t)size;
1070
1066
  g_stage_have = resume;
1071
-
1072
- JSValue o = ok_obj(ctx);
1073
- JS_SetPropertyStr(ctx, o, "resumeOffset", JS_NewInt64(ctx, resume));
1074
- return o;
1067
+ if (out_resume) *out_resume = resume;
1068
+ return true;
1075
1069
  }
1076
1070
 
1077
- // stageWrite(bytes) -> { ok } | { ok:false, error, kind? }
1078
- JSValue js_stage_write(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
1079
- if (argc < 1) return err_obj(ctx, "missing bytes");
1080
- size_t len = 0;
1081
- const uint8_t* data = JS_GetUint8Array(ctx, &len, argv[0]);
1082
- if (!data) {
1083
- JS_FreeValue(ctx, JS_GetException(ctx)); // JS_GetUint8Array throws; we report via err_obj
1084
- return err_obj(ctx, "bytes not a Uint8Array");
1085
- }
1086
- if (len == 0) return ok_obj(ctx);
1071
+ bool core_stage_write(const uint8_t* data, size_t len, CoreErr* e) {
1072
+ if (!data) return core_fail(e, "bytes not a Uint8Array");
1073
+ if (len == 0) return true;
1087
1074
 
1088
1075
  // Enforce the size cap from stageBegin so a runaway download can't fill flash.
1089
1076
  if (g_stage_cap > 0 && (uint64_t)g_stage_have + len > g_stage_cap) {
1090
- return err_obj_kind(ctx, "staged write exceeds declared size", OtaErr::kTransient);
1077
+ return core_fail(e, "staged write exceeds declared size", OtaErr::kTransient);
1091
1078
  }
1092
1079
 
1093
1080
  if (!g_stage_file) {
1094
1081
  g_stage_file = fopen(kStaging, "ab");
1095
- if (!g_stage_file) return err_obj_kind(ctx, "open staging file", OtaErr::kTransient);
1082
+ if (!g_stage_file) return core_fail(e, "open staging file", OtaErr::kTransient);
1096
1083
  }
1097
1084
  size_t wrote = fwrite(data, 1, len, g_stage_file);
1098
1085
  if (wrote != len) {
@@ -1105,27 +1092,13 @@ JSValue js_stage_write(JSContext* ctx, JSValueConst, int argc, JSValueConst* arg
1105
1092
  stage_close();
1106
1093
  long actual = file_size(kStaging);
1107
1094
  g_stage_have = actual < 0 ? g_stage_have + (long)wrote : actual;
1108
- return err_obj_kind(ctx, "write staging file (disk full?)", OtaErr::kTransient);
1095
+ return core_fail(e, "write staging file (disk full?)", OtaErr::kTransient);
1109
1096
  }
1110
1097
  g_stage_have += (long)len;
1111
- return ok_obj(ctx);
1098
+ return true;
1112
1099
  }
1113
1100
 
1114
- // stageFinish(trialBoots, requireConfirm, installNow)
1115
- // -> { ok } | { ok:false, error, kind }
1116
- // Verifies SHA-256 + size over the whole staged file, then either installs in
1117
- // place now (installNow) or marks a verified build staged-for-install so the
1118
- // next boot's reconcile installs it on a clean heap.
1119
- JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
1120
- int64_t trial_boots = 1;
1121
- if (argc >= 1 && JS_ToInt64(ctx, &trial_boots, argv[0])) {
1122
- // A throwing valueOf leaves trial_boots indeterminate and an exception
1123
- // pending; we report through the result object, so clear it and default.
1124
- JS_FreeValue(ctx, JS_GetException(ctx));
1125
- trial_boots = 1;
1126
- }
1127
- bool require_confirm = argc >= 2 && JS_ToBool(ctx, argv[1]);
1128
- bool install_now = argc >= 3 && JS_ToBool(ctx, argv[2]);
1101
+ bool core_stage_finish(int64_t trial_boots, bool require_confirm, bool install_now, CoreErr* e) {
1129
1102
  if (trial_boots < 0) trial_boots = 0;
1130
1103
  // A confirm-gated trial needs at least one boot to run in. Boot reconcile
1131
1104
  // evaluates the trial before the app loads, so trialBoots:0 with confirm set
@@ -1135,12 +1108,12 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
1135
1108
  if (trial_boots > 255) trial_boots = 255;
1136
1109
 
1137
1110
  if (!stage_close()) {
1138
- return err_obj_kind(ctx, "write staging file (disk full?)", OtaErr::kTransient);
1111
+ return core_fail(e, "write staging file (disk full?)", OtaErr::kTransient);
1139
1112
  }
1140
1113
 
1141
1114
  nvs_handle_t h;
1142
1115
  if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) {
1143
- return err_obj_kind(ctx, "nvs open failed", OtaErr::kTransient);
1116
+ return core_fail(e, "nvs open failed", OtaErr::kTransient);
1144
1117
  }
1145
1118
  char want_chk[96];
1146
1119
  bool has_chk = nvs_str(h, kKeyStgChk, want_chk, sizeof(want_chk));
@@ -1148,32 +1121,32 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
1148
1121
 
1149
1122
  if (!has_chk) {
1150
1123
  nvs_close(h);
1151
- return err_obj_kind(ctx, "no staged build", OtaErr::kTransient);
1124
+ return core_fail(e, "no staged build", OtaErr::kTransient);
1152
1125
  }
1153
1126
  long have = file_size(kStaging);
1154
1127
  if (have < 0) {
1155
1128
  nvs_close(h);
1156
- return err_obj_kind(ctx, "staging file missing", OtaErr::kTransient);
1129
+ return core_fail(e, "staging file missing", OtaErr::kTransient);
1157
1130
  }
1158
1131
  if ((uint32_t)have != want_size) {
1159
1132
  nvs_close(h);
1160
- return err_obj_kind(ctx, "staged size mismatch", OtaErr::kCorrupt);
1133
+ return core_fail(e, "staged size mismatch", OtaErr::kCorrupt);
1161
1134
  }
1162
1135
  char got_chk[65];
1163
1136
  if (!sha256_file(kStaging, got_chk)) {
1164
1137
  nvs_close(h);
1165
- return err_obj_kind(ctx, "read staging file", OtaErr::kTransient);
1138
+ return core_fail(e, "read staging file", OtaErr::kTransient);
1166
1139
  }
1167
1140
  if (strcmp(got_chk, want_chk) != 0) {
1168
1141
  nvs_close(h);
1169
- return err_obj_kind(ctx, "checksum mismatch (corrupt download)", OtaErr::kCorrupt);
1142
+ return core_fail(e, "checksum mismatch (corrupt download)", OtaErr::kCorrupt);
1170
1143
  }
1171
1144
 
1172
1145
  // Verified. Promote the staging file to the pending build.
1173
1146
  unlink(kPending);
1174
1147
  if (rename(kStaging, kPending) != 0) {
1175
1148
  nvs_close(h);
1176
- return err_obj_kind(ctx, "stage pending build", OtaErr::kTransient);
1149
+ return core_fail(e, "stage pending build", OtaErr::kTransient);
1177
1150
  }
1178
1151
  g_stage_cap = 0;
1179
1152
  g_stage_have = 0;
@@ -1202,7 +1175,7 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
1202
1175
  }
1203
1176
  nvs_commit(h);
1204
1177
  nvs_close(h);
1205
- return err_obj_kind(ctx, err, kind);
1178
+ return core_fail(e, err, kind);
1206
1179
  }
1207
1180
  // New app is live but unproven: enter the trial. The caller restarts.
1208
1181
  nvs_set_u8(h, kKeyState, kStateTrial);
@@ -1214,7 +1187,7 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
1214
1187
  clear_trial_failure(h); // fresh trial: this build has not failed yet
1215
1188
  nvs_commit(h);
1216
1189
  nvs_close(h);
1217
- return ok_obj(ctx);
1190
+ return true;
1218
1191
  }
1219
1192
 
1220
1193
  // Defer: install at the next boot on a clean heap.
@@ -1222,11 +1195,10 @@ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* ar
1222
1195
  nvs_set_u8(h, kKeyInstLeft, kInstallBudget);
1223
1196
  nvs_commit(h);
1224
1197
  nvs_close(h);
1225
- return ok_obj(ctx);
1198
+ return true;
1226
1199
  }
1227
1200
 
1228
- // stageAbort() -> void
1229
- JSValue js_stage_abort(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1201
+ void core_stage_abort(void) {
1230
1202
  stage_close();
1231
1203
  g_stage_cap = 0;
1232
1204
  g_stage_have = 0;
@@ -1238,14 +1210,13 @@ JSValue js_stage_abort(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1238
1210
  nvs_commit(h);
1239
1211
  nvs_close(h);
1240
1212
  }
1241
- return JS_UNDEFINED;
1242
1213
  }
1243
1214
 
1244
- // markValid() -> void. Confirm the running trial: promote it to GOOD and make
1245
- // the pending build the new revert target.
1246
- JSValue js_mark_valid(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1215
+ // Confirm the running trial: promote it to GOOD and make the pending build the
1216
+ // new revert target.
1217
+ void core_mark_valid(void) {
1247
1218
  nvs_handle_t h;
1248
- if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return JS_UNDEFINED;
1219
+ if (nvs_open(kNs, NVS_READWRITE, &h) != ESP_OK) return;
1249
1220
  if (nvs_u8(h, kKeyState, kStateGood) == kStateTrial) {
1250
1221
  // Stay in the trial if the promotion failed: going GOOD anyway would
1251
1222
  // point instChk at a build that has no .tgz left to revert to.
@@ -1263,15 +1234,13 @@ JSValue js_mark_valid(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1263
1234
  nvs_commit(h);
1264
1235
  }
1265
1236
  nvs_close(h);
1266
- return JS_UNDEFINED;
1267
1237
  }
1268
1238
 
1269
- // revert() -> { ok } | { ok:false, error }. Re-install the last-good build.
1270
- JSValue js_revert(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1271
- if (!path_exists(kLastGood)) return err_obj(ctx, "no last-good build to revert to");
1239
+ bool core_revert(CoreErr* e) {
1240
+ if (!path_exists(kLastGood)) return core_fail(e, "no last-good build to revert to");
1272
1241
  const char* err = nullptr;
1273
1242
  OtaErr kind = OtaErr::kTransient;
1274
- if (!install_build(kLastGood, &err, &kind)) return err_obj(ctx, err);
1243
+ if (!install_build(kLastGood, &err, &kind)) return core_fail(e, err, kind);
1275
1244
  nvs_handle_t h;
1276
1245
  if (nvs_open(kNs, NVS_READWRITE, &h) == ESP_OK) {
1277
1246
  nvs_set_u8(h, kKeyState, kStateGood);
@@ -1282,40 +1251,36 @@ JSValue js_revert(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1282
1251
  nvs_commit(h);
1283
1252
  nvs_close(h);
1284
1253
  }
1285
- return ok_obj(ctx);
1254
+ return true;
1286
1255
  }
1287
1256
 
1288
- // running() -> { checksum?, trial }
1289
- JSValue js_running(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1290
- JSValue o = JS_NewObject(ctx);
1257
+ void core_running(char* chk, size_t chk_len, bool* out_trial) {
1291
1258
  bool trial = false;
1292
- char chk[96] = {0};
1259
+ if (chk && chk_len) chk[0] = '\0';
1293
1260
  nvs_handle_t h;
1294
1261
  if (nvs_open(kNs, NVS_READONLY, &h) == ESP_OK) {
1295
1262
  trial = nvs_u8(h, kKeyState, kStateGood) == kStateTrial;
1296
- nvs_str(h, trial ? kKeyPendChk : kKeyInstChk, chk, sizeof(chk));
1263
+ if (chk && chk_len) nvs_str(h, trial ? kKeyPendChk : kKeyInstChk, chk, chk_len);
1297
1264
  nvs_close(h);
1298
1265
  }
1299
- if (chk[0]) JS_SetPropertyStr(ctx, o, "checksum", JS_NewString(ctx, chk));
1300
- JS_SetPropertyStr(ctx, o, "trial", JS_NewBool(ctx, trial));
1301
- return o;
1266
+ if (out_trial) *out_trial = trial;
1302
1267
  }
1303
1268
 
1304
- // reconcile() -> { installed?, reverted, diagnostic? }. Returns the outcome the
1305
- // boot-time reconcile recorded, then clears it so a second call reads empty.
1306
- JSValue js_reconcile(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1307
- JSValue o = JS_NewObject(ctx);
1269
+ // Read the outcome the boot-time reconcile recorded, then clear it so a second
1270
+ // call reads empty.
1271
+ void core_reconcile(char* installed, size_t installed_len, bool* out_reverted, char* reason,
1272
+ size_t reason_len, char* detail, size_t detail_len) {
1273
+ if (installed && installed_len) installed[0] = '\0';
1274
+ if (reason && reason_len) reason[0] = '\0';
1275
+ if (detail && detail_len) detail[0] = '\0';
1308
1276
  bool reverted = false;
1309
- char installed[96] = {0};
1310
- char reason[96] = {0};
1311
- char detail[160] = {0};
1312
1277
 
1313
1278
  nvs_handle_t h;
1314
1279
  if (nvs_open(kNs, NVS_READWRITE, &h) == ESP_OK) {
1315
1280
  reverted = nvs_u8(h, kKeyORevert, 0) != 0;
1316
- nvs_str(h, kKeyOInst, installed, sizeof(installed));
1317
- nvs_str(h, kKeyORsn, reason, sizeof(reason));
1318
- nvs_str(h, kKeyODetail, detail, sizeof(detail));
1281
+ if (installed && installed_len) nvs_str(h, kKeyOInst, installed, installed_len);
1282
+ if (reason && reason_len) nvs_str(h, kKeyORsn, reason, reason_len);
1283
+ if (detail && detail_len) nvs_str(h, kKeyODetail, detail, detail_len);
1319
1284
  nvs_erase_key(h, kKeyOInst);
1320
1285
  nvs_erase_key(h, kKeyORevert);
1321
1286
  nvs_erase_key(h, kKeyORsn);
@@ -1323,6 +1288,125 @@ JSValue js_reconcile(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1323
1288
  nvs_commit(h);
1324
1289
  nvs_close(h);
1325
1290
  }
1291
+ if (out_reverted) *out_reverted = reverted;
1292
+ }
1293
+
1294
+ // ── JS bindings ──────────────────────────────────────────────────────────────
1295
+ // Argument coercion and result shaping only; the work is in the cores above.
1296
+
1297
+ JSValue core_err_obj(JSContext* ctx, const CoreErr& e) {
1298
+ return err_obj_kind(ctx, e.msg ? e.msg : "ota error", e.kind);
1299
+ }
1300
+
1301
+ // stageBegin(checksum, size) -> { ok, resumeOffset } | { ok:false, error }
1302
+ JSValue js_stage_begin(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
1303
+ if (argc < 2) return err_obj(ctx, "missing checksum/size");
1304
+ const char* checksum = JS_ToCString(ctx, argv[0]);
1305
+ if (!checksum) {
1306
+ // JS_ToCString throws on a value with a throwing toString. Clear it
1307
+ // rather than strand it on the context for some unrelated later
1308
+ // operation to surface, the same way the size path below does.
1309
+ JS_FreeValue(ctx, JS_GetException(ctx));
1310
+ return err_obj(ctx, "checksum not a string");
1311
+ }
1312
+ int64_t size = 0;
1313
+ if (JS_ToInt64(ctx, &size, argv[1])) {
1314
+ JS_FreeCString(ctx, checksum);
1315
+ // JS_ToInt64 throws on a value with a throwing valueOf. We report through
1316
+ // the result object, so clear it rather than strand it on the context.
1317
+ JS_FreeValue(ctx, JS_GetException(ctx));
1318
+ return err_obj(ctx, "invalid size");
1319
+ }
1320
+
1321
+ CoreErr e;
1322
+ int64_t resume = 0;
1323
+ bool ok = core_stage_begin(checksum, size, &resume, &e);
1324
+ JS_FreeCString(ctx, checksum);
1325
+ if (!ok) return core_err_obj(ctx, e);
1326
+
1327
+ JSValue o = ok_obj(ctx);
1328
+ JS_SetPropertyStr(ctx, o, "resumeOffset", JS_NewInt64(ctx, resume));
1329
+ return o;
1330
+ }
1331
+
1332
+ // stageWrite(bytes) -> { ok } | { ok:false, error, kind? }
1333
+ JSValue js_stage_write(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
1334
+ if (argc < 1) return err_obj(ctx, "missing bytes");
1335
+ size_t len = 0;
1336
+ const uint8_t* data = JS_GetUint8Array(ctx, &len, argv[0]);
1337
+ if (!data) {
1338
+ JS_FreeValue(ctx, JS_GetException(ctx)); // JS_GetUint8Array throws; we report via err_obj
1339
+ return err_obj(ctx, "bytes not a Uint8Array");
1340
+ }
1341
+ CoreErr e;
1342
+ if (!core_stage_write(data, len, &e)) return core_err_obj(ctx, e);
1343
+ return ok_obj(ctx);
1344
+ }
1345
+
1346
+ // stageFinish(trialBoots, requireConfirm, installNow)
1347
+ // -> { ok } | { ok:false, error, kind }
1348
+ // Verifies SHA-256 + size over the whole staged file, then either installs in
1349
+ // place now (installNow) or marks a verified build staged-for-install so the
1350
+ // next boot's reconcile installs it on a clean heap.
1351
+ JSValue js_stage_finish(JSContext* ctx, JSValueConst, int argc, JSValueConst* argv) {
1352
+ int64_t trial_boots = 1;
1353
+ if (argc >= 1 && JS_ToInt64(ctx, &trial_boots, argv[0])) {
1354
+ // A throwing valueOf leaves trial_boots indeterminate and an exception
1355
+ // pending; we report through the result object, so clear it and default.
1356
+ JS_FreeValue(ctx, JS_GetException(ctx));
1357
+ trial_boots = 1;
1358
+ }
1359
+ bool require_confirm = argc >= 2 && JS_ToBool(ctx, argv[1]);
1360
+ bool install_now = argc >= 3 && JS_ToBool(ctx, argv[2]);
1361
+
1362
+ CoreErr e;
1363
+ if (!core_stage_finish(trial_boots, require_confirm, install_now, &e)) {
1364
+ return core_err_obj(ctx, e);
1365
+ }
1366
+ return ok_obj(ctx);
1367
+ }
1368
+
1369
+ // stageAbort() -> void
1370
+ JSValue js_stage_abort(JSContext*, JSValueConst, int, JSValueConst*) {
1371
+ core_stage_abort();
1372
+ return JS_UNDEFINED;
1373
+ }
1374
+
1375
+ // markValid() -> void
1376
+ JSValue js_mark_valid(JSContext*, JSValueConst, int, JSValueConst*) {
1377
+ core_mark_valid();
1378
+ return JS_UNDEFINED;
1379
+ }
1380
+
1381
+ // revert() -> { ok } | { ok:false, error }. Re-install the last-good build.
1382
+ JSValue js_revert(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1383
+ CoreErr e;
1384
+ // Historically reported without a kind, and the policy maps every revert
1385
+ // failure to transient anyway.
1386
+ if (!core_revert(&e)) return err_obj(ctx, e.msg ? e.msg : "revert failed");
1387
+ return ok_obj(ctx);
1388
+ }
1389
+
1390
+ // running() -> { checksum?, trial }
1391
+ JSValue js_running(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1392
+ JSValue o = JS_NewObject(ctx);
1393
+ char chk[96] = {0};
1394
+ bool trial = false;
1395
+ core_running(chk, sizeof(chk), &trial);
1396
+ if (chk[0]) JS_SetPropertyStr(ctx, o, "checksum", JS_NewString(ctx, chk));
1397
+ JS_SetPropertyStr(ctx, o, "trial", JS_NewBool(ctx, trial));
1398
+ return o;
1399
+ }
1400
+
1401
+ // reconcile() -> { installed?, reverted, diagnostic? }
1402
+ JSValue js_reconcile(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1403
+ JSValue o = JS_NewObject(ctx);
1404
+ char installed[96] = {0};
1405
+ char reason[96] = {0};
1406
+ char detail[160] = {0};
1407
+ bool reverted = false;
1408
+ core_reconcile(installed, sizeof(installed), &reverted, reason, sizeof(reason), detail,
1409
+ sizeof(detail));
1326
1410
 
1327
1411
  if (installed[0]) JS_SetPropertyStr(ctx, o, "installed", JS_NewString(ctx, installed));
1328
1412
  JS_SetPropertyStr(ctx, o, "reverted", JS_NewBool(ctx, reverted));
@@ -1335,6 +1419,109 @@ JSValue js_reconcile(JSContext* ctx, JSValueConst, int, JSValueConst*) {
1335
1419
  return o;
1336
1420
  }
1337
1421
 
1422
+ // ── MIKOtaEnv install ops ────────────────────────────────────────────────────
1423
+ // The native OTA client reaches the staging machinery through these. They are
1424
+ // the same cores the JS bindings above call, so nothing can drift between the
1425
+ // two implementations while both are in the tree.
1426
+
1427
+ namespace {
1428
+
1429
+ int env_err_kind(OtaErr kind) {
1430
+ switch (kind) {
1431
+ case OtaErr::kCorrupt:
1432
+ return MIK_OTA_ERR_CORRUPT;
1433
+ case OtaErr::kOom:
1434
+ return MIK_OTA_ERR_OOM;
1435
+ case OtaErr::kTransient:
1436
+ break;
1437
+ }
1438
+ return MIK_OTA_ERR_TRANSIENT;
1439
+ }
1440
+
1441
+ void env_copy_err(const CoreErr& e, char* err_buf, size_t err_len) {
1442
+ if (err_buf && err_len) snprintf(err_buf, err_len, "%s", e.msg ? e.msg : "ota error");
1443
+ }
1444
+
1445
+ bool env_stage_begin(void*, const char* checksum, size_t size, size_t* out_resume, char* err_buf,
1446
+ size_t err_len) {
1447
+ CoreErr e;
1448
+ int64_t resume = 0;
1449
+ if (!core_stage_begin(checksum, (int64_t)size, &resume, &e)) {
1450
+ env_copy_err(e, err_buf, err_len);
1451
+ return false;
1452
+ }
1453
+ if (out_resume) *out_resume = (size_t)resume;
1454
+ return true;
1455
+ }
1456
+
1457
+ bool env_stage_write(void*, const uint8_t* data, size_t len, char* err_buf, size_t err_len) {
1458
+ CoreErr e;
1459
+ if (!core_stage_write(data, len, &e)) {
1460
+ env_copy_err(e, err_buf, err_len);
1461
+ return false;
1462
+ }
1463
+ return true;
1464
+ }
1465
+
1466
+ bool env_stage_finish(void*, int trial_boots, bool require_confirm, bool install_now, char* err_buf,
1467
+ size_t err_len, int* out_err_kind) {
1468
+ CoreErr e;
1469
+ if (!core_stage_finish(trial_boots, require_confirm, install_now, &e)) {
1470
+ env_copy_err(e, err_buf, err_len);
1471
+ if (out_err_kind) *out_err_kind = env_err_kind(e.kind);
1472
+ return false;
1473
+ }
1474
+ return true;
1475
+ }
1476
+
1477
+ void env_stage_abort(void*) { core_stage_abort(); }
1478
+ void env_mark_valid(void*) { core_mark_valid(); }
1479
+
1480
+ bool env_revert(void*, char* err_buf, size_t err_len) {
1481
+ CoreErr e;
1482
+ if (!core_revert(&e)) {
1483
+ env_copy_err(e, err_buf, err_len);
1484
+ return false;
1485
+ }
1486
+ return true;
1487
+ }
1488
+
1489
+ bool env_running(void*, MIKOtaRunningBuild* out) {
1490
+ if (!out) return false;
1491
+ *out = {};
1492
+ char chk[96] = {0};
1493
+ bool trial = false;
1494
+ core_running(chk, sizeof(chk), &trial);
1495
+ // The checksum field is a 64-hex digest plus its terminator; a longer value
1496
+ // could only come from corrupted NVS, and truncating it is what the JS path
1497
+ // effectively did too.
1498
+ snprintf(out->checksum, sizeof(out->checksum), "%s", chk);
1499
+ out->trial = trial;
1500
+ // version is left empty on purpose: the policy fills it from the app's
1501
+ // package.json through read_app_version.
1502
+ return true;
1503
+ }
1504
+
1505
+ void env_reconcile(void*, MIKOtaReconcileOutcome* out) {
1506
+ if (!out) return;
1507
+ *out = {};
1508
+ char installed[96] = {0};
1509
+ char reason[96] = {0};
1510
+ char detail[160] = {0};
1511
+ bool reverted = false;
1512
+ core_reconcile(installed, sizeof(installed), &reverted, reason, sizeof(reason), detail,
1513
+ sizeof(detail));
1514
+ snprintf(out->installed, sizeof(out->installed), "%s", installed);
1515
+ out->reverted = reverted;
1516
+ if (reason[0]) {
1517
+ out->has_diagnostic = true;
1518
+ snprintf(out->diagnostic.reason, sizeof(out->diagnostic.reason), "%s", reason);
1519
+ snprintf(out->diagnostic.detail, sizeof(out->diagnostic.detail), "%s", detail);
1520
+ }
1521
+ }
1522
+
1523
+ } // namespace
1524
+
1338
1525
  // ── module registration ──────────────────────────────────────────────────────
1339
1526
  int mik__ota_module_init(JSContext* ctx, JSModuleDef* m) {
1340
1527
  JS_SetModuleExport(ctx, m, "stageBegin",
@@ -1482,6 +1669,18 @@ void adopt_fail(nvs_handle_t h, const char* chk, const char* reason, const char*
1482
1669
 
1483
1670
  } // namespace
1484
1671
 
1672
+ void mik__ota_fill_install_ops(MIKOtaEnv* env) {
1673
+ if (!env) return;
1674
+ env->stage_begin = env_stage_begin;
1675
+ env->stage_write = env_stage_write;
1676
+ env->stage_finish = env_stage_finish;
1677
+ env->stage_abort = env_stage_abort;
1678
+ env->mark_valid = env_mark_valid;
1679
+ env->revert = env_revert;
1680
+ env->running = env_running;
1681
+ env->reconcile = env_reconcile;
1682
+ }
1683
+
1485
1684
  // Stage a streamed .tgz for adopt install at the next boot (the cable-deploy
1486
1685
  // path, distinct from an OTA download which runs a trial). The install itself
1487
1686
  // runs from mik__ota_boot_reconcile() on a clean heap: doing it here, with the