@shipvo/runtime 0.1.3 → 0.1.5
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 +49 -0
- package/android/build.gradle +7 -0
- package/android/src/main/java/dev/shipvo/runtime/BootRules.kt +53 -0
- package/android/src/main/java/dev/shipvo/runtime/Clock.kt +9 -0
- package/android/src/main/java/dev/shipvo/runtime/HermesGuard.kt +42 -0
- package/android/src/main/java/dev/shipvo/runtime/PointerQuery.kt +36 -0
- package/android/src/main/java/dev/shipvo/runtime/ResumableDownload.kt +39 -0
- package/android/src/main/java/dev/shipvo/runtime/Rotation.kt +134 -0
- package/android/src/main/java/dev/shipvo/runtime/ShipvoHostHandler.kt +4 -5
- package/android/src/main/java/dev/shipvo/runtime/ShipvoRuntimeModule.kt +66 -10
- package/android/src/main/java/dev/shipvo/runtime/Store.kt +36 -3
- package/android/src/test/java/dev/shipvo/runtime/HermesGuardTest.kt +73 -0
- package/android/src/test/java/dev/shipvo/runtime/PointerQueryTest.kt +45 -0
- package/android/src/test/java/dev/shipvo/runtime/ResumableDownloadTest.kt +90 -0
- package/android/src/test/java/dev/shipvo/runtime/RotationTest.kt +141 -0
- package/android/src/test/java/dev/shipvo/runtime/StabilityTest.kt +76 -0
- package/app.plugin.js +75 -2
- package/ios/BundleSelector.swift +21 -1
- package/ios/HermesGuard.swift +43 -0
- package/ios/Rotation.swift +147 -0
- package/ios/ShipvoRuntimeModule.swift +27 -0
- package/ios/Store.swift +47 -0
- package/ios/Updater.swift +88 -14
- package/package.json +9 -1
- package/skills/shipvo-runtime/SKILL.md +39 -0
- package/src/index.ts +102 -1
- package/src/index.web.ts +3 -0
- package/src/install.ts +74 -0
- package/src/runtimeEvents.ts +38 -0
package/src/install.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install identity for signed telemetry. On first launch the app makes a random install id and registers it once
|
|
3
|
+
* (`POST registerUrl` with `{ install_id }`, answer `{ key }`). The key is a secret string. Reports are signed with
|
|
4
|
+
* HMAC-SHA256, using the UTF-8 bytes of the key, and the signature is lower-case hex.
|
|
5
|
+
*
|
|
6
|
+
* Signing rule: the sent body is `<unsigned>` with its last `}` replaced by `,"sig":"<hex>"}`. The HMAC covers the
|
|
7
|
+
* unsigned JSON string, including its closing `}`. To verify, the server cuts the suffix `,"sig":"<hex>"}` from the
|
|
8
|
+
* received string, appends `}`, and checks the HMAC of that string.
|
|
9
|
+
*/
|
|
10
|
+
export type InstallIdentity = { install_id: string; key: string };
|
|
11
|
+
|
|
12
|
+
/** Where the identity lives. Pass an AsyncStorage-style adapter on React Native. */
|
|
13
|
+
export interface TelemetryStorage {
|
|
14
|
+
getItem(key: string): Promise<string | null> | string | null;
|
|
15
|
+
setItem(key: string, value: string): Promise<void> | void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** React Native has no `crypto.subtle`, so apps can pass their own. */
|
|
19
|
+
export interface TelemetryCrypto {
|
|
20
|
+
/** Random bytes, for the install id. */
|
|
21
|
+
randomBytes(n: number): Uint8Array | Promise<Uint8Array>;
|
|
22
|
+
/** HMAC-SHA256 of `data` under `key`. */
|
|
23
|
+
hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array | Promise<Uint8Array>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const IDENTITY_STORAGE_KEY = "shipvo.install_identity";
|
|
27
|
+
|
|
28
|
+
const hex = (b: Uint8Array) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
|
|
30
|
+
/** Default for web and Node (needs `globalThis.crypto.subtle`). */
|
|
31
|
+
export const webCrypto: TelemetryCrypto = {
|
|
32
|
+
randomBytes: (n) => globalThis.crypto.getRandomValues(new Uint8Array(n)),
|
|
33
|
+
hmacSha256: async (key, data) => {
|
|
34
|
+
const k = await globalThis.crypto.subtle.importKey("raw", key as BufferSource, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
35
|
+
return new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", k, data as BufferSource));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load the stored identity, or create and register one. Returns `null` when registration fails (offline, server
|
|
41
|
+
* error, bad answer); nothing is stored then and the next launch tries again. Never throws.
|
|
42
|
+
*/
|
|
43
|
+
export async function ensureInstallIdentity(
|
|
44
|
+
storage: TelemetryStorage, crypto: TelemetryCrypto, registerUrl: string,
|
|
45
|
+
): Promise<InstallIdentity | null> {
|
|
46
|
+
try {
|
|
47
|
+
const raw = await storage.getItem(IDENTITY_STORAGE_KEY);
|
|
48
|
+
if (raw) {
|
|
49
|
+
let s: any = null;
|
|
50
|
+
try { s = JSON.parse(raw); } catch {} // corrupted: register again
|
|
51
|
+
if (typeof s?.install_id === "string" && typeof s?.key === "string") return { install_id: s.install_id, key: s.key };
|
|
52
|
+
}
|
|
53
|
+
const install_id = hex(await crypto.randomBytes(16));
|
|
54
|
+
const res = await fetch(registerUrl, { method: "POST", headers: { "content-type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ install_id }) });
|
|
56
|
+
if (!res.ok) return null;
|
|
57
|
+
const out: any = await res.json();
|
|
58
|
+
if (typeof out?.key !== "string" || !out.key) return null;
|
|
59
|
+
const identity = { install_id, key: out.key };
|
|
60
|
+
await storage.setItem(IDENTITY_STORAGE_KEY, JSON.stringify(identity));
|
|
61
|
+
return identity;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Sign `body`, the unsigned JSON string (with its closing `}`). See the signing rule at the top of this file. */
|
|
68
|
+
export async function signReport(
|
|
69
|
+
identity: InstallIdentity, body: string, crypto: TelemetryCrypto = webCrypto,
|
|
70
|
+
): Promise<{ install_id: string; sig: string }> {
|
|
71
|
+
const enc = new TextEncoder();
|
|
72
|
+
const mac = await crypto.hmacSha256(enc.encode(identity.key), enc.encode(body));
|
|
73
|
+
return { install_id: identity.install_id, sig: hex(mac) };
|
|
74
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Runtime events the app forwards to telemetry. Each one is sent once per event. */
|
|
2
|
+
export const FORWARDED_EVENTS = [
|
|
3
|
+
"crash_loop_revert",
|
|
4
|
+
"rolled_back",
|
|
5
|
+
"verify_failed",
|
|
6
|
+
"download_failed",
|
|
7
|
+
"hermes_bytecode_mismatch",
|
|
8
|
+
"native_module_missing",
|
|
9
|
+
"native_module_version",
|
|
10
|
+
] as const;
|
|
11
|
+
export type ForwardedEvent = (typeof FORWARDED_EVENTS)[number];
|
|
12
|
+
|
|
13
|
+
const CARRIES_RELEASE: string[] = ["verify_failed", "download_failed", "hermes_bytecode_mismatch"];
|
|
14
|
+
const NATIVE_MODULE_REASONS: string[] = ["native_module_missing", "native_module_version"];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Turn runtime event strings ("name" or "name:<release>") into telemetry reports.
|
|
18
|
+
* `verify_failed`, `download_failed` and `hermes_bytecode_mismatch:<release>:...`
|
|
19
|
+
* carry a release id: a failed release is reported under its own id. `crash_loop_revert` and `rolled_back` never override the
|
|
20
|
+
* release, so the sender uses the running release as before. A missing or non-numeric
|
|
21
|
+
* id also falls back to the running release.
|
|
22
|
+
*/
|
|
23
|
+
export function forwardRuntimeEvents(
|
|
24
|
+
events: string[],
|
|
25
|
+
send: (event: ForwardedEvent, release?: number) => void,
|
|
26
|
+
): void {
|
|
27
|
+
for (const e of events) {
|
|
28
|
+
const [name, id] = e.split(":");
|
|
29
|
+
if (name === "pointer_rejected") {
|
|
30
|
+
// "pointer_rejected:<reason>": only the two native-module reasons are reported.
|
|
31
|
+
if (id !== undefined && NATIVE_MODULE_REASONS.includes(id)) send(id as ForwardedEvent);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!(FORWARDED_EVENTS as readonly string[]).includes(name)) continue;
|
|
35
|
+
const release = CARRIES_RELEASE.includes(name) && id !== undefined && /^\d+$/.test(id) ? Number(id) : undefined;
|
|
36
|
+
send(name as ForwardedEvent, release);
|
|
37
|
+
}
|
|
38
|
+
}
|