@condorinst/mobile-bridge 0.21.0 → 0.22.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/AGENTS.md +10 -1
- package/CHANGELOG.md +23 -0
- package/README.md +22 -6
- package/contract/bridge-contract.json +4 -3
- package/dist/cli.cjs +514 -15
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +510 -11
- package/dist/cli.js.map +1 -1
- package/docs/agent-guide.md +3 -3
- package/docs/bridge-contract.md +1 -1
- package/docs/client-guide.md +7 -5
- package/docs/device-testing.md +52 -22
- package/docs/pilot-onboarding.md +10 -6
- package/examples/minimal-home/package.json +1 -1
- package/llms-full.txt +84 -39
- package/llms.txt +15 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { mkdirSync, writeFileSync } from "fs";
|
|
5
|
-
import { dirname as dirname3, resolve as
|
|
5
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
6
|
+
|
|
7
|
+
// package.json
|
|
8
|
+
var version = "0.22.0";
|
|
9
|
+
|
|
10
|
+
// src/canvas-admin/command.ts
|
|
11
|
+
import { createHash as createHash2 } from "crypto";
|
|
12
|
+
import { existsSync, readFileSync as readFileSync2, statSync } from "fs";
|
|
13
|
+
import { extname as extname2, join, resolve as resolve2 } from "path";
|
|
6
14
|
|
|
7
15
|
// src/cli-core.ts
|
|
8
16
|
import { createHash } from "crypto";
|
|
@@ -348,12 +356,15 @@ var USAGE = `condor-mobile-bridge \u2014 tooling for Condor Canvas client bundle
|
|
|
348
356
|
Usage:
|
|
349
357
|
npx condor-mobile-bridge build -i <html> [-o <zip>]
|
|
350
358
|
npx condor-mobile-bridge validate -i <zip|html> [--strict]
|
|
359
|
+
npx condor-mobile-bridge canvas publish|list|revoke ... (see: canvas --help)
|
|
351
360
|
npx condor-mobile-bridge dev -i <html|zip|dir> [--scenario <name>] [--native-version <v|null>] [--port <n>]
|
|
352
361
|
|
|
353
362
|
Commands:
|
|
354
363
|
build Inline a dist/index.html into a self-contained .zip.
|
|
355
364
|
validate Check a bundle against every rule the Canvas app enforces, plus
|
|
356
365
|
the APIs the sandbox and the CSP block at runtime.
|
|
366
|
+
canvas Publish, list and revoke the bundle of your company in the
|
|
367
|
+
Condor Canvas app (interactive login). See: canvas --help.
|
|
357
368
|
dev Serve the bundle in a browser harness that behaves like the app:
|
|
358
369
|
same sandbox, shell and policy, and a simulated native side.
|
|
359
370
|
Re-reads the input on every reload. Scenarios: default,
|
|
@@ -372,10 +383,477 @@ Options:
|
|
|
372
383
|
reach the native side.
|
|
373
384
|
-h, --help Show this help.`;
|
|
374
385
|
|
|
386
|
+
// src/canvas-admin/client.ts
|
|
387
|
+
var CANVAS_ADMIN_API = {
|
|
388
|
+
prod: "https://api.sleepdiary.condorapps.net",
|
|
389
|
+
dev: "https://dev.sleepdiary.condorapps.net"
|
|
390
|
+
};
|
|
391
|
+
var CanvasAdminError = class extends Error {
|
|
392
|
+
constructor(message, status, reason) {
|
|
393
|
+
super(message);
|
|
394
|
+
this.status = status;
|
|
395
|
+
this.reason = reason;
|
|
396
|
+
this.name = "CanvasAdminError";
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
function tokenFrom(headers) {
|
|
400
|
+
const values = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : [headers.get("set-cookie") ?? ""];
|
|
401
|
+
for (const value of values) {
|
|
402
|
+
const match = /(?:^|[,;\s])token=([^;,\s]*)/.exec(value);
|
|
403
|
+
if (match?.[1]) return match[1];
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
async function reasonFrom(response) {
|
|
408
|
+
try {
|
|
409
|
+
const body = await response.json();
|
|
410
|
+
for (const key of ["error", "message", "description", "code"]) {
|
|
411
|
+
const value = body[key];
|
|
412
|
+
if (typeof value === "string" && value) return value;
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
var CanvasAdminClient = class {
|
|
419
|
+
constructor(opts) {
|
|
420
|
+
this.token = null;
|
|
421
|
+
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
422
|
+
this.userAgent = opts.userAgent;
|
|
423
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
424
|
+
this.timeoutMs = opts.timeoutMs ?? 6e4;
|
|
425
|
+
}
|
|
426
|
+
async request(path, init = {}) {
|
|
427
|
+
const headers = new Headers(init.headers);
|
|
428
|
+
headers.set("user-agent", this.userAgent);
|
|
429
|
+
headers.set("accept", "application/json");
|
|
430
|
+
if (this.token) headers.set("cookie", `token=${this.token}`);
|
|
431
|
+
try {
|
|
432
|
+
return await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
433
|
+
...init,
|
|
434
|
+
headers,
|
|
435
|
+
redirect: "manual",
|
|
436
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
437
|
+
});
|
|
438
|
+
} catch (err) {
|
|
439
|
+
throw new CanvasAdminError(
|
|
440
|
+
`Could not reach ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
441
|
+
0,
|
|
442
|
+
null
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async fail(response, what) {
|
|
447
|
+
const reason = await reasonFrom(response);
|
|
448
|
+
throw new CanvasAdminError(
|
|
449
|
+
`${what} failed (HTTP ${response.status})${reason ? `: ${reason}` : ""}`,
|
|
450
|
+
response.status,
|
|
451
|
+
reason
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Logs in. Resolves `{ needsCode: true }` when the company requires a
|
|
456
|
+
* two-factor code (sent by e-mail): call {@link verifyCode} next.
|
|
457
|
+
*/
|
|
458
|
+
async login(creds) {
|
|
459
|
+
const response = await this.request("/auth/login", {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: { "content-type": "application/json" },
|
|
462
|
+
body: JSON.stringify({
|
|
463
|
+
company_id: creds.companyId,
|
|
464
|
+
user_id: creds.userId,
|
|
465
|
+
password: creds.password
|
|
466
|
+
})
|
|
467
|
+
});
|
|
468
|
+
if (!response.ok) await this.fail(response, "Login");
|
|
469
|
+
const token = tokenFrom(response.headers);
|
|
470
|
+
if (!token) throw new CanvasAdminError("Login answered without a session cookie", 200, null);
|
|
471
|
+
this.token = token;
|
|
472
|
+
const body = await response.json().catch(() => ({}));
|
|
473
|
+
return { needsCode: body.completed_auth_flow === false };
|
|
474
|
+
}
|
|
475
|
+
/** Sends the two-factor (or recovery) code; the server swaps the session cookie. */
|
|
476
|
+
async verifyCode(code) {
|
|
477
|
+
const response = await this.request("/auth/verify_2fa", {
|
|
478
|
+
method: "POST",
|
|
479
|
+
headers: { "content-type": "application/json" },
|
|
480
|
+
body: JSON.stringify({ otp_code: code })
|
|
481
|
+
});
|
|
482
|
+
if (!response.ok) await this.fail(response, "Two-factor verification");
|
|
483
|
+
const token = tokenFrom(response.headers);
|
|
484
|
+
if (token) this.token = token;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Publishes a bundle. **It goes live at once** for every user of the
|
|
488
|
+
* company: there is no draft. A version can be published only once per
|
|
489
|
+
* company (HTTP 409).
|
|
490
|
+
*/
|
|
491
|
+
async publish(input) {
|
|
492
|
+
const form = new FormData();
|
|
493
|
+
form.set(
|
|
494
|
+
"bundle",
|
|
495
|
+
new Blob([input.zipBytes], { type: "application/zip" }),
|
|
496
|
+
"bundle.zip"
|
|
497
|
+
);
|
|
498
|
+
form.set("version", input.version);
|
|
499
|
+
if (input.companyId !== void 0) form.set("company_id", String(input.companyId));
|
|
500
|
+
if (input.minAppVersion) form.set("min_app_version", input.minAppVersion);
|
|
501
|
+
const response = await this.request("/v2/condor_canvas/bundles", {
|
|
502
|
+
method: "POST",
|
|
503
|
+
body: form
|
|
504
|
+
});
|
|
505
|
+
if (!response.ok) await this.fail(response, "Publish");
|
|
506
|
+
const body = await response.json();
|
|
507
|
+
return body.data;
|
|
508
|
+
}
|
|
509
|
+
/** Lists the bundles of a company (a manager always sees their own). */
|
|
510
|
+
async list(companyId) {
|
|
511
|
+
const query = companyId === void 0 ? "" : `?company_id=${companyId}`;
|
|
512
|
+
const response = await this.request(`/v2/condor_canvas/bundles${query}`);
|
|
513
|
+
if (!response.ok) await this.fail(response, "List");
|
|
514
|
+
const body = await response.json();
|
|
515
|
+
return body.data;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Revokes a bundle. Without `fallbackVersion`, revoking the current bundle
|
|
519
|
+
* leaves the company with none: the app shows its default screen.
|
|
520
|
+
*/
|
|
521
|
+
async revoke(bundleId, fallbackVersion) {
|
|
522
|
+
const response = await this.request(`/v2/condor_canvas/bundles/${bundleId}/revoke`, {
|
|
523
|
+
method: "POST",
|
|
524
|
+
headers: { "content-type": "application/json" },
|
|
525
|
+
body: JSON.stringify(fallbackVersion ? { fallbackVersion } : {})
|
|
526
|
+
});
|
|
527
|
+
if (!response.ok) await this.fail(response, "Revoke");
|
|
528
|
+
const body = await response.json();
|
|
529
|
+
return body.data;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/canvas-admin/command.ts
|
|
534
|
+
var CANVAS_USAGE = `condor-mobile-bridge canvas \u2014 publish a bundle to the Condor Canvas app.
|
|
535
|
+
|
|
536
|
+
Usage:
|
|
537
|
+
npx condor-mobile-bridge canvas publish <dist/index.html|dir|bundle.zip> --version <v> [options]
|
|
538
|
+
npx condor-mobile-bridge canvas list [options]
|
|
539
|
+
npx condor-mobile-bridge canvas revoke <bundleId> [--fallback-version <v>] [options]
|
|
540
|
+
|
|
541
|
+
Publishing goes LIVE AT ONCE for every user of the company \u2014 there is no draft.
|
|
542
|
+
Use a test company, and see docs/device-testing.md.
|
|
543
|
+
|
|
544
|
+
Options:
|
|
545
|
+
--company <id> Company id, as in the Condor web login (or CONDOR_CANVAS_COMPANY).
|
|
546
|
+
--user <id> Your user id, as in the Condor web login (or CONDOR_CANVAS_USER).
|
|
547
|
+
Both are asked when not given.
|
|
548
|
+
--version <v> publish: bundle version, unique per company.
|
|
549
|
+
--min-app-version <v> publish: minimum app build that may load it.
|
|
550
|
+
--fallback-version <v> revoke: VERSION to serve instead (the version column of
|
|
551
|
+
canvas list, e.g. 0.0.8 \u2014 not the id).
|
|
552
|
+
--env prod|dev API environment (default: prod). dev is for Condor staff.
|
|
553
|
+
--base-url <url> Override the API URL.
|
|
554
|
+
--yes Do not ask for confirmation.
|
|
555
|
+
--json Print results as JSON.
|
|
556
|
+
|
|
557
|
+
Your password (and the two-factor code, when your company uses one) is asked
|
|
558
|
+
interactively, without echo, and never stored. There is no non-interactive login.`;
|
|
559
|
+
var intOrUndefined = (value) => {
|
|
560
|
+
if (value === void 0) return void 0;
|
|
561
|
+
const n = Number(value);
|
|
562
|
+
return Number.isInteger(n) && n > 0 ? n : Number.NaN;
|
|
563
|
+
};
|
|
564
|
+
function parseCanvasArgs(argv, env = {}) {
|
|
565
|
+
const [action, ...rest] = argv;
|
|
566
|
+
if (action !== "publish" && action !== "list" && action !== "revoke") {
|
|
567
|
+
return { error: `Unknown canvas command "${action ?? ""}". Use publish, list or revoke.` };
|
|
568
|
+
}
|
|
569
|
+
const args = {
|
|
570
|
+
action,
|
|
571
|
+
env: "prod",
|
|
572
|
+
yes: false,
|
|
573
|
+
json: false,
|
|
574
|
+
companyId: intOrUndefined(env.CONDOR_CANVAS_COMPANY),
|
|
575
|
+
userId: intOrUndefined(env.CONDOR_CANVAS_USER)
|
|
576
|
+
};
|
|
577
|
+
const positional = [];
|
|
578
|
+
for (let i = 0; i < rest.length; i++) {
|
|
579
|
+
const token = rest[i] ?? "";
|
|
580
|
+
const next = () => rest[++i];
|
|
581
|
+
switch (token) {
|
|
582
|
+
case "--env": {
|
|
583
|
+
const env2 = next();
|
|
584
|
+
if (env2 !== "prod" && env2 !== "dev") return { error: "--env must be prod or dev." };
|
|
585
|
+
args.env = env2;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case "--base-url":
|
|
589
|
+
args.baseUrl = next();
|
|
590
|
+
break;
|
|
591
|
+
case "--company":
|
|
592
|
+
args.companyId = intOrUndefined(next());
|
|
593
|
+
break;
|
|
594
|
+
case "--user":
|
|
595
|
+
args.userId = intOrUndefined(next());
|
|
596
|
+
break;
|
|
597
|
+
case "--version":
|
|
598
|
+
args.version = next();
|
|
599
|
+
break;
|
|
600
|
+
case "--min-app-version":
|
|
601
|
+
args.minAppVersion = next();
|
|
602
|
+
break;
|
|
603
|
+
case "--fallback-version":
|
|
604
|
+
args.fallbackVersion = next();
|
|
605
|
+
break;
|
|
606
|
+
case "--yes":
|
|
607
|
+
args.yes = true;
|
|
608
|
+
break;
|
|
609
|
+
case "--json":
|
|
610
|
+
args.json = true;
|
|
611
|
+
break;
|
|
612
|
+
case "--password":
|
|
613
|
+
return {
|
|
614
|
+
error: "There is no --password flag: the password is only read from the prompt."
|
|
615
|
+
};
|
|
616
|
+
default:
|
|
617
|
+
if (token.startsWith("-")) return { error: `Unknown option ${token}.` };
|
|
618
|
+
positional.push(token);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (Number.isNaN(args.companyId) || Number.isNaN(args.userId)) {
|
|
622
|
+
return {
|
|
623
|
+
error: "--company and --user (or CONDOR_CANVAS_COMPANY / CONDOR_CANVAS_USER) take a positive integer id."
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (action === "publish") {
|
|
627
|
+
args.input = positional[0];
|
|
628
|
+
if (!args.input)
|
|
629
|
+
return { error: "canvas publish needs the bundle: dist/index.html, a dir or a .zip." };
|
|
630
|
+
if (!args.version) return { error: "canvas publish needs --version." };
|
|
631
|
+
}
|
|
632
|
+
if (action === "revoke") {
|
|
633
|
+
args.bundleId = intOrUndefined(positional[0]);
|
|
634
|
+
if (!args.bundleId || Number.isNaN(args.bundleId)) {
|
|
635
|
+
return { error: "canvas revoke needs the bundle id (see canvas list)." };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return args;
|
|
639
|
+
}
|
|
640
|
+
function loadZip(input) {
|
|
641
|
+
const path = resolve2(input);
|
|
642
|
+
if (!existsSync(path)) throw new Error(`${input}: not found.`);
|
|
643
|
+
let zipBytes;
|
|
644
|
+
if (extname2(path).toLowerCase() === ".zip") {
|
|
645
|
+
zipBytes = new Uint8Array(readFileSync2(path));
|
|
646
|
+
} else {
|
|
647
|
+
const html = statSync(path).isDirectory() ? join(path, "index.html") : path;
|
|
648
|
+
zipBytes = buildBundle(html).zipBytes;
|
|
649
|
+
}
|
|
650
|
+
return { zipBytes, findings: validateZipBytes(zipBytes).findings };
|
|
651
|
+
}
|
|
652
|
+
async function askId(io, label, value) {
|
|
653
|
+
if (value !== void 0) return value;
|
|
654
|
+
const answer = (await io.ask(`${label}: `)).trim();
|
|
655
|
+
const n = Number(answer);
|
|
656
|
+
if (!Number.isInteger(n) || n <= 0) throw new Error(`${label} must be a positive integer.`);
|
|
657
|
+
return n;
|
|
658
|
+
}
|
|
659
|
+
async function confirm(io, args, question2) {
|
|
660
|
+
if (args.yes) return true;
|
|
661
|
+
const answer = (await io.ask(`${question2} [y/N] `)).trim().toLowerCase();
|
|
662
|
+
return answer === "y" || answer === "yes";
|
|
663
|
+
}
|
|
664
|
+
async function logIn(io, client, companyId, userId) {
|
|
665
|
+
io.print("");
|
|
666
|
+
io.print("Log in with your Condor web account (typing is hidden; press Enter when done).");
|
|
667
|
+
let password = "";
|
|
668
|
+
for (let attempt = 0; attempt < 3 && !password.trim(); attempt++) {
|
|
669
|
+
if (attempt > 0) io.printError("The password cannot be empty.");
|
|
670
|
+
password = await io.askSecret("Password: ");
|
|
671
|
+
}
|
|
672
|
+
if (!password.trim()) throw new Error("No password given. Nothing was sent.");
|
|
673
|
+
const { needsCode } = await client.login({ companyId, userId, password });
|
|
674
|
+
if (!needsCode) return;
|
|
675
|
+
io.print(
|
|
676
|
+
"Your company uses two-factor login: a code was sent to your e-mail. Type it below (hidden)."
|
|
677
|
+
);
|
|
678
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
679
|
+
const code = (await io.askSecret("Code (or a recovery code): ")).trim();
|
|
680
|
+
try {
|
|
681
|
+
await client.verifyCode(code);
|
|
682
|
+
return;
|
|
683
|
+
} catch (err) {
|
|
684
|
+
if (attempt === 2 || !(err instanceof CanvasAdminError) || err.status !== 400) throw err;
|
|
685
|
+
io.printError("That code was not accepted. Try once more.");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function describeError(err) {
|
|
690
|
+
if (!(err instanceof CanvasAdminError)) return err instanceof Error ? err.message : String(err);
|
|
691
|
+
if (err.status === 400 && /fallback/i.test(err.reason ?? "")) {
|
|
692
|
+
return `${err.message}
|
|
693
|
+
--fallback-version takes the VERSION column of \`canvas list\` (e.g. 0.0.8), not the id.`;
|
|
694
|
+
}
|
|
695
|
+
switch (err.status) {
|
|
696
|
+
case 401:
|
|
697
|
+
return `${err.message}
|
|
698
|
+
Check the company id, user id and password.`;
|
|
699
|
+
case 403:
|
|
700
|
+
return `${err.message}
|
|
701
|
+
Your user cannot do this for that company: publishing needs a manager of the company.`;
|
|
702
|
+
case 409:
|
|
703
|
+
return `${err.message}
|
|
704
|
+
This version was already published for the company. Use a new --version.`;
|
|
705
|
+
case 413:
|
|
706
|
+
return `${err.message}
|
|
707
|
+
The .zip is over 2 MB.`;
|
|
708
|
+
default:
|
|
709
|
+
return err.message;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function table(rows) {
|
|
713
|
+
if (rows.length === 0) return ["No bundle published for this company."];
|
|
714
|
+
return [
|
|
715
|
+
"id version current revoked published_at min_app",
|
|
716
|
+
...rows.map(
|
|
717
|
+
(r) => `${String(r.id).padEnd(8)}${r.version.padEnd(15)}${(r.is_current ? "yes" : "").padEnd(9)}${(r.revoked ? "yes" : "").padEnd(9)}${(r.published_at ?? "").padEnd(28)}${r.min_app_version ?? ""}`
|
|
718
|
+
)
|
|
719
|
+
];
|
|
720
|
+
}
|
|
721
|
+
async function runCanvas(args, io, deps) {
|
|
722
|
+
const baseUrl = args.baseUrl ?? CANVAS_ADMIN_API[args.env];
|
|
723
|
+
const envLabel = args.baseUrl ? `custom (${baseUrl})` : `${args.env.toUpperCase()} (${baseUrl})`;
|
|
724
|
+
try {
|
|
725
|
+
let zip;
|
|
726
|
+
if (args.action === "publish") {
|
|
727
|
+
zip = loadZip(args.input ?? "");
|
|
728
|
+
const errors = zip.findings.filter((f) => f.severity === "error");
|
|
729
|
+
for (const f of zip.findings) {
|
|
730
|
+
(f.severity === "error" ? io.printError : io.print)(
|
|
731
|
+
`${f.severity.toUpperCase()} [${f.rule}] ${f.message}`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (errors.length > 0) {
|
|
735
|
+
io.printError("The app would reject this bundle. Nothing was sent.");
|
|
736
|
+
return 1;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (!io.interactive) {
|
|
740
|
+
io.printError(
|
|
741
|
+
"canvas needs an interactive terminal: the password is only read from a prompt."
|
|
742
|
+
);
|
|
743
|
+
return 1;
|
|
744
|
+
}
|
|
745
|
+
const companyId = await askId(io, "Company id", args.companyId);
|
|
746
|
+
const userId = await askId(io, "User id", args.userId);
|
|
747
|
+
if (args.action === "publish" && zip) {
|
|
748
|
+
const hash = `sha256-${createHash2("sha256").update(zip.zipBytes).digest("hex")}`;
|
|
749
|
+
io.print("");
|
|
750
|
+
io.print(`Environment: ${envLabel}`);
|
|
751
|
+
io.print(`Company: ${companyId}`);
|
|
752
|
+
io.print(
|
|
753
|
+
`Version: ${args.version}${args.minAppVersion ? ` (min app ${args.minAppVersion})` : ""}`
|
|
754
|
+
);
|
|
755
|
+
io.print(`Bundle: ${zip.zipBytes.length} bytes, ${hash}`);
|
|
756
|
+
io.print("It goes LIVE AT ONCE for every user of this company.");
|
|
757
|
+
if (!await confirm(io, args, "Publish?")) {
|
|
758
|
+
io.print("Cancelled. Nothing was sent.");
|
|
759
|
+
return 1;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (args.action === "revoke") {
|
|
763
|
+
io.print("");
|
|
764
|
+
io.print(`Environment: ${envLabel}`);
|
|
765
|
+
io.print(`Revoke bundle ${args.bundleId} of company ${companyId}.`);
|
|
766
|
+
io.print(
|
|
767
|
+
args.fallbackVersion ? `The app switches to version ${args.fallbackVersion} (a version, not an id).` : "Without --fallback-version, revoking the current bundle leaves the company with none: the app shows its default screen."
|
|
768
|
+
);
|
|
769
|
+
if (!await confirm(io, args, "Revoke?")) {
|
|
770
|
+
io.print("Cancelled. Nothing was sent.");
|
|
771
|
+
return 1;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const client = new CanvasAdminClient({
|
|
775
|
+
baseUrl,
|
|
776
|
+
userAgent: deps.userAgent,
|
|
777
|
+
fetchImpl: deps.fetchImpl
|
|
778
|
+
});
|
|
779
|
+
await logIn(io, client, companyId, userId);
|
|
780
|
+
if (args.action === "publish" && zip) {
|
|
781
|
+
const record = await client.publish({
|
|
782
|
+
zipBytes: zip.zipBytes,
|
|
783
|
+
version: args.version ?? "",
|
|
784
|
+
companyId,
|
|
785
|
+
minAppVersion: args.minAppVersion
|
|
786
|
+
});
|
|
787
|
+
if (args.json) io.print(JSON.stringify(record));
|
|
788
|
+
else {
|
|
789
|
+
io.print(
|
|
790
|
+
`Published version ${record.version} (id ${record.id}) \u2014 current: ${record.is_current}.`
|
|
791
|
+
);
|
|
792
|
+
io.print(`bundleHash: ${record.bundle_hash}`);
|
|
793
|
+
io.print("Apps of this company load it the next time they come to the foreground.");
|
|
794
|
+
}
|
|
795
|
+
return 0;
|
|
796
|
+
}
|
|
797
|
+
if (args.action === "list") {
|
|
798
|
+
const rows = await client.list(companyId);
|
|
799
|
+
if (args.json) io.print(JSON.stringify(rows));
|
|
800
|
+
else for (const line of table(rows)) io.print(line);
|
|
801
|
+
return 0;
|
|
802
|
+
}
|
|
803
|
+
const result = await client.revoke(args.bundleId ?? 0, args.fallbackVersion);
|
|
804
|
+
io.print(args.json ? JSON.stringify(result) : `Revoked. ${JSON.stringify(result)}`);
|
|
805
|
+
return 0;
|
|
806
|
+
} catch (err) {
|
|
807
|
+
io.printError(describeError(err));
|
|
808
|
+
return 1;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/canvas-admin/tty.ts
|
|
813
|
+
import { createInterface } from "readline";
|
|
814
|
+
var rl = null;
|
|
815
|
+
var mutedPrompt = null;
|
|
816
|
+
function reader() {
|
|
817
|
+
if (rl) return rl;
|
|
818
|
+
rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
819
|
+
const out = rl;
|
|
820
|
+
const write = out._writeToOutput.bind(rl);
|
|
821
|
+
out._writeToOutput = (s) => {
|
|
822
|
+
if (mutedPrompt === null || s.includes(mutedPrompt)) write(s);
|
|
823
|
+
};
|
|
824
|
+
return rl;
|
|
825
|
+
}
|
|
826
|
+
function question(prompt, secret) {
|
|
827
|
+
const r = reader();
|
|
828
|
+
return new Promise((resolve5) => {
|
|
829
|
+
mutedPrompt = secret ? prompt : null;
|
|
830
|
+
r.question(prompt, (answer) => {
|
|
831
|
+
if (secret) {
|
|
832
|
+
mutedPrompt = null;
|
|
833
|
+
process.stderr.write("\n");
|
|
834
|
+
}
|
|
835
|
+
resolve5(answer);
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function terminalIo() {
|
|
840
|
+
return {
|
|
841
|
+
interactive: Boolean(process.stdin.isTTY),
|
|
842
|
+
ask: (q) => question(q, false),
|
|
843
|
+
askSecret: (q) => question(q, true),
|
|
844
|
+
print: (line) => console.log(line),
|
|
845
|
+
printError: (line) => console.error(line)
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function closeTerminalIo() {
|
|
849
|
+
rl?.close();
|
|
850
|
+
rl = null;
|
|
851
|
+
}
|
|
852
|
+
|
|
375
853
|
// src/cli-dev.ts
|
|
376
|
-
import { existsSync, readFileSync as
|
|
854
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
377
855
|
import { createServer } from "http";
|
|
378
|
-
import { dirname as dirname2, extname as
|
|
856
|
+
import { dirname as dirname2, extname as extname3, join as join2, resolve as resolve3 } from "path";
|
|
379
857
|
import { fileURLToPath } from "url";
|
|
380
858
|
import { strFromU8, unzipSync as unzipSync2 } from "fflate";
|
|
381
859
|
|
|
@@ -614,12 +1092,12 @@ var SCENARIOS = {
|
|
|
614
1092
|
|
|
615
1093
|
// src/cli-dev.ts
|
|
616
1094
|
function readClientHtml(input) {
|
|
617
|
-
const path =
|
|
618
|
-
if (
|
|
619
|
-
return inlineHtml(
|
|
1095
|
+
const path = resolve3(input);
|
|
1096
|
+
if (existsSync2(path) && statSync2(path).isDirectory()) {
|
|
1097
|
+
return inlineHtml(join2(path, "index.html"));
|
|
620
1098
|
}
|
|
621
|
-
if (
|
|
622
|
-
const files = unzipSync2(new Uint8Array(
|
|
1099
|
+
if (extname3(path).toLowerCase() === ".zip") {
|
|
1100
|
+
const files = unzipSync2(new Uint8Array(readFileSync3(path)));
|
|
623
1101
|
const entry = files["index.html"];
|
|
624
1102
|
if (!entry) throw new Error(`${input}: no index.html at the root of the zip.`);
|
|
625
1103
|
return strFromU8(entry);
|
|
@@ -691,7 +1169,7 @@ function harnessPage(scenario, nativeVersion) {
|
|
|
691
1169
|
}
|
|
692
1170
|
function harnessScriptPath() {
|
|
693
1171
|
const here = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath(import.meta.url));
|
|
694
|
-
return
|
|
1172
|
+
return join2(here, "dev-harness.global.js");
|
|
695
1173
|
}
|
|
696
1174
|
function startDevServer(opts) {
|
|
697
1175
|
if (!(opts.scenario in SCENARIOS)) {
|
|
@@ -716,7 +1194,7 @@ function startDevServer(opts) {
|
|
|
716
1194
|
"content-type": "text/javascript; charset=utf-8",
|
|
717
1195
|
"cache-control": "no-store"
|
|
718
1196
|
});
|
|
719
|
-
res.end(
|
|
1197
|
+
res.end(readFileSync3(harnessScriptPath()));
|
|
720
1198
|
return;
|
|
721
1199
|
}
|
|
722
1200
|
if (url.pathname === "/") {
|
|
@@ -761,7 +1239,7 @@ function runDev(input, scenario, port, nativeVersion) {
|
|
|
761
1239
|
}
|
|
762
1240
|
function runBuild(htmlPath, outPath, maxBytes) {
|
|
763
1241
|
const result = buildBundle(htmlPath, maxBytes);
|
|
764
|
-
const resolvedOut =
|
|
1242
|
+
const resolvedOut = resolve4(outPath);
|
|
765
1243
|
mkdirSync(dirname3(resolvedOut), { recursive: true });
|
|
766
1244
|
writeFileSync(resolvedOut, result.zipBytes);
|
|
767
1245
|
const kb = (Buffer.byteLength(result.html) / 1024).toFixed(1);
|
|
@@ -811,7 +1289,28 @@ ${errors.length} error(s), ${warnings.length} warning(s).` + (result.size === nu
|
|
|
811
1289
|
);
|
|
812
1290
|
return 0;
|
|
813
1291
|
}
|
|
1292
|
+
async function runCanvasCommand(argv) {
|
|
1293
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
1294
|
+
console.log(CANVAS_USAGE);
|
|
1295
|
+
process.exit(argv.length === 0 ? 1 : 0);
|
|
1296
|
+
}
|
|
1297
|
+
const parsed = parseCanvasArgs(argv, process.env);
|
|
1298
|
+
if ("error" in parsed) {
|
|
1299
|
+
console.error(parsed.error);
|
|
1300
|
+
console.error("Run `condor-mobile-bridge canvas --help`.");
|
|
1301
|
+
process.exit(1);
|
|
1302
|
+
}
|
|
1303
|
+
const code = await runCanvas(parsed, terminalIo(), {
|
|
1304
|
+
userAgent: `condor-mobile-bridge-cli/${version}`
|
|
1305
|
+
});
|
|
1306
|
+
closeTerminalIo();
|
|
1307
|
+
process.exit(code);
|
|
1308
|
+
}
|
|
814
1309
|
function main() {
|
|
1310
|
+
if (process.argv[2] === "canvas") {
|
|
1311
|
+
void runCanvasCommand(process.argv.slice(3));
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
815
1314
|
const args = parseArgs(process.argv.slice(2));
|
|
816
1315
|
if (args.help || !args.htmlPath) {
|
|
817
1316
|
console.log(USAGE);
|