ubriot 1.1.1 → 1.4.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/README.md +21 -0
- package/dist/api.js +24 -10
- package/dist/app-config-signing.js +67 -0
- package/dist/binary-certificate.js +93 -0
- package/dist/build-identity.js +72 -0
- package/dist/build-preflight.js +99 -0
- package/dist/child-env.js +10 -0
- package/dist/config.js +23 -13
- package/dist/index.js +601 -54
- package/dist/migrate.js +132 -0
- package/dist/ota-signing.js +271 -0
- package/dist/play-options.js +22 -0
- package/dist/publish-signing.js +74 -0
- package/dist/runtime-preflight.js +36 -0
- package/dist/signing-commands.js +609 -0
- package/dist/unreal-detect.js +69 -0
- package/dist/x509.js +117 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -28,8 +28,29 @@ ubriot update # publish an OTA update
|
|
|
28
28
|
| `ubriot update` | Export and publish an OTA update |
|
|
29
29
|
| `ubriot push setup --app <name>` | Set up native push notifications |
|
|
30
30
|
| `ubriot apps list` | List your release apps |
|
|
31
|
+
| `ubriot apps activity <app>` | Show who changed what on an app, newest first |
|
|
31
32
|
| `ubriot builds list --app <name>` | List recent builds |
|
|
32
33
|
| `ubriot builds watch <buildId>` | Watch a build live: streaming logs + status until it finishes |
|
|
34
|
+
| `ubriot signing init --app <name>` | Generate an OTA signing key outside the repo and register its certificate (`--mode chain` for an offline root) |
|
|
35
|
+
| `ubriot signing configure --app <name>` | Embed the signing certificate in app.json (expo-updates or ubriot-updates-rn) |
|
|
36
|
+
| `ubriot signing status --app <name>` | Keys, expiry, devices per key, rejected update checks |
|
|
37
|
+
| `ubriot signing import` | Register an existing certificate, for example from EAS Update |
|
|
38
|
+
| `ubriot signing issue-leaf` / `rotate` / `retire` / `revoke` / `require` | Rotate, retire or revoke keys, and refuse unsigned publishes |
|
|
39
|
+
| `ubriot signing verify-binary <app.ipa>` | List the signing certificates an IPA, APK or AAB trusts |
|
|
40
|
+
| `ubriot signing managed create\|rotate\|audit` | Ubriot-managed signing keys (paid plans) |
|
|
41
|
+
| `ubriot updates resign --app <name>` | Sign live update groups again with the current key |
|
|
42
|
+
|
|
43
|
+
OTA updates are signed automatically when a key is available: `--private-key-path` and
|
|
44
|
+
`--certificate-path`, the `UBRIOT_OTA_PRIVATE_KEY` environment variable in CI, or the key
|
|
45
|
+
paths saved by `ubriot signing init` in `.ubriot/signing.json` (paths only, never key
|
|
46
|
+
contents). Use `--sign-with <keyid>` more than once to sign with two keys during a
|
|
47
|
+
rotation. Guide: https://docs.ubriot.dev/ota-code-signing
|
|
48
|
+
|
|
49
|
+
Before queueing a build, `ubriot ship` and `ubriot builds create` read the project's Expo
|
|
50
|
+
SDK (or React Native version) and warn when no build worker has the toolchain it needs,
|
|
51
|
+
for example Expo SDK 58 without Xcode 26.4 or newer. `ubriot update` warns when your own
|
|
52
|
+
Node.js is too old for the SDK's export step. Pass `--yes` to skip the question. Guide:
|
|
53
|
+
https://docs.ubriot.dev/expo-sdk-58
|
|
33
54
|
|
|
34
55
|
## Documentation
|
|
35
56
|
|
package/dist/api.js
CHANGED
|
@@ -40,28 +40,42 @@ function makeClient() {
|
|
|
40
40
|
const original = error.config;
|
|
41
41
|
const status = error.response?.status;
|
|
42
42
|
const isAuthCall = typeof original?.url === "string" && original.url.includes("/auth/");
|
|
43
|
-
if (status !== 401 || !original ||
|
|
43
|
+
if (status !== 401 || !original || isAuthCall) {
|
|
44
44
|
return Promise.reject(error);
|
|
45
45
|
}
|
|
46
|
+
const newToken = original._retry ? null : await refreshAccessToken();
|
|
47
|
+
if (!newToken)
|
|
48
|
+
exitAuthenticationFailed(error);
|
|
46
49
|
original._retry = true;
|
|
47
|
-
const newToken = await refreshAccessToken();
|
|
48
|
-
if (!newToken) {
|
|
49
|
-
console.error("Session expired. Run: ubriot login");
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
50
|
original.headers = original.headers ?? {};
|
|
53
51
|
original.headers.Authorization = `Bearer ${newToken}`;
|
|
54
52
|
return client(original);
|
|
55
53
|
});
|
|
56
54
|
return client;
|
|
57
55
|
}
|
|
56
|
+
function serverMessage(err) {
|
|
57
|
+
return err.response?.data?.message ?? err.message;
|
|
58
|
+
}
|
|
59
|
+
function exitAuthenticationFailed(err) {
|
|
60
|
+
const message = String(serverMessage(err)).replace(/\.+$/, "");
|
|
61
|
+
const nextStep = /does not accept API keys/.test(message)
|
|
62
|
+
? ""
|
|
63
|
+
: (0, config_1.envToken)()
|
|
64
|
+
? ` Check UBRIOT_TOKEN, or create a new API key at ${config_1.API_KEYS_URL}.`
|
|
65
|
+
: " Run: ubriot login, or set UBRIOT_TOKEN.";
|
|
66
|
+
console.error(`Authentication failed: ${message}.${nextStep}`);
|
|
67
|
+
process.exit(2);
|
|
68
|
+
}
|
|
58
69
|
function handleError(err) {
|
|
59
|
-
if (axios_1.default.isAxiosError(err)) {
|
|
60
|
-
const
|
|
61
|
-
console.error(`Error: ${
|
|
70
|
+
if (axios_1.default.isAxiosError(err) && !err.response) {
|
|
71
|
+
const apiBase = err.config?.baseURL ?? normalizeApiBase((0, config_1.readConfig)().apiBase);
|
|
72
|
+
console.error(`Error: could not reach the Ubriot API at ${apiBase} (${err.code ?? err.message}). Check your connection, or point the CLI at another API with UBRIOT_API_BASE or ubriot login --api-base <url>.`);
|
|
73
|
+
}
|
|
74
|
+
else if (axios_1.default.isAxiosError(err)) {
|
|
75
|
+
console.error(`Error: ${serverMessage(err)}`);
|
|
62
76
|
}
|
|
63
77
|
else {
|
|
64
|
-
console.error(
|
|
78
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
65
79
|
}
|
|
66
80
|
process.exit(1);
|
|
67
81
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CODE_SIGNING_PLUGIN = exports.UPDATES_RN_PLUGIN = void 0;
|
|
4
|
+
exports.hasUpdatesRnPlugin = hasUpdatesRnPlugin;
|
|
5
|
+
exports.applyExpoSigningConfig = applyExpoSigningConfig;
|
|
6
|
+
exports.applyBareSigningConfig = applyBareSigningConfig;
|
|
7
|
+
exports.UPDATES_RN_PLUGIN = "ubriot-updates-rn";
|
|
8
|
+
exports.CODE_SIGNING_PLUGIN = "ubriot-updates-rn/code-signing";
|
|
9
|
+
function pluginName(entry) {
|
|
10
|
+
return Array.isArray(entry) ? entry[0] : entry;
|
|
11
|
+
}
|
|
12
|
+
function withPluginOptions(plugins, name, options) {
|
|
13
|
+
const index = plugins.findIndex((entry) => pluginName(entry) === name);
|
|
14
|
+
const next = options ? [name, options] : name;
|
|
15
|
+
if (index === -1)
|
|
16
|
+
return [...plugins, next];
|
|
17
|
+
const copy = [...plugins];
|
|
18
|
+
copy[index] = next;
|
|
19
|
+
return copy;
|
|
20
|
+
}
|
|
21
|
+
function withoutPlugin(plugins, name) {
|
|
22
|
+
return plugins.filter((entry) => pluginName(entry) !== name);
|
|
23
|
+
}
|
|
24
|
+
function expoRoot(appJson) {
|
|
25
|
+
const root = appJson && typeof appJson === "object" ? { ...appJson } : {};
|
|
26
|
+
const expo = root.expo && typeof root.expo === "object" ? { ...root.expo } : {};
|
|
27
|
+
return { root, expo };
|
|
28
|
+
}
|
|
29
|
+
function hasUpdatesRnPlugin(appJson) {
|
|
30
|
+
const plugins = Array.isArray(appJson?.expo?.plugins) ? appJson.expo.plugins : [];
|
|
31
|
+
return plugins.some((entry) => pluginName(entry) === exports.UPDATES_RN_PLUGIN);
|
|
32
|
+
}
|
|
33
|
+
function applyExpoSigningConfig(appJson, input) {
|
|
34
|
+
const { root, expo } = expoRoot(appJson);
|
|
35
|
+
const updates = expo.updates && typeof expo.updates === "object" ? { ...expo.updates } : {};
|
|
36
|
+
delete updates.codeSigningAllowUnsignedManifests;
|
|
37
|
+
updates.codeSigningCertificate = input.certificatePath;
|
|
38
|
+
updates.codeSigningMetadata = { keyid: input.keyid, alg: "rsa-v1_5-sha256" };
|
|
39
|
+
expo.updates = updates;
|
|
40
|
+
const plugins = Array.isArray(expo.plugins) ? expo.plugins : [];
|
|
41
|
+
expo.plugins =
|
|
42
|
+
input.mode === "chain"
|
|
43
|
+
? withPluginOptions(plugins, exports.CODE_SIGNING_PLUGIN, { includeManifestResponseCertificateChain: true })
|
|
44
|
+
: withoutPlugin(plugins, exports.CODE_SIGNING_PLUGIN);
|
|
45
|
+
if (expo.plugins.length === 0)
|
|
46
|
+
delete expo.plugins;
|
|
47
|
+
root.expo = expo;
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
function applyBareSigningConfig(appJson, input) {
|
|
51
|
+
const { root, expo } = expoRoot(appJson);
|
|
52
|
+
const plugins = Array.isArray(expo.plugins) ? expo.plugins : [];
|
|
53
|
+
const existing = plugins.find((entry) => pluginName(entry) === exports.UPDATES_RN_PLUGIN);
|
|
54
|
+
const current = Array.isArray(existing) && existing[1] && typeof existing[1] === "object" ? { ...existing[1] } : {};
|
|
55
|
+
delete current.codeSigningAllowUnsignedManifests;
|
|
56
|
+
const options = {
|
|
57
|
+
...current,
|
|
58
|
+
codeSigningCertificate: input.certificatePath,
|
|
59
|
+
codeSigningMetadata: { keyid: input.keyid, alg: "rsa-v1_5-sha256" },
|
|
60
|
+
codeSigningIncludeManifestResponseCertificateChain: input.mode === "chain",
|
|
61
|
+
};
|
|
62
|
+
if (input.additionalCertificates?.length)
|
|
63
|
+
options.additionalTrustedCertificates = input.additionalCertificates;
|
|
64
|
+
expo.plugins = withPluginOptions(plugins, exports.UPDATES_RN_PLUGIN, options);
|
|
65
|
+
root.expo = expo;
|
|
66
|
+
return root;
|
|
67
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findCertificatesInBuffer = findCertificatesInBuffer;
|
|
4
|
+
exports.extractEmbeddedCertificates = extractEmbeddedCertificates;
|
|
5
|
+
exports.matchAnchors = matchAnchors;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const os_1 = require("os");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const MAX_FILE_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const MAX_EXTRACTED_BYTES = 4 * 1024 * 1024 * 1024;
|
|
13
|
+
const MAX_FILES = 20000;
|
|
14
|
+
const PEM = /-----BEGIN CERTIFICATE-----(?:[A-Za-z0-9+/=\s]|\\n|\\r)+?-----END CERTIFICATE-----/g;
|
|
15
|
+
function swapBytes(buffer) {
|
|
16
|
+
const even = buffer.length - (buffer.length % 2);
|
|
17
|
+
const copy = Buffer.from(buffer.subarray(0, even));
|
|
18
|
+
copy.swap16();
|
|
19
|
+
return copy;
|
|
20
|
+
}
|
|
21
|
+
function findCertificatesInBuffer(buffer) {
|
|
22
|
+
const texts = [buffer.toString("utf8"), buffer.toString("utf16le"), swapBytes(buffer).toString("utf16le")];
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const text of texts) {
|
|
25
|
+
for (const match of text.match(PEM) ?? [])
|
|
26
|
+
out.push(match.replace(/\\r/g, "").replace(/\\n/g, "\n"));
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function walk(dir, files = []) {
|
|
31
|
+
for (const entry of (0, fs_1.readdirSync)(dir, { withFileTypes: true })) {
|
|
32
|
+
const full = (0, path_1.join)(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory())
|
|
34
|
+
walk(full, files);
|
|
35
|
+
else if (entry.isFile())
|
|
36
|
+
files.push(full);
|
|
37
|
+
if (files.length > MAX_FILES)
|
|
38
|
+
throw new Error("Archive has too many files to scan");
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
function declaredSize(archivePath) {
|
|
43
|
+
const listing = (0, child_process_1.spawnSync)("unzip", ["-l", archivePath], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
44
|
+
if (listing.error || listing.status !== 0)
|
|
45
|
+
throw new Error(`Could not unpack ${archivePath}. Pass an .ipa, .apk or .aab file.`);
|
|
46
|
+
const lines = listing.stdout.trim().split("\n");
|
|
47
|
+
const total = Number(lines[lines.length - 1]?.trim().split(/\s+/)[0]);
|
|
48
|
+
if (!Number.isFinite(total))
|
|
49
|
+
throw new Error(`Could not read the contents of ${archivePath}`);
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
function extractEmbeddedCertificates(archivePath, options = {}) {
|
|
53
|
+
const limit = options.maxExtractedBytes ?? MAX_EXTRACTED_BYTES;
|
|
54
|
+
if (declaredSize(archivePath) > limit)
|
|
55
|
+
throw new Error(`${archivePath} unpacks to more than ${Math.round(limit / 1024 / 1024)} MB. Refusing to scan it.`);
|
|
56
|
+
const dir = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), "ubriot-verify-binary-"));
|
|
57
|
+
try {
|
|
58
|
+
const result = (0, child_process_1.spawnSync)("unzip", ["-qq", "-o", archivePath, "-d", dir], { encoding: "utf8" });
|
|
59
|
+
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
60
|
+
throw new Error(`Could not unpack ${archivePath}. Pass an .ipa, .apk or .aab file.`);
|
|
61
|
+
}
|
|
62
|
+
const found = new Map();
|
|
63
|
+
for (const file of walk(dir)) {
|
|
64
|
+
if ((0, fs_1.statSync)(file).size > MAX_FILE_BYTES)
|
|
65
|
+
continue;
|
|
66
|
+
for (const pem of findCertificatesInBuffer((0, fs_1.readFileSync)(file))) {
|
|
67
|
+
let certificate;
|
|
68
|
+
try {
|
|
69
|
+
certificate = new crypto_1.X509Certificate(pem);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const fingerprintSha256 = certificate.fingerprint256.replace(/:/g, "").toLowerCase();
|
|
75
|
+
const entry = found.get(fingerprintSha256) ?? { fingerprintSha256, subject: certificate.subject, notAfter: certificate.validTo, foundIn: [] };
|
|
76
|
+
const where = (0, path_1.relative)(dir, file);
|
|
77
|
+
if (!entry.foundIn.includes(where))
|
|
78
|
+
entry.foundIn.push(where);
|
|
79
|
+
found.set(fingerprintSha256, entry);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...found.values()];
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
(0, fs_1.rmSync)(dir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function matchAnchors(embedded, anchors) {
|
|
89
|
+
return embedded.map((certificate) => ({
|
|
90
|
+
...certificate,
|
|
91
|
+
matches: anchors.filter((anchor) => anchor.fingerprintSha256 === certificate.fingerprintSha256).map((anchor) => anchor.label),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildIdentityEnv = buildIdentityEnv;
|
|
4
|
+
exports.describeBuildIdentity = describeBuildIdentity;
|
|
5
|
+
exports.assertConfigMatchesApp = assertConfigMatchesApp;
|
|
6
|
+
exports.assertUpdateMatchesApp = assertUpdateMatchesApp;
|
|
7
|
+
exports.hasDynamicExpoConfig = hasDynamicExpoConfig;
|
|
8
|
+
exports.resolveExpoConfig = resolveExpoConfig;
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const child_env_1 = require("./child-env");
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const path_1 = require("path");
|
|
13
|
+
const DYNAMIC_EXPO_CONFIG_FILES = ["app.config.js", "app.config.ts", "app.config.cjs", "app.config.mjs"];
|
|
14
|
+
function buildIdentityEnv(app) {
|
|
15
|
+
const identity = {
|
|
16
|
+
UBRIOT_APP_SLUG: app.slug,
|
|
17
|
+
UBRIOT_IOS_BUNDLE_ID: app.iosBundleIdentifier,
|
|
18
|
+
UBRIOT_ANDROID_APPLICATION_ID: app.androidApplicationId,
|
|
19
|
+
};
|
|
20
|
+
const env = {};
|
|
21
|
+
for (const [key, value] of Object.entries(identity)) {
|
|
22
|
+
if (value?.trim())
|
|
23
|
+
env[key] = value.trim();
|
|
24
|
+
}
|
|
25
|
+
return env;
|
|
26
|
+
}
|
|
27
|
+
function describeBuildIdentity(env) {
|
|
28
|
+
return Object.entries(env).map(([key, value]) => `${key}=${value}`).join(" ") || "none";
|
|
29
|
+
}
|
|
30
|
+
function assertConfigMatchesApp(config, app) {
|
|
31
|
+
const checks = [
|
|
32
|
+
["ios.bundleIdentifier", config?.ios?.bundleIdentifier, app.iosBundleIdentifier],
|
|
33
|
+
["android.package", config?.android?.package, app.androidApplicationId],
|
|
34
|
+
];
|
|
35
|
+
for (const [field, local, expected] of checks) {
|
|
36
|
+
if (local && expected?.trim() && local !== expected.trim()) {
|
|
37
|
+
throw new Error(`App config ${field} is ${local}, but Ubriot app ${app.slug ?? ""} is ${expected.trim()}. Refusing to publish an update built for another app.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function assertUpdateMatchesApp(config, app, runtimeVersion) {
|
|
42
|
+
assertConfigMatchesApp(config, app);
|
|
43
|
+
const configuredSlug = config?.extra?.ubriot?.pushAppSlug;
|
|
44
|
+
if (typeof configuredSlug === "string" && configuredSlug.trim() && configuredSlug !== app.slug) {
|
|
45
|
+
throw new Error(`App config extra.ubriot.pushAppSlug is ${configuredSlug}, but Ubriot app is ${app.slug ?? "unknown"}. Refusing to publish an update built for another app.`);
|
|
46
|
+
}
|
|
47
|
+
const configuredRuntime = config?.runtimeVersion;
|
|
48
|
+
if (typeof configuredRuntime === "string" && configuredRuntime.trim() && configuredRuntime !== runtimeVersion) {
|
|
49
|
+
throw new Error(`App config runtimeVersion is ${configuredRuntime}, but the update targets ${runtimeVersion}. Refusing to publish a bundle with a different runtime.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function hasDynamicExpoConfig(projectDir) {
|
|
53
|
+
return DYNAMIC_EXPO_CONFIG_FILES.some((file) => (0, fs_1.existsSync)((0, path_1.join)(projectDir, file)));
|
|
54
|
+
}
|
|
55
|
+
function resolveExpoConfig(projectDir, identityEnv, spawn = child_process_1.spawnSync) {
|
|
56
|
+
const result = spawn("npx", ["expo", "config", "--json", "--type", "public"], {
|
|
57
|
+
cwd: projectDir,
|
|
58
|
+
env: (0, child_env_1.childProcessEnv)(identityEnv),
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
61
|
+
});
|
|
62
|
+
if (result.error)
|
|
63
|
+
throw result.error;
|
|
64
|
+
if (result.status !== 0)
|
|
65
|
+
throw new Error(`npx expo config failed with exit code ${result.status ?? "unknown"}: ${result.stderr.trim()}`);
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(result.stdout);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error(`npx expo config did not return JSON: ${result.stdout.slice(0, 200)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectProjectRuntime = detectProjectRuntime;
|
|
4
|
+
exports.localNodeProblem = localNodeProblem;
|
|
5
|
+
exports.fetchBuildPreflight = fetchBuildPreflight;
|
|
6
|
+
exports.renderBuildPreflight = renderBuildPreflight;
|
|
7
|
+
exports.buildPreflightDecision = buildPreflightDecision;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = require("path");
|
|
10
|
+
const SDK58_NODE_LINES = [
|
|
11
|
+
{ major: 22, minimum: [22, 13] },
|
|
12
|
+
{ major: 24, minimum: [24, 3] },
|
|
13
|
+
];
|
|
14
|
+
const SDK58_EVEN_MAJORS_FROM = 26;
|
|
15
|
+
function versionParts(version) {
|
|
16
|
+
const match = version.trim().replace(/^v/i, "").match(/^\d+(\.\d+)*/);
|
|
17
|
+
return match ? match[0].split(".").map(Number) : [];
|
|
18
|
+
}
|
|
19
|
+
function cleanVersion(value) {
|
|
20
|
+
if (typeof value !== "string")
|
|
21
|
+
return null;
|
|
22
|
+
return value.match(/(\d+\.\d+(\.\d+)?)/)?.[1] ?? value.match(/(\d+)/)?.[1] ?? null;
|
|
23
|
+
}
|
|
24
|
+
function readJson(path) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse((0, fs_1.readFileSync)(path, "utf8"));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function detectProjectRuntime(projectDir) {
|
|
33
|
+
const pkg = readJson((0, path_1.join)(projectDir, "package.json"));
|
|
34
|
+
const deps = { ...(pkg?.devDependencies ?? {}), ...(pkg?.dependencies ?? {}) };
|
|
35
|
+
const installed = (name) => cleanVersion(readJson((0, path_1.join)(projectDir, "node_modules", name, "package.json"))?.version);
|
|
36
|
+
const expo = installed("expo") ?? cleanVersion(deps.expo);
|
|
37
|
+
const reactNative = installed("react-native") ?? cleanVersion(deps["react-native"]);
|
|
38
|
+
return { expoSdk: expo ? versionParts(expo)[0] ?? null : null, reactNative };
|
|
39
|
+
}
|
|
40
|
+
function sdk58Reason(runtime) {
|
|
41
|
+
if (runtime.expoSdk !== null)
|
|
42
|
+
return runtime.expoSdk >= 58 ? `Expo SDK ${runtime.expoSdk}` : null;
|
|
43
|
+
if (!runtime.reactNative)
|
|
44
|
+
return null;
|
|
45
|
+
const [major, minor] = versionParts(runtime.reactNative);
|
|
46
|
+
return major === 0 && minor >= 88 ? `React Native 0.${minor}` : null;
|
|
47
|
+
}
|
|
48
|
+
function nodeSupported(version) {
|
|
49
|
+
const [major, minor = 0] = versionParts(version);
|
|
50
|
+
if (major === undefined)
|
|
51
|
+
return false;
|
|
52
|
+
const line = SDK58_NODE_LINES.find((l) => l.major === major);
|
|
53
|
+
if (line)
|
|
54
|
+
return minor >= line.minimum[1];
|
|
55
|
+
return major >= SDK58_EVEN_MAJORS_FROM && major % 2 === 0;
|
|
56
|
+
}
|
|
57
|
+
function localNodeProblem(runtime, nodeVersion) {
|
|
58
|
+
const reason = sdk58Reason(runtime);
|
|
59
|
+
if (!reason || nodeSupported(nodeVersion))
|
|
60
|
+
return null;
|
|
61
|
+
return `${reason} tooling needs Node.js 22.13 or newer on the 22 line (24.3+ or an even release from 26 also work), but this terminal runs Node.js ${nodeVersion}. Install it with nvm install 22, then run this command again.`;
|
|
62
|
+
}
|
|
63
|
+
async function fetchBuildPreflight(client, input) {
|
|
64
|
+
try {
|
|
65
|
+
const params = { platform: input.platform };
|
|
66
|
+
if (input.runtime.expoSdk !== null)
|
|
67
|
+
params.expoSdk = String(input.runtime.expoSdk);
|
|
68
|
+
if (input.runtime.reactNative)
|
|
69
|
+
params.reactNative = input.runtime.reactNative;
|
|
70
|
+
const { data } = await client.get(`/ubriot/apps/${encodeURIComponent(input.app)}/builds/preflight`, { params, timeout: 20000 });
|
|
71
|
+
if (!data || !Array.isArray(data.warnings) || !data.runtime)
|
|
72
|
+
return null;
|
|
73
|
+
return data;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function describeRuntime(runtime) {
|
|
80
|
+
if (runtime.expoSdk !== null)
|
|
81
|
+
return `Expo SDK ${runtime.expoSdk}${runtime.reactNative ? ` (React Native ${runtime.reactNative})` : ""}`;
|
|
82
|
+
return runtime.reactNative ? `React Native ${runtime.reactNative}` : null;
|
|
83
|
+
}
|
|
84
|
+
function renderBuildPreflight(result) {
|
|
85
|
+
const lines = [];
|
|
86
|
+
const detected = describeRuntime(result.runtime);
|
|
87
|
+
if (detected) {
|
|
88
|
+
const from = result.detectedFrom?.source === "last-build" && result.detectedFrom.buildId ? ` from build #${result.detectedFrom.buildId}` : "";
|
|
89
|
+
lines.push(`Detected ${detected}${from}.`);
|
|
90
|
+
}
|
|
91
|
+
for (const w of result.warnings)
|
|
92
|
+
lines.push(`${w.level === "warn" ? "Warning" : "Note"}: ${w.message}`);
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
function buildPreflightDecision(result, opts) {
|
|
96
|
+
if (!result || opts.yes || !opts.interactive)
|
|
97
|
+
return "continue";
|
|
98
|
+
return result.warnings.some((w) => w.level === "warn") ? "prompt" : "continue";
|
|
99
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PRIVATE_KEY_ENV = void 0;
|
|
4
|
+
exports.childProcessEnv = childProcessEnv;
|
|
5
|
+
exports.PRIVATE_KEY_ENV = "UBRIOT_OTA_PRIVATE_KEY";
|
|
6
|
+
function childProcessEnv(extra = {}, base = process.env) {
|
|
7
|
+
const env = { ...base, ...extra };
|
|
8
|
+
delete env[exports.PRIVATE_KEY_ENV];
|
|
9
|
+
return env;
|
|
10
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.API_KEYS_URL = void 0;
|
|
4
|
+
exports.envToken = envToken;
|
|
5
|
+
exports.readSavedConfig = readSavedConfig;
|
|
3
6
|
exports.readConfig = readConfig;
|
|
4
7
|
exports.writeConfig = writeConfig;
|
|
5
8
|
exports.requireAuth = requireAuth;
|
|
@@ -14,30 +17,37 @@ const defaults = {
|
|
|
14
17
|
refreshToken: null,
|
|
15
18
|
email: null,
|
|
16
19
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
exports.API_KEYS_URL = "https://ubriot.dev/dashboard/api-keys";
|
|
21
|
+
function envToken() {
|
|
22
|
+
return process.env.UBRIOT_TOKEN?.trim() || null;
|
|
23
|
+
}
|
|
24
|
+
function readSavedConfig() {
|
|
22
25
|
if (!(0, fs_1.existsSync)(CONFIG_FILE))
|
|
23
|
-
return
|
|
26
|
+
return { ...defaults };
|
|
24
27
|
try {
|
|
25
|
-
return
|
|
28
|
+
return { ...defaults, ...JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, "utf8")) };
|
|
26
29
|
}
|
|
27
30
|
catch {
|
|
28
|
-
return
|
|
31
|
+
return { ...defaults };
|
|
29
32
|
}
|
|
30
33
|
}
|
|
34
|
+
function readConfig() {
|
|
35
|
+
const saved = readSavedConfig();
|
|
36
|
+
const cfg = { ...saved, apiBase: process.env.UBRIOT_API_BASE?.trim() || saved.apiBase };
|
|
37
|
+
const token = envToken();
|
|
38
|
+
return token ? { ...cfg, token, refreshToken: null, email: null } : cfg;
|
|
39
|
+
}
|
|
31
40
|
function writeConfig(updates) {
|
|
32
|
-
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
33
|
-
const current =
|
|
34
|
-
(0, fs_1.writeFileSync)(CONFIG_FILE, JSON.stringify({ ...current, ...updates }, null, 2), "utf8");
|
|
41
|
+
(0, fs_1.mkdirSync)(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
42
|
+
const current = readSavedConfig();
|
|
43
|
+
(0, fs_1.writeFileSync)(CONFIG_FILE, JSON.stringify({ ...current, ...updates }, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
44
|
+
(0, fs_1.chmodSync)(CONFIG_FILE, 0o600);
|
|
35
45
|
}
|
|
36
46
|
function requireAuth() {
|
|
37
47
|
const cfg = readConfig();
|
|
38
48
|
if (!cfg.token) {
|
|
39
|
-
console.error(`Not logged in. Run: ubriot login --api-base ${cfg.apiBase}
|
|
40
|
-
process.exit(
|
|
49
|
+
console.error(`Not logged in. Run: ubriot login --api-base ${cfg.apiBase}, or set UBRIOT_TOKEN to an API key from ${exports.API_KEYS_URL}.`);
|
|
50
|
+
process.exit(2);
|
|
41
51
|
}
|
|
42
52
|
return cfg;
|
|
43
53
|
}
|