eoas 3.0.5 → 3.1.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 +5 -5
- package/bin/dev.js +3 -0
- package/bin/run.js +3 -0
- package/dist/commands/generate-certs.js +22 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/publish.js +55 -31
- package/dist/commands/republish.js +87 -27
- package/dist/commands/server/init.d.ts +17 -0
- package/dist/commands/server/init.js +654 -0
- package/dist/commands/server/validate.d.ts +10 -0
- package/dist/commands/server/validate.js +115 -0
- package/dist/lib/assets.d.ts +30 -8
- package/dist/lib/assets.js +158 -4
- package/dist/lib/auth.js +2 -1
- package/dist/lib/log.d.ts +8 -2
- package/dist/lib/log.js +47 -31
- package/dist/lib/ora.d.ts +10 -9
- package/dist/lib/ora.js +49 -88
- package/dist/lib/packageRunner.js +2 -1
- package/dist/lib/prompts.d.ts +32 -0
- package/dist/lib/prompts.js +86 -1
- package/dist/lib/serverConfig/choices.d.ts +62 -0
- package/dist/lib/serverConfig/choices.js +65 -0
- package/dist/lib/serverConfig/envCatalog.d.ts +41 -0
- package/dist/lib/serverConfig/envCatalog.js +582 -0
- package/dist/lib/serverConfig/helmValues.d.ts +22 -0
- package/dist/lib/serverConfig/helmValues.js +281 -0
- package/dist/lib/serverConfig/passwordPolicy.d.ts +3 -0
- package/dist/lib/serverConfig/passwordPolicy.js +36 -0
- package/dist/lib/serverUpdates.d.ts +45 -0
- package/dist/lib/serverUpdates.js +108 -0
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +18 -14
- package/package.json +8 -6
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
6
|
+
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
7
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
8
|
+
const log_1 = tslib_1.__importDefault(require("../../lib/log"));
|
|
9
|
+
const envCatalog_1 = require("../../lib/serverConfig/envCatalog");
|
|
10
|
+
const helmValues_1 = require("../../lib/serverConfig/helmValues");
|
|
11
|
+
/** A single document can carry either half of the pair, or both when merged. */
|
|
12
|
+
function classifyInto(docs, doc) {
|
|
13
|
+
if (!docs.values && (0, helmValues_1.looksLikeChartValues)(doc)) {
|
|
14
|
+
docs.values = doc;
|
|
15
|
+
}
|
|
16
|
+
if (!docs.secretEnv) {
|
|
17
|
+
docs.secretEnv = (0, helmValues_1.extractSecretEnv)(doc);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Picks the Helm pair out of a directory, canonical file names first. */
|
|
21
|
+
async function collectHelmDocs(dir, docs, skip) {
|
|
22
|
+
const names = (await fs_extra_1.default.readdir(dir))
|
|
23
|
+
.filter(name => /\.ya?ml$/i.test(name))
|
|
24
|
+
.sort((a, b) => {
|
|
25
|
+
const canonical = (name) => name === 'values.yaml' || name === 'secrets.yaml' ? 0 : 1;
|
|
26
|
+
return canonical(a) - canonical(b) || a.localeCompare(b);
|
|
27
|
+
});
|
|
28
|
+
for (const name of names) {
|
|
29
|
+
const filePath = path_1.default.join(dir, name);
|
|
30
|
+
if (filePath === skip || !(await fs_extra_1.default.stat(filePath)).isFile()) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
classifyInto(docs, (0, helmValues_1.parseYamlFile)(await fs_extra_1.default.readFile(filePath, 'utf8')));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Not a single YAML mapping (multi-doc, template output...): not ours.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class ServerValidate extends core_1.Command {
|
|
42
|
+
static args = {
|
|
43
|
+
file: core_1.Args.string({
|
|
44
|
+
description: 'Path to a server .env file, a Helm values or secrets YAML, or the directory holding the Helm pair',
|
|
45
|
+
required: true,
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
static description = 'Check a server configuration (.env file or Helm values/secrets pair) for missing or inconsistent variables';
|
|
49
|
+
static examples = [
|
|
50
|
+
'<%= config.bin %> <%= command.id %> .env.xprem',
|
|
51
|
+
'<%= config.bin %> <%= command.id %> xprem-helm',
|
|
52
|
+
'<%= config.bin %> <%= command.id %> xprem-helm/values.yaml',
|
|
53
|
+
];
|
|
54
|
+
static flags = {};
|
|
55
|
+
async run() {
|
|
56
|
+
const { args } = await this.parse(ServerValidate);
|
|
57
|
+
const target = path_1.default.resolve(process.cwd(), args.file);
|
|
58
|
+
if (!(await fs_extra_1.default.pathExists(target))) {
|
|
59
|
+
log_1.default.error(`File not found: ${target}`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
let issues;
|
|
63
|
+
if ((await fs_extra_1.default.stat(target)).isDirectory()) {
|
|
64
|
+
const docs = {};
|
|
65
|
+
await collectHelmDocs(target, docs);
|
|
66
|
+
if (!docs.values && !docs.secretEnv) {
|
|
67
|
+
log_1.default.error(`No Helm values or secrets YAML found in ${target}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
issues = (0, helmValues_1.validateHelmPair)(docs.values, docs.secretEnv);
|
|
71
|
+
}
|
|
72
|
+
else if (/\.ya?ml$/i.test(target)) {
|
|
73
|
+
let doc;
|
|
74
|
+
try {
|
|
75
|
+
doc = (0, helmValues_1.parseYamlFile)(await fs_extra_1.default.readFile(target, 'utf8'));
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
log_1.default.error(`Could not parse ${args.file} as YAML: ${e instanceof Error ? e.message : e}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const docs = {};
|
|
83
|
+
classifyInto(docs, doc);
|
|
84
|
+
if (!docs.values && !docs.secretEnv) {
|
|
85
|
+
log_1.default.error(`${args.file} looks like neither a values file for the xprem Helm chart nor a secrets overlay (secretEnv map).`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
// The other half of the pair is picked up from the same directory.
|
|
89
|
+
await collectHelmDocs(path_1.default.dirname(target), docs, target);
|
|
90
|
+
issues = (0, helmValues_1.validateHelmPair)(docs.values, docs.secretEnv);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
issues = (0, envCatalog_1.validateEnvMap)((0, envCatalog_1.parseEnvFile)(await fs_extra_1.default.readFile(target, 'utf8')));
|
|
94
|
+
}
|
|
95
|
+
const errors = issues.filter(issue => issue.level === 'error');
|
|
96
|
+
const warnings = issues.filter(issue => issue.level === 'warning');
|
|
97
|
+
for (const issue of errors) {
|
|
98
|
+
log_1.default.log(`${chalk_1.default.red('✖')} ${issue.message}`);
|
|
99
|
+
}
|
|
100
|
+
for (const issue of warnings) {
|
|
101
|
+
log_1.default.log(`${chalk_1.default.yellow('⚠')} ${issue.message}`);
|
|
102
|
+
}
|
|
103
|
+
log_1.default.newLine();
|
|
104
|
+
if (errors.length > 0) {
|
|
105
|
+
log_1.default.fail(`${args.file}: ${errors.length} error(s), ${warnings.length} warning(s). The server would not boot with this configuration.`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
if (warnings.length > 0) {
|
|
109
|
+
log_1.default.succeed(`${args.file}: no errors, ${warnings.length} warning(s).`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
log_1.default.succeed(`${args.file}: everything looks good.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.default = ServerValidate;
|
package/dist/lib/assets.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import Joi from 'joi';
|
|
|
2
2
|
import { Credentials } from './auth';
|
|
3
3
|
import { RequestedPlatform } from './expoConfig';
|
|
4
4
|
export declare const MetadataJoi: Joi.ObjectSchema<any>;
|
|
5
|
-
interface AssetToUpload {
|
|
5
|
+
export interface AssetToUpload {
|
|
6
6
|
path: string;
|
|
7
7
|
name: string;
|
|
8
8
|
ext: string;
|
|
@@ -14,8 +14,34 @@ export interface RequestUploadUrlItem {
|
|
|
14
14
|
filePath: string;
|
|
15
15
|
headers?: Record<string, string>;
|
|
16
16
|
}
|
|
17
|
+
export interface RequestUploadUrlsResponse {
|
|
18
|
+
uploadRequests: RequestUploadUrlItem[];
|
|
19
|
+
updateId: string;
|
|
20
|
+
rolloutPercentage?: number;
|
|
21
|
+
publishGroup?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const RequestUploadUrlsResponseJoi: Joi.ObjectSchema<any>;
|
|
24
|
+
export interface ResolvedUploadRequest {
|
|
25
|
+
item: RequestUploadUrlItem;
|
|
26
|
+
absolutePath: string;
|
|
27
|
+
manifestEntry: AssetToUpload;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Maps every upload request of ONE server response onto a file the CLI itself
|
|
31
|
+
* exported, and throws on anything it cannot account for. This runs to
|
|
32
|
+
* completion before the first byte is read: a single bad entry aborts the whole
|
|
33
|
+
* publish rather than uploading the files that happened to be fine.
|
|
34
|
+
*
|
|
35
|
+
* Call it once per response: `eoas publish --platform all` requests upload URLs
|
|
36
|
+
* per runtime version, and every response legitimately names the same files.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveUploadRequests({ uploadRequests, exportDir, manifest, }: {
|
|
39
|
+
uploadRequests: RequestUploadUrlItem[];
|
|
40
|
+
exportDir: string;
|
|
41
|
+
manifest: AssetToUpload[];
|
|
42
|
+
}): Promise<ResolvedUploadRequest[]>;
|
|
17
43
|
export declare function activeRolloutConflictMessage(branch: string): string;
|
|
18
|
-
export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, branch, }: {
|
|
44
|
+
export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, publishGroup, branch, }: {
|
|
19
45
|
body: {
|
|
20
46
|
fileNames: string[];
|
|
21
47
|
};
|
|
@@ -26,10 +52,6 @@ export declare function requestUploadUrls({ body, requestUploadUrl, auth, runtim
|
|
|
26
52
|
commitHash?: string;
|
|
27
53
|
message?: string;
|
|
28
54
|
rolloutPercentage?: number;
|
|
55
|
+
publishGroup?: string;
|
|
29
56
|
branch: string;
|
|
30
|
-
}): Promise<
|
|
31
|
-
uploadRequests: RequestUploadUrlItem[];
|
|
32
|
-
updateId: string;
|
|
33
|
-
rolloutPercentage?: number;
|
|
34
|
-
}>;
|
|
35
|
-
export {};
|
|
57
|
+
}): Promise<RequestUploadUrlsResponse>;
|
package/dist/lib/assets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.requestUploadUrls = exports.activeRolloutConflictMessage = exports.computeFilesRequests = exports.MetadataJoi = void 0;
|
|
3
|
+
exports.requestUploadUrls = exports.activeRolloutConflictMessage = exports.resolveUploadRequests = exports.RequestUploadUrlsResponseJoi = exports.computeFilesRequests = exports.MetadataJoi = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
|
|
6
6
|
const joi_1 = tslib_1.__importDefault(require("joi"));
|
|
@@ -73,11 +73,155 @@ function computeFilesRequests(projectDir, outputDir, requestedPlatform) {
|
|
|
73
73
|
return assets;
|
|
74
74
|
}
|
|
75
75
|
exports.computeFilesRequests = computeFilesRequests;
|
|
76
|
+
// The server dictates which local files the CLI opens and where their bytes are
|
|
77
|
+
// sent, so its answer is untrusted input: a hostile or compromised server that
|
|
78
|
+
// gets a forged filePath past us reads any file it wants off the developer or CI
|
|
79
|
+
// machine. Every field is typed here, and the paths are checked against the
|
|
80
|
+
// export manifest in resolveUploadRequests before anything is opened.
|
|
81
|
+
const uploadRequestHeadersJoi = joi_1.default.object()
|
|
82
|
+
.pattern(
|
|
83
|
+
// RFC 7230 header field-name, and a value that cannot smuggle a CRLF.
|
|
84
|
+
/^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/, joi_1.default.string()
|
|
85
|
+
.allow('')
|
|
86
|
+
.pattern(/^[^\r\n]*$/))
|
|
87
|
+
.optional();
|
|
88
|
+
const uploadRequestJoi = joi_1.default.object({
|
|
89
|
+
requestUploadUrl: joi_1.default.string()
|
|
90
|
+
.uri({ scheme: ['http', 'https'] })
|
|
91
|
+
.required(),
|
|
92
|
+
fileName: joi_1.default.string().required(),
|
|
93
|
+
filePath: joi_1.default.string().required(),
|
|
94
|
+
headers: uploadRequestHeadersJoi,
|
|
95
|
+
// Unknown keys are tolerated so a newer server can add fields without
|
|
96
|
+
// breaking older CLIs; nothing reads them.
|
|
97
|
+
}).unknown(true);
|
|
98
|
+
exports.RequestUploadUrlsResponseJoi = joi_1.default.object({
|
|
99
|
+
// The server marshals updateId from an int64, so it arrives as a JSON number;
|
|
100
|
+
// older or third-party servers may send it as a string. Normalized to a string
|
|
101
|
+
// below, which is what the markUpdateAsUploaded query parameter needs.
|
|
102
|
+
updateId: joi_1.default.alternatives().try(joi_1.default.string(), joi_1.default.number()).required(),
|
|
103
|
+
uploadRequests: joi_1.default.array().items(uploadRequestJoi).required(),
|
|
104
|
+
rolloutPercentage: joi_1.default.number().optional(),
|
|
105
|
+
publishGroup: joi_1.default.string().optional(),
|
|
106
|
+
})
|
|
107
|
+
.required()
|
|
108
|
+
.unknown(true);
|
|
109
|
+
function isLoopbackHost(hostname) {
|
|
110
|
+
const host = hostname.replace(/^\[/, '').replace(/\]$/, '').toLowerCase();
|
|
111
|
+
// Deliberately does not accept *.localhost: resolvers are free to answer it
|
|
112
|
+
// from DNS, which would let a remote host claim the plain-HTTP exemption.
|
|
113
|
+
return host === 'localhost' || host === '::1' || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
114
|
+
}
|
|
115
|
+
// Escape hatch for the deployments that legitimately serve upload URLs over
|
|
116
|
+
// plain HTTP on a non-loopback host: a MinIO or S3-compatible endpoint set
|
|
117
|
+
// through AWS_BASE_ENDPOINT, or a local bucket whose BASE_URL is an internal
|
|
118
|
+
// hostname. It only lifts the transport requirement, never a path check.
|
|
119
|
+
const INSECURE_UPLOAD_URLS_ENV = 'EOAS_ALLOW_INSECURE_UPLOAD_URLS';
|
|
120
|
+
function insecureUploadUrlsAllowed() {
|
|
121
|
+
const value = process.env[INSECURE_UPLOAD_URLS_ENV];
|
|
122
|
+
return value === '1' || value?.toLowerCase() === 'true';
|
|
123
|
+
}
|
|
124
|
+
function assertSafeUploadUrl(requestUploadUrl) {
|
|
125
|
+
let url;
|
|
126
|
+
try {
|
|
127
|
+
url = new URL(requestUploadUrl);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
throw new Error(`The server returned an unusable upload URL: ${requestUploadUrl}`);
|
|
131
|
+
}
|
|
132
|
+
if (url.protocol === 'https:') {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (url.protocol === 'http:' && isLoopbackHost(url.hostname)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (url.protocol === 'http:' && insecureUploadUrlsAllowed()) {
|
|
139
|
+
log_1.default.warn(`Uploading to ${url.origin} over plain HTTP because ${INSECURE_UPLOAD_URLS_ENV} is set. Your update artifacts travel unencrypted.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Refusing to upload to ${url.origin}: update artifacts may only be sent over HTTPS (plain HTTP is allowed for loopback addresses only). Set ${INSECURE_UPLOAD_URLS_ENV}=1 if your storage endpoint is intentionally served over HTTP.`);
|
|
143
|
+
}
|
|
144
|
+
function assertRelativePathShape(filePath) {
|
|
145
|
+
if (!filePath || filePath.includes('\0')) {
|
|
146
|
+
throw new Error('The server returned an empty or malformed file path.');
|
|
147
|
+
}
|
|
148
|
+
// win32.isAbsolute also covers the posix cases, drive letters and UNC paths.
|
|
149
|
+
if (path_1.default.isAbsolute(filePath) || path_1.default.win32.isAbsolute(filePath)) {
|
|
150
|
+
throw new Error(`Refusing to upload the absolute path "${filePath}" requested by the server.`);
|
|
151
|
+
}
|
|
152
|
+
if (filePath.split(/[/\\]/).some(segment => segment === '..')) {
|
|
153
|
+
throw new Error(`Refusing to upload "${filePath}": the server requested a path outside the export directory.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Maps every upload request of ONE server response onto a file the CLI itself
|
|
158
|
+
* exported, and throws on anything it cannot account for. This runs to
|
|
159
|
+
* completion before the first byte is read: a single bad entry aborts the whole
|
|
160
|
+
* publish rather than uploading the files that happened to be fine.
|
|
161
|
+
*
|
|
162
|
+
* Call it once per response: `eoas publish --platform all` requests upload URLs
|
|
163
|
+
* per runtime version, and every response legitimately names the same files.
|
|
164
|
+
*/
|
|
165
|
+
async function resolveUploadRequests({ uploadRequests, exportDir, manifest, }) {
|
|
166
|
+
const manifestByPath = new Map(manifest.map(entry => [entry.path, entry]));
|
|
167
|
+
// Canonical root, so a symlinked export directory (or /tmp on macOS) does not
|
|
168
|
+
// make every containment check fail below.
|
|
169
|
+
let exportRoot;
|
|
170
|
+
try {
|
|
171
|
+
exportRoot = await fs_extra_1.default.realpath(exportDir);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw new Error(`Export directory ${exportDir} could not be resolved.`);
|
|
175
|
+
}
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const resolved = [];
|
|
178
|
+
for (const item of uploadRequests) {
|
|
179
|
+
assertSafeUploadUrl(item.requestUploadUrl);
|
|
180
|
+
assertRelativePathShape(item.filePath);
|
|
181
|
+
const manifestEntry = manifestByPath.get(item.filePath);
|
|
182
|
+
if (!manifestEntry) {
|
|
183
|
+
throw new Error(`Refusing to upload "${item.filePath}": the server asked for a file that is not part of this export.`);
|
|
184
|
+
}
|
|
185
|
+
if (item.fileName !== path_1.default.basename(item.filePath)) {
|
|
186
|
+
throw new Error(`Refusing to upload "${item.filePath}": the server returned the mismatched name "${item.fileName}".`);
|
|
187
|
+
}
|
|
188
|
+
if (seen.has(item.filePath)) {
|
|
189
|
+
throw new Error(`The server requested "${item.filePath}" more than once.`);
|
|
190
|
+
}
|
|
191
|
+
seen.add(item.filePath);
|
|
192
|
+
const absolutePath = path_1.default.resolve(exportRoot, item.filePath);
|
|
193
|
+
// Unreachable on POSIX: a path with no '..' segment and no leading separator
|
|
194
|
+
// cannot resolve out of the root. Kept for the Windows drive-relative case
|
|
195
|
+
// ("C:file" when the export root sits on another drive) and as a backstop if
|
|
196
|
+
// the checks above are ever relaxed.
|
|
197
|
+
if (absolutePath !== exportRoot && !absolutePath.startsWith(exportRoot + path_1.default.sep)) {
|
|
198
|
+
throw new Error(`Refusing to upload "${item.filePath}": it resolves outside the export directory.`);
|
|
199
|
+
}
|
|
200
|
+
let realPath;
|
|
201
|
+
try {
|
|
202
|
+
realPath = await fs_extra_1.default.realpath(absolutePath);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
throw new Error(`File ${item.filePath} not found in the export directory.`);
|
|
206
|
+
}
|
|
207
|
+
// The root is already canonical, so any difference here means a symlink was
|
|
208
|
+
// traversed, either as the file itself or as one of its parent directories.
|
|
209
|
+
if (realPath !== absolutePath) {
|
|
210
|
+
throw new Error(`Refusing to upload "${item.filePath}": it is or goes through a symlink.`);
|
|
211
|
+
}
|
|
212
|
+
if (!(await fs_extra_1.default.lstat(absolutePath)).isFile()) {
|
|
213
|
+
throw new Error(`Refusing to upload "${item.filePath}": it is not a regular file.`);
|
|
214
|
+
}
|
|
215
|
+
resolved.push({ item, absolutePath, manifestEntry });
|
|
216
|
+
}
|
|
217
|
+
return resolved;
|
|
218
|
+
}
|
|
219
|
+
exports.resolveUploadRequests = resolveUploadRequests;
|
|
76
220
|
function activeRolloutConflictMessage(branch) {
|
|
77
221
|
return `A progressive rollout is already active for branch "${branch}" on this runtime version. End or revert it from the dashboard before publishing a new update.`;
|
|
78
222
|
}
|
|
79
223
|
exports.activeRolloutConflictMessage = activeRolloutConflictMessage;
|
|
80
|
-
async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, branch, }) {
|
|
224
|
+
async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion, platform, commitHash, message, rolloutPercentage, publishGroup, branch, }) {
|
|
81
225
|
const uploadUrl = new URL(requestUploadUrl);
|
|
82
226
|
uploadUrl.searchParams.set('runtimeVersion', runtimeVersion);
|
|
83
227
|
uploadUrl.searchParams.set('platform', platform);
|
|
@@ -85,6 +229,9 @@ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion,
|
|
|
85
229
|
if (rolloutPercentage !== undefined) {
|
|
86
230
|
uploadUrl.searchParams.set('rolloutPercentage', String(rolloutPercentage));
|
|
87
231
|
}
|
|
232
|
+
if (publishGroup) {
|
|
233
|
+
uploadUrl.searchParams.set('publishGroup', publishGroup);
|
|
234
|
+
}
|
|
88
235
|
const requestBody = { ...body };
|
|
89
236
|
if (message) {
|
|
90
237
|
requestBody.message = message;
|
|
@@ -105,12 +252,19 @@ async function requestUploadUrls({ body, requestUploadUrl, auth, runtimeVersion,
|
|
|
105
252
|
throw new Error(`Failed to request upload URL: ${text}`);
|
|
106
253
|
}
|
|
107
254
|
const json = await response.json();
|
|
255
|
+
// Joi's sanitized value, not the raw payload: it is the object the schema
|
|
256
|
+
// actually vouched for, with inherited keys such as __proto__ dropped.
|
|
257
|
+
const { error, value } = exports.RequestUploadUrlsResponseJoi.validate(json);
|
|
258
|
+
if (error) {
|
|
259
|
+
throw new Error(`The server returned an invalid upload response: ${error.message}`);
|
|
260
|
+
}
|
|
261
|
+
const validated = { ...value, updateId: String(value.updateId) };
|
|
108
262
|
// An old server silently ignores unknown query params, so a missing echo means
|
|
109
263
|
// the rollout was not applied even though the flag was set. Abort before any
|
|
110
264
|
// file is uploaded: continuing would finalize a full 100% publish.
|
|
111
|
-
if (rolloutPercentage !== undefined &&
|
|
265
|
+
if (rolloutPercentage !== undefined && validated.rolloutPercentage === undefined) {
|
|
112
266
|
throw new Error('The server ignored --rollout-percentage and would publish to 100% of devices. Update the server to a version that supports progressive rollouts, or publish without --rollout-percentage.');
|
|
113
267
|
}
|
|
114
|
-
return
|
|
268
|
+
return validated;
|
|
115
269
|
}
|
|
116
270
|
exports.requestUploadUrls = requestUploadUrls;
|
package/dist/lib/auth.js
CHANGED
|
@@ -19,8 +19,9 @@ function retrieveCredentials() {
|
|
|
19
19
|
}
|
|
20
20
|
exports.retrieveCredentials = retrieveCredentials;
|
|
21
21
|
function validateCredentials(credentials) {
|
|
22
|
-
if (!credentials)
|
|
22
|
+
if (!credentials) {
|
|
23
23
|
return false;
|
|
24
|
+
}
|
|
24
25
|
return !!(credentials.token || credentials.sessionSecret);
|
|
25
26
|
}
|
|
26
27
|
exports.validateCredentials = validateCredentials;
|
package/dist/lib/log.d.ts
CHANGED
|
@@ -12,10 +12,16 @@ export default class Log {
|
|
|
12
12
|
static succeed(message: string): void;
|
|
13
13
|
static withTick(...args: any[]): void;
|
|
14
14
|
static withInfo(...args: any[]): void;
|
|
15
|
-
|
|
15
|
+
/** Opens a clack session frame; every line after it hangs on the gutter. */
|
|
16
|
+
static intro(title: string): void;
|
|
17
|
+
/** Closes the clack session frame opened by intro. */
|
|
18
|
+
static outro(message: string): void;
|
|
19
|
+
static note(content: string, title?: string): void;
|
|
20
|
+
static cancel(message: string): void;
|
|
21
|
+
private static write;
|
|
16
22
|
private static withTextColor;
|
|
17
23
|
private static isLastLineNewLine;
|
|
18
|
-
private static
|
|
24
|
+
private static track;
|
|
19
25
|
}
|
|
20
26
|
/**
|
|
21
27
|
* Prints a link for given URL, using text if provided, otherwise text is just the URL.
|
package/dist/lib/log.js
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.learnMore = exports.link = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
|
-
//
|
|
5
|
+
// Rendering for every command's output, drawn with the @clack/prompts
|
|
6
|
+
// primitives so the whole CLI shares one visual identity (gutter, symbols,
|
|
7
|
+
// notes). The static API is kept from the original eas-cli logger so call
|
|
8
|
+
// sites did not have to change.
|
|
9
|
+
const clack = tslib_1.__importStar(require("@clack/prompts"));
|
|
6
10
|
const chalk_1 = tslib_1.__importDefault(require("chalk"));
|
|
7
|
-
const figures_1 = tslib_1.__importDefault(require("figures"));
|
|
8
11
|
const getenv_1 = require("getenv");
|
|
9
|
-
const log_symbols_1 = tslib_1.__importDefault(require("log-symbols"));
|
|
10
12
|
const terminal_link_1 = tslib_1.__importDefault(require("terminal-link"));
|
|
13
|
+
const util_1 = require("util");
|
|
11
14
|
class Log {
|
|
12
15
|
static isDebug = (0, getenv_1.boolish)('DEBUG', false);
|
|
13
16
|
static log(...args) {
|
|
14
|
-
Log.
|
|
17
|
+
Log.write((0, util_1.format)(...args));
|
|
15
18
|
}
|
|
16
19
|
static newLine() {
|
|
17
|
-
Log.
|
|
20
|
+
Log.write('');
|
|
21
|
+
Log.isLastLineNewLine = true;
|
|
18
22
|
}
|
|
19
23
|
static addNewLineIfNone() {
|
|
20
24
|
if (!Log.isLastLineNewLine) {
|
|
@@ -22,56 +26,68 @@ class Log {
|
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
static error(...args) {
|
|
25
|
-
Log.
|
|
29
|
+
Log.track();
|
|
30
|
+
clack.log.error(Log.withTextColor(args, chalk_1.default.red).join(' '));
|
|
26
31
|
}
|
|
27
32
|
static warn(...args) {
|
|
28
|
-
Log.
|
|
33
|
+
Log.track();
|
|
34
|
+
clack.log.warn(Log.withTextColor(args, chalk_1.default.yellow).join(' '));
|
|
29
35
|
}
|
|
30
36
|
static debug(...args) {
|
|
31
37
|
if (Log.isDebug) {
|
|
32
|
-
Log.
|
|
38
|
+
Log.write((0, util_1.format)(...args));
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
static gray(...args) {
|
|
36
|
-
Log.
|
|
42
|
+
Log.write(Log.withTextColor(args, chalk_1.default.gray).join(' '));
|
|
37
43
|
}
|
|
38
44
|
static warnDeprecatedFlag(flag, message) {
|
|
39
45
|
Log.warn(`› ${chalk_1.default.bold('--' + flag)} flag is deprecated. ${message}`);
|
|
40
46
|
}
|
|
41
47
|
static fail(message) {
|
|
42
|
-
Log.
|
|
48
|
+
Log.track();
|
|
49
|
+
clack.log.error(message);
|
|
43
50
|
}
|
|
44
51
|
static succeed(message) {
|
|
45
|
-
Log.
|
|
52
|
+
Log.track();
|
|
53
|
+
clack.log.success(message);
|
|
46
54
|
}
|
|
47
55
|
static withTick(...args) {
|
|
48
|
-
Log.
|
|
56
|
+
Log.track();
|
|
57
|
+
clack.log.success((0, util_1.format)(...args));
|
|
49
58
|
}
|
|
50
59
|
static withInfo(...args) {
|
|
51
|
-
Log.
|
|
60
|
+
Log.track();
|
|
61
|
+
clack.log.info((0, util_1.format)(...args));
|
|
52
62
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
/** Opens a clack session frame; every line after it hangs on the gutter. */
|
|
64
|
+
static intro(title) {
|
|
65
|
+
Log.track();
|
|
66
|
+
clack.intro(title);
|
|
67
|
+
}
|
|
68
|
+
/** Closes the clack session frame opened by intro. */
|
|
69
|
+
static outro(message) {
|
|
70
|
+
Log.track();
|
|
71
|
+
clack.outro(message);
|
|
72
|
+
}
|
|
73
|
+
static note(content, title) {
|
|
74
|
+
Log.track();
|
|
75
|
+
clack.note(content, title);
|
|
76
|
+
}
|
|
77
|
+
static cancel(message) {
|
|
78
|
+
Log.track();
|
|
79
|
+
clack.cancel(message);
|
|
80
|
+
}
|
|
81
|
+
static write(text) {
|
|
82
|
+
Log.track(text);
|
|
83
|
+
clack.log.message(text);
|
|
57
84
|
}
|
|
58
85
|
static withTextColor(args, chalkColor) {
|
|
59
|
-
return args.map(arg => chalkColor(arg));
|
|
86
|
+
return args.map(arg => chalkColor((0, util_1.format)(arg)));
|
|
60
87
|
}
|
|
61
88
|
static isLastLineNewLine = false;
|
|
62
|
-
static
|
|
63
|
-
|
|
64
|
-
Log.isLastLineNewLine = true;
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
const lastArg = args[args.length - 1];
|
|
68
|
-
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
|
|
69
|
-
Log.isLastLineNewLine = true;
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
Log.isLastLineNewLine = false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
89
|
+
static track(text) {
|
|
90
|
+
Log.isLastLineNewLine = text !== undefined && text === '';
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
93
|
exports.default = Log;
|
package/dist/lib/ora.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
export type Spinner = {
|
|
2
|
+
start(text?: string): Spinner;
|
|
3
|
+
succeed(text?: string): Spinner;
|
|
4
|
+
fail(text?: string): Spinner;
|
|
5
|
+
warn(text?: string): Spinner;
|
|
6
|
+
stop(): Spinner;
|
|
7
|
+
};
|
|
8
|
+
export declare function ora(options?: string | {
|
|
9
|
+
text?: string;
|
|
10
|
+
}): Spinner;
|