@lynxship/cli 0.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.
Files changed (68) hide show
  1. package/README.md +122 -0
  2. package/dist/android-build.d.ts +14 -0
  3. package/dist/android-build.d.ts.map +1 -0
  4. package/dist/android-build.js +201 -0
  5. package/dist/artifact-name.d.ts +3 -0
  6. package/dist/artifact-name.d.ts.map +1 -0
  7. package/dist/artifact-name.js +4 -0
  8. package/dist/autolink.d.ts +14 -0
  9. package/dist/autolink.d.ts.map +1 -0
  10. package/dist/autolink.js +144 -0
  11. package/dist/config.d.ts +51 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +62 -0
  14. package/dist/configure.d.ts +11 -0
  15. package/dist/configure.d.ts.map +1 -0
  16. package/dist/configure.js +172 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +1082 -0
  20. package/dist/ios-build.d.ts +13 -0
  21. package/dist/ios-build.d.ts.map +1 -0
  22. package/dist/ios-build.js +174 -0
  23. package/dist/ota-assets.d.ts +3 -0
  24. package/dist/ota-assets.d.ts.map +1 -0
  25. package/dist/ota-assets.js +48 -0
  26. package/dist/ota-doctor.d.ts +10 -0
  27. package/dist/ota-doctor.d.ts.map +1 -0
  28. package/dist/ota-doctor.js +53 -0
  29. package/dist/paths.d.ts +2 -0
  30. package/dist/paths.d.ts.map +1 -0
  31. package/dist/paths.js +12 -0
  32. package/dist/process-runner.d.ts +18 -0
  33. package/dist/process-runner.d.ts.map +1 -0
  34. package/dist/process-runner.js +97 -0
  35. package/dist/prompt.d.ts +3 -0
  36. package/dist/prompt.d.ts.map +1 -0
  37. package/dist/prompt.js +58 -0
  38. package/dist/r2.d.ts +32 -0
  39. package/dist/r2.d.ts.map +1 -0
  40. package/dist/r2.js +135 -0
  41. package/dist/remote.d.ts +33 -0
  42. package/dist/remote.d.ts.map +1 -0
  43. package/dist/remote.js +116 -0
  44. package/dist/runtime-fingerprint.d.ts +10 -0
  45. package/dist/runtime-fingerprint.d.ts.map +1 -0
  46. package/dist/runtime-fingerprint.js +238 -0
  47. package/dist/secure-store.d.ts +32 -0
  48. package/dist/secure-store.d.ts.map +1 -0
  49. package/dist/secure-store.js +263 -0
  50. package/dist/ui/colors.d.ts +24 -0
  51. package/dist/ui/colors.d.ts.map +1 -0
  52. package/dist/ui/colors.js +64 -0
  53. package/dist/ui/components.d.ts +36 -0
  54. package/dist/ui/components.d.ts.map +1 -0
  55. package/dist/ui/components.js +268 -0
  56. package/dist/ui/index.d.ts +24 -0
  57. package/dist/ui/index.d.ts.map +1 -0
  58. package/dist/ui/index.js +68 -0
  59. package/dist/ui/logo.d.ts +3 -0
  60. package/dist/ui/logo.d.ts.map +1 -0
  61. package/dist/ui/logo.js +32 -0
  62. package/dist/ui/state.d.ts +5 -0
  63. package/dist/ui/state.d.ts.map +1 -0
  64. package/dist/ui/state.js +7 -0
  65. package/dist/ui/terminal.d.ts +12 -0
  66. package/dist/ui/terminal.d.ts.map +1 -0
  67. package/dist/ui/terminal.js +23 -0
  68. package/package.json +75 -0
package/dist/r2.js ADDED
@@ -0,0 +1,135 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { GetObjectCommand, HeadBucketCommand, S3Client, } from "@aws-sdk/client-s3";
4
+ import { Upload } from "@aws-sdk/lib-storage";
5
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
6
+ import { assert, sha256 } from "@lynxship/contracts";
7
+ import { loadCredentials } from "./secure-store.js";
8
+ import { globalLynxShipDirectory } from "./paths.js";
9
+ const configFileName = "r2.json";
10
+ function configFile(root) {
11
+ return join(root, ".lynxship", configFileName);
12
+ }
13
+ function globalConfigFile() {
14
+ return join(globalLynxShipDirectory(), configFileName);
15
+ }
16
+ function defaultEndpoint(accountId) {
17
+ return `https://${accountId}.r2.cloudflarestorage.com`;
18
+ }
19
+ function validateConfig(config) {
20
+ assert(/^[a-f0-9]{32}$/i.test(config.accountId), "CLI_R2_ACCOUNT_ID", "Cloudflare account ID must be a 32-character hexadecimal value");
21
+ assert(/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(config.bucket), "CLI_R2_BUCKET", "R2 bucket name is invalid");
22
+ assert(config.endpoint.startsWith("https://"), "CLI_R2_ENDPOINT", "R2 S3 endpoint must use HTTPS");
23
+ assert(config.expiresIn >= 1 && config.expiresIn <= 604800, "CLI_R2_EXPIRY", "R2 download URL expiry must be between 1 second and 7 days");
24
+ return config;
25
+ }
26
+ export async function loadR2(root) {
27
+ let config;
28
+ try {
29
+ let file = configFile(root);
30
+ if (!(await access(file)
31
+ .then(() => true)
32
+ .catch(() => false)))
33
+ file = globalConfigFile();
34
+ config = JSON.parse(await readFile(file, "utf8"));
35
+ }
36
+ catch (error) {
37
+ if (error.code === "ENOENT") {
38
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
39
+ const bucket = process.env.R2_BUCKET;
40
+ assert(accountId && bucket, "CLI_R2_REQUIRED", "R2 is not configured. Run `lynxship storage configure` first.");
41
+ config = {
42
+ accountId,
43
+ bucket,
44
+ endpoint: process.env.R2_ENDPOINT ?? defaultEndpoint(accountId),
45
+ expiresIn: Number(process.env.R2_DOWNLOAD_EXPIRES_IN ?? "86400"),
46
+ };
47
+ }
48
+ else
49
+ throw error;
50
+ }
51
+ const credentials = (await loadCredentials(root)).r2 ??
52
+ (process.env.R2_ACCESS_KEY_ID && process.env.R2_SECRET_ACCESS_KEY
53
+ ? {
54
+ accessKeyId: process.env.R2_ACCESS_KEY_ID,
55
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
56
+ }
57
+ : undefined);
58
+ assert(credentials, "CLI_R2_CREDENTIALS", "R2 credentials are missing. Run `lynxship storage configure` again.");
59
+ return { config: validateConfig(config), credentials };
60
+ }
61
+ export function createR2Client(config, credentials) {
62
+ return new S3Client({
63
+ region: "auto",
64
+ endpoint: config.endpoint,
65
+ credentials,
66
+ });
67
+ }
68
+ export async function verifyR2(config, credentials) {
69
+ const client = createR2Client(config, credentials);
70
+ try {
71
+ await client.send(new HeadBucketCommand({ Bucket: config.bucket }));
72
+ }
73
+ finally {
74
+ client.destroy();
75
+ }
76
+ }
77
+ export async function uploadR2Artifact(root, projectId, buildId, file, contentType, objectName, options = {}) {
78
+ const { config, credentials } = await loadR2(root);
79
+ const content = await readFile(file);
80
+ const hash = sha256(content);
81
+ const filename = file.split(/[\\/]/).at(-1) ?? "artifact";
82
+ const key = `artifacts/${projectId}/${buildId}/${objectName ?? filename}`;
83
+ const client = createR2Client(config, credentials);
84
+ options.onProgress?.(0, content.length);
85
+ try {
86
+ const upload = new Upload({
87
+ client,
88
+ queueSize: 1,
89
+ partSize: 8 * 1024 * 1024,
90
+ params: {
91
+ Bucket: config.bucket,
92
+ Key: key,
93
+ Body: content,
94
+ ContentType: contentType,
95
+ ContentDisposition: `attachment; filename="${filename}"`,
96
+ ContentLength: content.length,
97
+ Metadata: { sha256: hash, buildId },
98
+ },
99
+ });
100
+ upload.on("httpUploadProgress", (progress) => {
101
+ options.onProgress?.(progress.loaded ?? 0, progress.total ?? content.length);
102
+ });
103
+ await upload.done();
104
+ const expiresAt = new Date(Date.now() + config.expiresIn * 1000);
105
+ const url = await getSignedUrl(client, new GetObjectCommand({
106
+ Bucket: config.bucket,
107
+ Key: key,
108
+ }), { expiresIn: config.expiresIn });
109
+ return {
110
+ key,
111
+ hash,
112
+ size: content.length,
113
+ contentType,
114
+ url,
115
+ expiresAt: expiresAt.toISOString(),
116
+ };
117
+ }
118
+ finally {
119
+ client.destroy();
120
+ }
121
+ }
122
+ export async function writeR2Config(root, config, options = {}) {
123
+ validateConfig(config);
124
+ const directory = options.global
125
+ ? globalLynxShipDirectory()
126
+ : join(root, ".lynxship");
127
+ await mkdir(directory, { recursive: true });
128
+ const file = options.global ? globalConfigFile() : configFile(root);
129
+ await writeFile(file, `${JSON.stringify(config, null, 2)}\n`, {
130
+ encoding: "utf8",
131
+ mode: 0o600,
132
+ });
133
+ await access(file);
134
+ }
135
+ export { defaultEndpoint };
@@ -0,0 +1,33 @@
1
+ import type { BuildJob } from "@lynxship/contracts";
2
+ import type { LynxShipConfig } from "./config.js";
3
+ export interface RemoteCliState {
4
+ remoteOrganizationId?: string;
5
+ remoteProjectId?: string;
6
+ }
7
+ export interface OtaPublishRequest {
8
+ projectId: string;
9
+ organizationId: string;
10
+ channel: string;
11
+ platform: "android" | "ios";
12
+ runtimeVersion: string;
13
+ assets: Array<{
14
+ path: string;
15
+ hash: string;
16
+ size: number;
17
+ url: string;
18
+ }>;
19
+ message?: string;
20
+ rollout?: number;
21
+ policyApprovalId?: string | null;
22
+ }
23
+ export declare function ensureRemoteTarget(config: LynxShipConfig, state: RemoteCliState): Promise<{
24
+ organizationId: string;
25
+ projectId: string;
26
+ }>;
27
+ export declare function submitRealArtifact(config: LynxShipConfig, state: RemoteCliState, job: BuildJob, latest: boolean): Promise<unknown>;
28
+ export declare function publishOtaRelease(config: LynxShipConfig, state: RemoteCliState, input: OtaPublishRequest): Promise<unknown>;
29
+ export declare function fetchOtaPublicKey(config: LynxShipConfig): Promise<{
30
+ keyId: string;
31
+ publicKey: string;
32
+ }>;
33
+ //# sourceMappingURL=remote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,GAAG,KAAK,CAAC;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC;AAsCD,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CA+CxD;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,OAAO,GACd,OAAO,CAAC,OAAO,CAAC,CAoDlB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,OAAO,CAAC,CAalB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ/C"}
package/dist/remote.js ADDED
@@ -0,0 +1,116 @@
1
+ function apiUrl(options) {
2
+ return (options.apiUrl ??
3
+ process.env.LYNXSHIP_API_URL ??
4
+ "http://127.0.0.1:8787").replace(/\/$/, "");
5
+ }
6
+ async function request(options, path, init = {}) {
7
+ const headers = new Headers(init.headers);
8
+ headers.set("content-type", "application/json");
9
+ if (options.token)
10
+ headers.set("authorization", `Bearer ${options.token}`);
11
+ const response = await fetch(`${apiUrl(options)}${path}`, {
12
+ ...init,
13
+ headers,
14
+ });
15
+ const body = (await response.json());
16
+ if (!response.ok)
17
+ throw new Error(`LynxShip API ${response.status}: ${body.message ?? body.error ?? response.statusText}`);
18
+ return body;
19
+ }
20
+ export async function ensureRemoteTarget(config, state) {
21
+ const options = {
22
+ apiUrl: config.cli?.apiUrl,
23
+ token: config.cli?.token ?? process.env.LYNXSHIP_TOKEN,
24
+ };
25
+ let organizationId = state.remoteOrganizationId ??
26
+ config.cli?.organizationId ??
27
+ process.env.LYNXSHIP_ORGANIZATION_ID;
28
+ if (!organizationId) {
29
+ const organization = await request(options, "/v1/organizations", {
30
+ method: "POST",
31
+ body: JSON.stringify({
32
+ name: `${config.projectId ?? "local_project"} organization`,
33
+ ownerUserId: "cli",
34
+ }),
35
+ });
36
+ organizationId = organization.id;
37
+ state.remoteOrganizationId = organizationId;
38
+ }
39
+ let projectId = state.remoteProjectId;
40
+ if (!projectId) {
41
+ const projects = await request(options, `/v1/projects?organizationId=${encodeURIComponent(organizationId)}`);
42
+ const projectName = config.projectId ?? "local_project";
43
+ projectId = projects.find((project) => project.name === projectName)?.id;
44
+ if (!projectId) {
45
+ const project = await request(options, "/v1/projects", {
46
+ method: "POST",
47
+ body: JSON.stringify({
48
+ organizationId,
49
+ name: projectName,
50
+ }),
51
+ });
52
+ projectId = project.id;
53
+ }
54
+ state.remoteProjectId = projectId;
55
+ }
56
+ return { organizationId, projectId };
57
+ }
58
+ export async function submitRealArtifact(config, state, job, latest) {
59
+ if (!job.artifact?.key || !job.artifact.url)
60
+ throw new Error("The build artifact is not registered in Cloudflare R2");
61
+ const { organizationId, projectId } = await ensureRemoteTarget(config, state);
62
+ const filename = job.artifact.name;
63
+ const artifactResponse = await request({
64
+ apiUrl: config.cli?.apiUrl,
65
+ token: config.cli?.token ?? process.env.LYNXSHIP_TOKEN,
66
+ }, "/v1/artifacts", {
67
+ method: "POST",
68
+ body: JSON.stringify({
69
+ projectId,
70
+ organizationId,
71
+ filename,
72
+ artifact: {
73
+ key: job.artifact.key,
74
+ hash: job.artifact.hash,
75
+ size: job.artifact.size,
76
+ contentType: job.artifact.contentType,
77
+ url: job.artifact.url,
78
+ expiresAt: job.artifact.expiresAt,
79
+ },
80
+ }),
81
+ });
82
+ return request({
83
+ apiUrl: config.cli?.apiUrl,
84
+ token: config.cli?.token ?? process.env.LYNXSHIP_TOKEN,
85
+ }, "/v1/submissions", {
86
+ method: "POST",
87
+ body: JSON.stringify({
88
+ projectId,
89
+ organizationId,
90
+ platform: job.platform,
91
+ artifact: { hash: artifactResponse.artifact.hash },
92
+ artifactKey: artifactResponse.artifact.key,
93
+ downloadUrl: artifactResponse.artifact.url,
94
+ downloadExpiresAt: job.artifact.expiresAt,
95
+ latest,
96
+ buildId: latest ? null : job.id,
97
+ idempotencyKey: `cli:${job.id}:${job.artifact.hash}`,
98
+ }),
99
+ });
100
+ }
101
+ export async function publishOtaRelease(config, state, input) {
102
+ const target = await ensureRemoteTarget(config, state);
103
+ return request({
104
+ apiUrl: config.cli?.apiUrl,
105
+ token: config.cli?.token ?? process.env.LYNXSHIP_TOKEN,
106
+ }, "/v1/ota/releases", {
107
+ method: "POST",
108
+ body: JSON.stringify({ ...input, ...target }),
109
+ });
110
+ }
111
+ export async function fetchOtaPublicKey(config) {
112
+ return request({
113
+ apiUrl: config.cli?.apiUrl,
114
+ token: config.cli?.token ?? process.env.LYNXSHIP_TOKEN,
115
+ }, "/v1/ota/public-key");
116
+ }
@@ -0,0 +1,10 @@
1
+ import { type BuildOrchestrator } from "@lynxship/build-orchestrator";
2
+ import { type Platform } from "@lynxship/contracts";
3
+ import type { LynxShipConfig } from "./config.js";
4
+ export interface RuntimeFingerprintReport {
5
+ value: string;
6
+ inputs: Record<string, unknown>;
7
+ }
8
+ export declare function assertCompatibleBinaryBuild(builds: BuildOrchestrator, platform: Platform, runtimeVersion: string): void;
9
+ export declare function inspectRuntimeFingerprint(root: string, platform: Platform, config: LynxShipConfig): Promise<RuntimeFingerprintReport>;
10
+ //# sourceMappingURL=runtime-fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-fingerprint.d.ts","sourceRoot":"","sources":["../src/runtime-fingerprint.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,iBAAiB,EAGvB,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAQlD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,MAAM,GACrB,IAAI,CAmBN;AA0MD,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,wBAAwB,CAAC,CAyCnC"}
@@ -0,0 +1,238 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
4
+ import { basename, dirname, extname, join, relative, sep } from "node:path";
5
+ import { runtimeFingerprint, } from "@lynxship/build-orchestrator";
6
+ import { assert, sha256 } from "@lynxship/contracts";
7
+ export function assertCompatibleBinaryBuild(builds, platform, runtimeVersion) {
8
+ const successfulBuild = builds
9
+ .list()
10
+ .filter((job) => job.platform === platform && job.state === "success")
11
+ .sort((left, right) => {
12
+ const leftAt = left.transitions.at(-1)?.at ?? "";
13
+ const rightAt = right.transitions.at(-1)?.at ?? "";
14
+ return rightAt.localeCompare(leftAt);
15
+ })[0];
16
+ assert(successfulBuild?.runtimeVersion === runtimeVersion, "OTA_NATIVE_CHANGE_REQUIRED", "Native project inputs changed or no compatible binary build exists. Run `lynxship build` before publishing an OTA update.", {
17
+ platform,
18
+ runtimeVersion,
19
+ lastBuildRuntimeVersion: successfulBuild?.runtimeVersion ?? null,
20
+ });
21
+ }
22
+ const nativeExtensions = new Set([
23
+ ".gradle",
24
+ ".gradle.kts",
25
+ ".h",
26
+ ".java",
27
+ ".json",
28
+ ".kt",
29
+ ".m",
30
+ ".mm",
31
+ ".plist",
32
+ ".podspec",
33
+ ".properties",
34
+ ".rb",
35
+ ".swift",
36
+ ".xml",
37
+ ".entitlements",
38
+ ".storyboard",
39
+ ".xib",
40
+ ".xcconfig",
41
+ ]);
42
+ const nativeNames = new Set([
43
+ "Podfile",
44
+ "Podfile.lock",
45
+ "project.pbxproj",
46
+ "settings.gradle",
47
+ "settings.gradle.kts",
48
+ "gradle-wrapper.properties",
49
+ "AndroidManifest.xml",
50
+ ]);
51
+ const ignoredDirectories = new Set([
52
+ ".gradle",
53
+ ".git",
54
+ ".lynxship",
55
+ "build",
56
+ "DerivedData",
57
+ "Pods",
58
+ ]);
59
+ function normalizedPath(value) {
60
+ return value.split(sep).join("/");
61
+ }
62
+ async function readJsonFile(file) {
63
+ try {
64
+ return JSON.parse(await readFile(file, "utf8"));
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ async function fileFingerprint(root, file) {
71
+ const data = await readFile(file);
72
+ return {
73
+ path: normalizedPath(relative(root, file)),
74
+ hash: createHash("sha256")
75
+ .update(data.toString("latin1"), "latin1")
76
+ .digest("hex"),
77
+ size: data.byteLength,
78
+ };
79
+ }
80
+ async function collectNativeFiles(root, directory) {
81
+ const result = [];
82
+ async function visit(current) {
83
+ let entries;
84
+ try {
85
+ entries = await readdir(current, { withFileTypes: true });
86
+ }
87
+ catch {
88
+ return;
89
+ }
90
+ for (const entry of entries) {
91
+ if (ignoredDirectories.has(entry.name))
92
+ continue;
93
+ const file = join(current, entry.name);
94
+ if (entry.isDirectory()) {
95
+ await visit(file);
96
+ continue;
97
+ }
98
+ if (!entry.isFile())
99
+ continue;
100
+ if (!nativeNames.has(entry.name) &&
101
+ !nativeExtensions.has(extname(entry.name).toLowerCase()))
102
+ continue;
103
+ const details = await stat(file);
104
+ if (details.size > 2_000_000)
105
+ continue;
106
+ result.push(await fileFingerprint(root, file));
107
+ }
108
+ }
109
+ await visit(directory);
110
+ return result.sort((a, b) => a.path.localeCompare(b.path));
111
+ }
112
+ async function collectLynxLibraryManifests(root) {
113
+ const result = [];
114
+ const visited = new Set();
115
+ async function visit(current, depth) {
116
+ if (depth > 7)
117
+ return;
118
+ let realDirectory;
119
+ try {
120
+ realDirectory = (await stat(current)).isDirectory()
121
+ ? await realpath(current)
122
+ : "";
123
+ }
124
+ catch {
125
+ return;
126
+ }
127
+ if (!realDirectory || visited.has(realDirectory))
128
+ return;
129
+ visited.add(realDirectory);
130
+ let entries;
131
+ try {
132
+ entries = await readdir(current, { withFileTypes: true });
133
+ }
134
+ catch {
135
+ return;
136
+ }
137
+ for (const entry of entries) {
138
+ const file = join(current, entry.name);
139
+ if (entry.isFile() && entry.name === "lynx.lib.json") {
140
+ result.push(await fileFingerprint(root, file));
141
+ continue;
142
+ }
143
+ if (entry.name === ".cache")
144
+ continue;
145
+ if (entry.isDirectory()) {
146
+ await visit(file, depth + 1);
147
+ continue;
148
+ }
149
+ if (entry.isSymbolicLink()) {
150
+ try {
151
+ if ((await stat(file)).isDirectory())
152
+ await visit(file, depth + 1);
153
+ }
154
+ catch {
155
+ // Broken package links are ignored; the lockfile still fingerprints them.
156
+ }
157
+ }
158
+ }
159
+ }
160
+ await visit(join(root, "node_modules"), 0);
161
+ const rootManifest = join(root, "lynx.lib.json");
162
+ try {
163
+ result.push(await fileFingerprint(root, rootManifest));
164
+ }
165
+ catch {
166
+ // A root manifest is optional; package manifests are discovered above.
167
+ }
168
+ return result.sort((a, b) => a.path.localeCompare(b.path));
169
+ }
170
+ function packageManager(packageJson) {
171
+ if (typeof packageJson.packageManager === "string")
172
+ return packageJson.packageManager;
173
+ return "pnpm-lock.yaml";
174
+ }
175
+ function findLockfile(root) {
176
+ let current = root;
177
+ while (true) {
178
+ for (const name of ["pnpm-lock.yaml", "package-lock.json", "yarn.lock"]) {
179
+ const file = join(current, name);
180
+ if (existsSync(file))
181
+ return file;
182
+ }
183
+ const parent = dirname(current);
184
+ if (parent === current)
185
+ return undefined;
186
+ current = parent;
187
+ }
188
+ }
189
+ function lynxPackages(packageJson) {
190
+ const versions = {};
191
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
192
+ const dependencies = packageJson[field];
193
+ if (!dependencies || typeof dependencies !== "object")
194
+ continue;
195
+ for (const [name, value] of Object.entries(dependencies)) {
196
+ if ((name.startsWith("@lynx-js/") || name.startsWith("lynx")) &&
197
+ typeof value === "string")
198
+ versions[name] = value;
199
+ }
200
+ }
201
+ return Object.fromEntries(Object.entries(versions).sort(([a], [b]) => a.localeCompare(b)));
202
+ }
203
+ export async function inspectRuntimeFingerprint(root, platform, config) {
204
+ const packageJson = (await readJsonFile(join(root, "package.json"))) ??
205
+ {};
206
+ const lockfile = findLockfile(root);
207
+ const lockfileHash = lockfile ? sha256(await readFile(lockfile)) : "none";
208
+ const nativeFiles = await collectNativeFiles(root, join(root, platform));
209
+ const moduleManifests = await collectLynxLibraryManifests(root);
210
+ const nativeHash = sha256(JSON.stringify(nativeFiles));
211
+ const modulesHash = sha256(JSON.stringify(moduleManifests));
212
+ const packages = lynxPackages(packageJson);
213
+ const managerPackageJson = {
214
+ ...packageJson,
215
+ packageManager: packageJson.packageManager ?? (lockfile ? basename(lockfile) : undefined),
216
+ };
217
+ const input = {
218
+ platform,
219
+ config: { update: { protocolVersion: config.update?.protocolVersion } },
220
+ packageManager: packageManager(managerPackageJson),
221
+ lockfileHash,
222
+ native: {
223
+ engine: packages["@lynx-js/lynx"] ?? "unknown",
224
+ sdk: packages["@lynx-js/react"] ?? "unknown",
225
+ nativeHash,
226
+ modulesHash,
227
+ },
228
+ };
229
+ const fingerprint = runtimeFingerprint(input);
230
+ if (config.runtimeVersion?.policy === "manual") {
231
+ assert(config.runtimeVersion.value, "CONFIG_RUNTIME_VALUE", "Manual runtimeVersion.value is required");
232
+ return {
233
+ value: config.runtimeVersion.value,
234
+ inputs: { ...fingerprint.inputs, policy: "manual" },
235
+ };
236
+ }
237
+ return fingerprint;
238
+ }
@@ -0,0 +1,32 @@
1
+ export interface StoredCredentials {
2
+ r2?: {
3
+ accessKeyId: string;
4
+ secretAccessKey: string;
5
+ };
6
+ android?: {
7
+ keystorePath: string;
8
+ keyAlias: string;
9
+ keystorePassword: string;
10
+ keyPassword: string;
11
+ };
12
+ googlePlay?: {
13
+ serviceAccountJson: string;
14
+ applicationId: string;
15
+ track: string;
16
+ releaseStatus: "draft" | "completed" | "inProgress" | "halted";
17
+ };
18
+ appStoreConnect?: {
19
+ apiKeyId: string;
20
+ issuerId: string;
21
+ privateKey: string;
22
+ bundleIdentifier: string;
23
+ ascAppId?: string;
24
+ transporterPath?: string;
25
+ };
26
+ }
27
+ export declare function credentialStorageDescription(): string;
28
+ export declare function loadCredentials(root: string): Promise<StoredCredentials>;
29
+ export declare function saveCredentials(root: string, credentials: StoredCredentials, options?: {
30
+ global?: boolean;
31
+ }): Promise<void>;
32
+ //# sourceMappingURL=secure-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-store.d.ts","sourceRoot":"","sources":["../src/secure-store.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE;QACH,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,UAAU,CAAC,EAAE;QACX,kBAAkB,EAAE,MAAM,CAAC;QAC3B,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,EAAE,OAAO,GAAG,WAAW,GAAG,YAAY,GAAG,QAAQ,CAAC;KAChE,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,gBAAgB,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAIrD;AA6RD,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,iBAAiB,CAAC,CAM5B;AAED,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,iBAAiB,EAC9B,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACjC,OAAO,CAAC,IAAI,CAAC,CA6Bf"}