@rightkit/release 0.6.5 → 0.6.7
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 +196 -11
- package/admission-files.mjs +3 -2
- package/advisory-db.json +4 -0
- package/advisory-db.mjs +124 -0
- package/advisory-exceptions.mjs +13 -10
- package/android-play-upload.mjs +4 -4
- package/app-check.mjs +702 -26
- package/apple-profile-create.mjs +4 -3
- package/apple-profile-renew.mjs +5 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-agent-hook.mjs +9 -0
- package/bin/rightkit-gate.mjs +671 -133
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +20 -12
- package/cache-command.mjs +4 -3
- package/cargo-license-notices.mjs +2 -1
- package/cargo-manifest-text.mjs +25 -0
- package/cargo-profile-contract.mjs +26 -13
- package/cli/right-release.mjs +37 -7
- package/command-spec.mjs +4 -5
- package/compile-inputs.mjs +66 -0
- package/consumer-contracts.json +1044 -0
- package/consumer-contracts.mjs +627 -0
- package/consumer-input-contracts.mjs +422 -0
- package/consumer-policy.mjs +588 -0
- package/consumer-source-contracts.mjs +469 -0
- package/deny-default.toml +42 -0
- package/deps.mjs +5 -4
- package/deterministic-scans.mjs +513 -54
- package/development.mjs +5 -5
- package/disk-command.mjs +270 -30
- package/disk-policy.json +50 -10
- package/disk-policy.mjs +482 -35
- package/disk-report.mjs +129 -0
- package/door-policy.mjs +228 -0
- package/gate-executor.mjs +30 -0
- package/gate-stages.mjs +110 -0
- package/github-release.mjs +5 -3
- package/github-updater-release.mjs +48 -8
- package/github-upload-gate.mjs +6 -6
- package/init-command.mjs +175 -8
- package/ios-upload.mjs +4 -4
- package/legion-semantic-contracts.mjs +428 -0
- package/mirror-root-artifact.mjs +3 -4
- package/npm-license-notices.mjs +2 -1
- package/outdated-command.mjs +91 -0
- package/ownership-class-rules.mjs +277 -0
- package/ownership-key-authority.mjs +3 -1
- package/ownership-mandatory-set.mjs +3 -2
- package/package.json +46 -3
- package/pipeline-admission.mjs +12 -0
- package/publish-cargo.mjs +219 -4
- package/publish-update.mjs +19 -14
- package/release-config.mjs +87 -11
- package/release-dependencies-command.mjs +4 -3
- package/release-qa-audit.mjs +12 -10
- package/release-qa-command.mjs +42 -7
- package/release-qa-hardening.mjs +10 -3
- package/release-qa-policy.mjs +214 -47
- package/release.mjs +4 -4
- package/rightkit-registry-resolver.mjs +5 -2
- package/rightkit-versions.json +178 -170
- package/shell-contract-bridge.json +251 -0
- package/shell-contract.mjs +1069 -0
- package/sign-updater.mjs +24 -4
- package/source-closure-rules.mjs +204 -0
- package/stage-sidecar-command.mjs +7 -6
- package/stage-sidecar.mjs +437 -114
- package/suite-api.generated.mjs +96 -6
- package/suite-trust-roots.mjs +198 -0
- package/test-command.mjs +3 -2
- package/upload-release.mjs +3 -2
- package/web-build.mjs +2 -1
package/suite-api.generated.mjs
CHANGED
|
@@ -13,6 +13,48 @@ export const RIGHT_SUITE_GATED_ARTIFACT_HOST = "rightapps-license-gate.adrdsouza
|
|
|
13
13
|
export const RIGHT_SUITE_PUBLIC_ARTIFACT_HOST = "pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev";
|
|
14
14
|
export const RIGHT_SUITE_PUBLIC_INSTALLER_BASE = "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev";
|
|
15
15
|
export const RIGHT_SUITE_ARTIFACT_PATH_PREFIX = "/{app}";
|
|
16
|
+
export const RIGHT_SUITE_LICENSE_PUBLIC_KEY_HEX = "730732885355c4400c115592a42c91b8b0277d460cb2b5d59c1e2815a74d5d22";
|
|
17
|
+
export const RIGHT_SUITE_UPDATER_SIGNING_KEY_ID = "29395F9FF466261D";
|
|
18
|
+
export const RIGHT_SUITE_UPDATER_PUBLIC_KEYS = [
|
|
19
|
+
{
|
|
20
|
+
"kid": "29395F9FF466261D",
|
|
21
|
+
"publicKey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI5Mzk1RjlGRjQ2NjI2MUQKUldRZEptYjBuMTg1S1VSUXlBdFM4WmtzaHArYko0U2hRMDVlSDJmSExVZG82Q0hoQ2srUlhqanAK",
|
|
22
|
+
"notAfter": null,
|
|
23
|
+
"notAfterUnix": null
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
export const RIGHT_SUITE_CLIENT_PATHS = {
|
|
27
|
+
"licenseActivate": "/v1/licenses/activate",
|
|
28
|
+
"licenseRefresh": "/v1/licenses/refresh",
|
|
29
|
+
"licenseDeactivate": "/v1/licenses/deactivate",
|
|
30
|
+
"licenseTrial": "/v1/licenses/trial",
|
|
31
|
+
"bridgeFromActivation": "/v1/licenses/bridge/from-activation",
|
|
32
|
+
"bridgeExchange": "/v1/licenses/bridge/exchange",
|
|
33
|
+
"bridgeFromPlay": "/v1/licenses/bridge/from-play",
|
|
34
|
+
"bridgeFromStorekit": "/v1/licenses/bridge/from-storekit",
|
|
35
|
+
"leaseIssue": "/v1/pro/{app}/devices/lease",
|
|
36
|
+
"leaseRenew": "/v1/pro/{app}/devices/lease/renew",
|
|
37
|
+
"leaseRemove": "/v1/pro/{app}/devices/lease/remove",
|
|
38
|
+
"windowsLinkRequest": "/v1/pro/{app}/devices/windows-link/request",
|
|
39
|
+
"windowsLinkRedeem": "/v1/pro/{app}/devices/windows-link/redeem",
|
|
40
|
+
"accountMagicLinkRequest": "/v1/shop/auth/magic-link/request",
|
|
41
|
+
"accountMagicLinkConsume": "/v1/shop/auth/magic-link/consume",
|
|
42
|
+
"accountMe": "/v1/shop/auth/me",
|
|
43
|
+
"accountLogout": "/v1/shop/auth/logout",
|
|
44
|
+
"accountDevices": "/v1/shop/account/devices",
|
|
45
|
+
"accountDelete": "/v1/shop/account",
|
|
46
|
+
"entitlement": "/v1/entitlement"
|
|
47
|
+
};
|
|
48
|
+
export const RIGHT_SUITE_APP_KEYS = ["coderight","cutright","genright","heardright","mailright","postright","pulse","scraperight","viewright"];
|
|
49
|
+
export const RIGHT_SUITE_APP_LICENSE_HOSTS = {
|
|
50
|
+
"coderight": "coderight.cc",
|
|
51
|
+
"genright": "heardright.app",
|
|
52
|
+
"heardright": "heardright.app",
|
|
53
|
+
"mailright": "mailright.cc",
|
|
54
|
+
"scraperight": "scraperight.cc",
|
|
55
|
+
"viewright": "viewright.cc"
|
|
56
|
+
};
|
|
57
|
+
export const RIGHT_SUITE_PENDING_NATIVE_UPDATER = ["cutright","genright","heardright","postright","pulse","scraperight"];
|
|
16
58
|
export const APP_ARTIFACT_HOSTS = {
|
|
17
59
|
"coderight": [
|
|
18
60
|
"rightapps-license-gate.adrdsouza.workers.dev",
|
|
@@ -36,24 +78,37 @@ export const RIGHT_SUITE_API_CONTRACT = {
|
|
|
36
78
|
"artifactHosts": [
|
|
37
79
|
"gated",
|
|
38
80
|
"public"
|
|
39
|
-
]
|
|
81
|
+
],
|
|
82
|
+
"licenseHost": "coderight.cc"
|
|
83
|
+
},
|
|
84
|
+
"cutright": {},
|
|
85
|
+
"genright": {
|
|
86
|
+
"licenseHost": "heardright.app"
|
|
40
87
|
},
|
|
41
88
|
"heardright": {
|
|
42
89
|
"artifactHosts": [
|
|
43
90
|
"gated",
|
|
44
91
|
"public"
|
|
45
|
-
]
|
|
92
|
+
],
|
|
93
|
+
"licenseHost": "heardright.app"
|
|
46
94
|
},
|
|
47
95
|
"mailright": {
|
|
48
96
|
"artifactHosts": [
|
|
49
97
|
"gated",
|
|
50
98
|
"public"
|
|
51
|
-
]
|
|
99
|
+
],
|
|
100
|
+
"licenseHost": "mailright.cc"
|
|
101
|
+
},
|
|
102
|
+
"postright": {},
|
|
103
|
+
"pulse": {},
|
|
104
|
+
"scraperight": {
|
|
105
|
+
"licenseHost": "scraperight.cc"
|
|
52
106
|
},
|
|
53
107
|
"viewright": {
|
|
54
108
|
"artifactHosts": [
|
|
55
109
|
"public"
|
|
56
|
-
]
|
|
110
|
+
],
|
|
111
|
+
"licenseHost": "viewright.cc"
|
|
57
112
|
}
|
|
58
113
|
},
|
|
59
114
|
"artifactHostSets": {
|
|
@@ -61,7 +116,30 @@ export const RIGHT_SUITE_API_CONTRACT = {
|
|
|
61
116
|
"public": "pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev"
|
|
62
117
|
},
|
|
63
118
|
"artifactPathPrefix": "/{app}",
|
|
119
|
+
"clientPaths": {
|
|
120
|
+
"accountDelete": "/v1/shop/account",
|
|
121
|
+
"accountDevices": "/v1/shop/account/devices",
|
|
122
|
+
"accountLogout": "/v1/shop/auth/logout",
|
|
123
|
+
"accountMagicLinkConsume": "/v1/shop/auth/magic-link/consume",
|
|
124
|
+
"accountMagicLinkRequest": "/v1/shop/auth/magic-link/request",
|
|
125
|
+
"accountMe": "/v1/shop/auth/me",
|
|
126
|
+
"bridgeExchange": "/v1/licenses/bridge/exchange",
|
|
127
|
+
"bridgeFromActivation": "/v1/licenses/bridge/from-activation",
|
|
128
|
+
"bridgeFromPlay": "/v1/licenses/bridge/from-play",
|
|
129
|
+
"bridgeFromStorekit": "/v1/licenses/bridge/from-storekit",
|
|
130
|
+
"entitlement": "/v1/entitlement",
|
|
131
|
+
"leaseIssue": "/v1/pro/{app}/devices/lease",
|
|
132
|
+
"leaseRemove": "/v1/pro/{app}/devices/lease/remove",
|
|
133
|
+
"leaseRenew": "/v1/pro/{app}/devices/lease/renew",
|
|
134
|
+
"licenseActivate": "/v1/licenses/activate",
|
|
135
|
+
"licenseDeactivate": "/v1/licenses/deactivate",
|
|
136
|
+
"licenseRefresh": "/v1/licenses/refresh",
|
|
137
|
+
"licenseTrial": "/v1/licenses/trial",
|
|
138
|
+
"windowsLinkRedeem": "/v1/pro/{app}/devices/windows-link/redeem",
|
|
139
|
+
"windowsLinkRequest": "/v1/pro/{app}/devices/windows-link/request"
|
|
140
|
+
},
|
|
64
141
|
"host": "api.orthiclabs.com",
|
|
142
|
+
"licensePublicKeyHex": "730732885355c4400c115592a42c91b8b0277d460cb2b5d59c1e2815a74d5d22",
|
|
65
143
|
"origin": "https://api.orthiclabs.com",
|
|
66
144
|
"paths": {
|
|
67
145
|
"adminPatches": "/v1/admin/apps/patches",
|
|
@@ -71,8 +149,20 @@ export const RIGHT_SUITE_API_CONTRACT = {
|
|
|
71
149
|
"updaterManifest": "/v1/apps/{app}/releases/latest.json?platform={{target}}"
|
|
72
150
|
},
|
|
73
151
|
"pendingNativeUpdater": [
|
|
74
|
-
"
|
|
152
|
+
"cutright",
|
|
153
|
+
"genright",
|
|
154
|
+
"heardright",
|
|
155
|
+
"postright",
|
|
156
|
+
"pulse",
|
|
157
|
+
"scraperight"
|
|
75
158
|
],
|
|
76
159
|
"publicInstallerBase": "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev",
|
|
77
|
-
"schema": "right-suite-api.v1"
|
|
160
|
+
"schema": "right-suite-api.v1",
|
|
161
|
+
"updaterPublicKeys": [
|
|
162
|
+
{
|
|
163
|
+
"kid": "29395F9FF466261D",
|
|
164
|
+
"publicKey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI5Mzk1RjlGRjQ2NjI2MUQKUldRZEptYjBuMTg1S1VSUXlBdFM4WmtzaHArYko0U2hRMDVlSDJmSExVZG82Q0hoQ2srUlhqanAK"
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"updaterSigningKeyId": "29395F9FF466261D"
|
|
78
168
|
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// The suite's trust roots have one owner, contracts/right-suite-api.v1.json (schema right-suite-api.v1). These preflight rules find
|
|
2
|
+
// what an app does instead of taking them from it: a pasted copy of the licence public key (any spelling), an updater key in a
|
|
3
|
+
// Tauri config that the contract does not list, and the engine reading the owner's record store itself. Pure functions over
|
|
4
|
+
// (root, files), each returning [{ file, subject, problem, fix }]; packages/release/app-check.mjs registers them (lane C3, so an app's `rightkit-gate check` runs
|
|
5
|
+
// them too) and an app's existing findings are ratcheted like G13-G21.
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { RIGHT_SUITE_LICENSE_PUBLIC_KEY_HEX, RIGHT_SUITE_UPDATER_PUBLIC_KEYS } from "./suite-api.generated.mjs";
|
|
9
|
+
|
|
10
|
+
const literal = (text) => text.replace(/[\\^$.*+?()[\]{}|/]/g, "\\$&");
|
|
11
|
+
|
|
12
|
+
// Files the rules do not read: documentation and dependency trees, and test code, which pastes keys on purpose to prove them.
|
|
13
|
+
// Segments of the test directories are matched without case: Swift packages call theirs Tests.
|
|
14
|
+
// `docs` is documentation only at the repository root: a TypeScript app's src/docs is source. `target` stays skipped at any depth (Cargo build output sits in each crate).
|
|
15
|
+
const SKIPPED_TREE = /^docs\/|(^|\/)(node_modules|target|\.git)\//;
|
|
16
|
+
const TEST_SEGMENT = /(^|\/)(tests?|__tests__|test-vectors|fixtures)(\/|$)/i;
|
|
17
|
+
const TEST_NAME = /\.(?:test|spec)\.|_tests?\./;
|
|
18
|
+
const scanned = (file) => !SKIPPED_TREE.test(file) && !TEST_SEGMENT.test(file) && !TEST_NAME.test(path.posix.basename(file)) && !/\.md$/i.test(file);
|
|
19
|
+
|
|
20
|
+
// A tauri*.conf.json carries the updater key by design (suiteUpdaterKey checks that copy), so the pasted-key rule leaves it alone.
|
|
21
|
+
const TAURI_CONF = /(^|\/)tauri[^/]*\.conf\.json$/;
|
|
22
|
+
|
|
23
|
+
// What the contract's owner generates from it: RightKit holds the key by definition, apps do not. RightKit is recognised by its
|
|
24
|
+
// generator, which no app carries.
|
|
25
|
+
const OWNER_GENERATOR = "scripts/gen-suite-api.mjs";
|
|
26
|
+
const OWNER_FILES = new Set([
|
|
27
|
+
"contracts/right-suite-api.v1.json",
|
|
28
|
+
"crates/rightkit-license/src/suite_api_generated.rs",
|
|
29
|
+
"crates/rightkit-license/test-vectors/right-suite-api.v1.json",
|
|
30
|
+
"packages/app-update/src/suite-api.generated.ts",
|
|
31
|
+
"packages/release/suite-api.generated.mjs",
|
|
32
|
+
"swift/RightKitSwift/Sources/RightKitLicense/RightSuiteAPI.generated.swift",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/** The text of a tracked file, or null when it cannot be read or is binary (a NUL byte: images, archives, UTF-16). */
|
|
36
|
+
function textOf(root, file) {
|
|
37
|
+
let bytes;
|
|
38
|
+
try { bytes = fs.readFileSync(path.join(root, file)); } catch { return null; }
|
|
39
|
+
return bytes.includes(0) ? null : bytes.toString("utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The distinct 1-based lines on which `pattern` (global) starts a match, ascending. */
|
|
43
|
+
function matchLines(text, pattern) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
let line = 1;
|
|
46
|
+
let at = 0;
|
|
47
|
+
for (const m of text.matchAll(pattern)) {
|
|
48
|
+
for (; at < m.index; at += 1) if (text.charCodeAt(at) === 10) line += 1;
|
|
49
|
+
if (lines.at(-1) !== line) lines.push(line);
|
|
50
|
+
}
|
|
51
|
+
return lines;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const listed = (lines) => `${lines.slice(0, 8).join(",")}${lines.length > 8 ? ",…" : ""}`;
|
|
55
|
+
const lineAt = (text, offset) => (offset < 0 ? 1 : text.slice(0, offset).split("\n").length);
|
|
56
|
+
|
|
57
|
+
// An element of a pasted byte array may carry Rust's integer suffix.
|
|
58
|
+
const INT_SUFFIX = "(?:_?u8)?";
|
|
59
|
+
// Whitespace and comments (`// s` after each byte, `/* s */` before a comma) may sit between elements.
|
|
60
|
+
const FILL = "(?:\\s|//[^\\n]*|/\\*[\\s\\S]*?\\*/)*";
|
|
61
|
+
const SEPARATOR = `${FILL},${FILL}`;
|
|
62
|
+
|
|
63
|
+
/** One pattern per way an app pastes the licence key, each global: [{ subject, what, pattern }], derived from the contract's hex so
|
|
64
|
+
* a rotation changes what is found without touching this file. The 64-digit hex and the base64 forms are matched inside longer
|
|
65
|
+
* text on purpose: a PEM or a concatenation that contains the key still pastes it, and no other text contains it by chance. */
|
|
66
|
+
export function licenceKeyPatterns(hex = RIGHT_SUITE_LICENSE_PUBLIC_KEY_HEX) {
|
|
67
|
+
if (!/^[0-9a-f]{64}$/i.test(hex)) throw new Error(`the suite licence public key is not 64 hex digits: ${JSON.stringify(hex)}`);
|
|
68
|
+
const key = Buffer.from(hex, "hex");
|
|
69
|
+
const bytes = [...key];
|
|
70
|
+
const array = (element) => bytes.map(element).join(SEPARATOR);
|
|
71
|
+
const decimal = array((b) => `${b}${INT_SUFFIX}`);
|
|
72
|
+
// 0x7 and 0x07 are the same byte.
|
|
73
|
+
const prefixed = array((b) => `0x${b < 16 ? "0?" : ""}${b.toString(16)}${INT_SUFFIX}`);
|
|
74
|
+
// The two base64 alphabets differ only at + / and - _, so a key whose encoding has none of them reads the same in both. The
|
|
75
|
+
// unpadded text then counts as base64url and only the padded text as base64, which keeps one paste at one finding.
|
|
76
|
+
const url = key.toString("base64url");
|
|
77
|
+
const standard = key.toString("base64").replace(/=+$/, "");
|
|
78
|
+
const same = url === standard;
|
|
79
|
+
return [
|
|
80
|
+
{ subject: "licence-key:bytes", what: "as a byte array", pattern: new RegExp(`(?<![\\w.])(?:${decimal}|${prefixed})(?![\\w.])`, "gi") },
|
|
81
|
+
{ subject: "licence-key:escaped", what: "as \\x escapes", pattern: new RegExp(bytes.map((b) => `\\\\x${b < 16 ? "0" : ""}${b.toString(16)}`).join(""), "gi") },
|
|
82
|
+
{ subject: "licence-key:hex", what: "as 64 hex digits", pattern: new RegExp(hex, "gi") },
|
|
83
|
+
{ subject: "licence-key:base64url", what: "as unpadded base64url", pattern: new RegExp(`${literal(url)}${same ? "(?!=)" : ""}`, "g") },
|
|
84
|
+
{ subject: "licence-key:base64", what: "as base64", pattern: new RegExp(`${literal(standard)}${same ? "=" : "=?"}`, "g") },
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The pattern for the text of any contract updater key (the base64 a tauri.conf.json carries), or null with no keys. */
|
|
89
|
+
export function updaterKeyPattern(keys = RIGHT_SUITE_UPDATER_PUBLIC_KEYS) {
|
|
90
|
+
const texts = keys.map((k) => k.publicKey);
|
|
91
|
+
// An empty alternative would match every file.
|
|
92
|
+
if (texts.some((t) => typeof t !== "string" || t === "")) throw new Error("a suite updater public key has no text");
|
|
93
|
+
return texts.length ? new RegExp(texts.map(literal).join("|"), "g") : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const KEY_SOURCES = "rightkit_license::suite_api::LICENSE_PUBLIC_KEY (Rust), the suite-api export of @rightkit/app-update (TypeScript) or RightSuiteAPI (Swift)";
|
|
97
|
+
|
|
98
|
+
/** The licence public key in any spelling, or the updater key text outside a tauri*.conf.json, in a tracked non-test file. One
|
|
99
|
+
* finding per file and spelling; the line numbers are in the problem text only, so an edit above does not change the identity. */
|
|
100
|
+
export function suiteKeyLiteral(root, files) {
|
|
101
|
+
const updater = updaterKeyPattern();
|
|
102
|
+
const forms = [
|
|
103
|
+
...licenceKeyPatterns().map((form) => ({ ...form, what: `carries the suite licence public key ${form.what}`, fix: `take the key from ${KEY_SOURCES}; never paste a copy` })),
|
|
104
|
+
...(updater ? [{ subject: "updater-key", what: "carries the suite updater public key text", pattern: updater, skipsTauriConf: true, fix: `plugins.updater.pubkey of a tauri.conf.json is the one committed copy (Tauri's plugin config requires it); anywhere else take the key from ${KEY_SOURCES}` }] : []),
|
|
105
|
+
];
|
|
106
|
+
const owner = files.includes(OWNER_GENERATOR);
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
if (!scanned(file) || (owner && OWNER_FILES.has(file))) continue;
|
|
110
|
+
const text = textOf(root, file);
|
|
111
|
+
if (text === null) continue;
|
|
112
|
+
for (const form of forms) {
|
|
113
|
+
if (form.skipsTauriConf && TAURI_CONF.test(file)) continue;
|
|
114
|
+
const lines = matchLines(text, form.pattern);
|
|
115
|
+
if (lines.length) out.push({ file, subject: form.subject, problem: `${file}:${listed(lines)} ${form.what}`, fix: form.fix });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
122
|
+
|
|
123
|
+
/** `(file, text) => boolean`: whether `text` carries a marker that the nearest placeholders.json above `file` lists for that file. A
|
|
124
|
+
* placeholders.json is a template's own ledger of what an app generated from it must replace; an app has none. */
|
|
125
|
+
function declaredPlaceholders(root, files) {
|
|
126
|
+
const ledgers = new Map();
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
if (path.posix.basename(file) !== "placeholders.json" || !scanned(file)) continue;
|
|
129
|
+
const text = textOf(root, file);
|
|
130
|
+
let list;
|
|
131
|
+
try { list = JSON.parse(text ?? "").replace; } catch { continue; }
|
|
132
|
+
if (!Array.isArray(list)) continue;
|
|
133
|
+
ledgers.set(path.posix.dirname(file), list.filter((e) => isObject(e) && typeof e.file === "string" && typeof e.marker === "string" && e.marker !== ""));
|
|
134
|
+
}
|
|
135
|
+
return (file, text) => {
|
|
136
|
+
for (let dir = path.posix.dirname(file); ; dir = path.posix.dirname(dir)) {
|
|
137
|
+
const list = ledgers.get(dir);
|
|
138
|
+
if (list) {
|
|
139
|
+
const relative = path.posix.relative(dir, file);
|
|
140
|
+
return list.some((entry) => entry.file === relative && text.includes(entry.marker));
|
|
141
|
+
}
|
|
142
|
+
if (dir === "." || dir === "/") return false;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Every tracked tauri*.conf.json whose plugins.updater block does not carry a copy of a contract key: a missing key, a placeholder,
|
|
148
|
+
* another key, or a file that is not JSON. A file with no plugins.updater block (a platform override) has nothing to check. */
|
|
149
|
+
export function suiteUpdaterKey(root, files) {
|
|
150
|
+
const declared = declaredPlaceholders(root, files);
|
|
151
|
+
const accepted = new Set(RIGHT_SUITE_UPDATER_PUBLIC_KEYS.map((k) => k.publicKey));
|
|
152
|
+
const fix = "set plugins.updater.pubkey to a copy of a publicKey in contracts/right-suite-api.v1.json updaterPublicKeys; the conf holds a copy only because Tauri's plugin config requires one, and the app takes the key from the contract everywhere else";
|
|
153
|
+
const out = [];
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
if (!TAURI_CONF.test(file) || !scanned(file)) continue;
|
|
156
|
+
const text = textOf(root, file);
|
|
157
|
+
if (text === null) continue;
|
|
158
|
+
let conf;
|
|
159
|
+
// A byte-order mark is how some Windows editors save JSON; Tauri reads it, so it is not a defect here.
|
|
160
|
+
try { conf = JSON.parse(text.replace(/^/, "")); } catch { out.push({ file, subject: "unparseable", problem: `${file} is not valid JSON, so its plugins.updater.pubkey cannot be checked`, fix }); continue; }
|
|
161
|
+
const updater = conf?.plugins?.updater;
|
|
162
|
+
if (!isObject(updater)) continue;
|
|
163
|
+
const block = text.search(/"updater"\s*:/);
|
|
164
|
+
const { pubkey } = updater;
|
|
165
|
+
// A template's marker is handled by its own placeholders.json (the template contract refuses a marker that is not listed there).
|
|
166
|
+
if (typeof pubkey === "string" && declared(file, pubkey)) continue;
|
|
167
|
+
let why;
|
|
168
|
+
let offset = block;
|
|
169
|
+
// A platform override (tauri.windows.conf.json) is merged over the base config, so it may leave the key to it.
|
|
170
|
+
if (pubkey === undefined) { if (!/(^|\/)tauri\.conf\.json$/.test(file)) continue; why = "has no plugins.updater.pubkey"; }
|
|
171
|
+
else {
|
|
172
|
+
offset = text.indexOf('"pubkey"', Math.max(block, 0));
|
|
173
|
+
if (typeof pubkey !== "string") why = "has a plugins.updater.pubkey that is not a string";
|
|
174
|
+
else if (!accepted.has(pubkey)) why = `has plugins.updater.pubkey "${pubkey.slice(0, 32)}${pubkey.length > 32 ? "…" : ""}", which is not a publicKey of the suite contract`;
|
|
175
|
+
}
|
|
176
|
+
if (why) out.push({ file, subject: "plugins.updater.pubkey", problem: `${file}:${lineAt(text, offset)} ${why}`, fix });
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// The owner binary hands the engine a snapshot of the licence record (rightkit_credentials::handoff); an engine that opens the record
|
|
182
|
+
// store itself reads a keychain item across binaries. Assembled so this file does not itself carry the call it refuses.
|
|
183
|
+
const RECORD_STORE_READER = "from_record_store";
|
|
184
|
+
const RECORD_STORE_CALL = new RegExp(literal(`${RECORD_STORE_READER}(`), "g");
|
|
185
|
+
// The crate that defines the reader, and the one place that may call it.
|
|
186
|
+
const RECORD_STORE_OWNER = "crates/rightkit-license/";
|
|
187
|
+
|
|
188
|
+
/** A tracked non-test file outside the licence crate that calls the engine's record-store reader. */
|
|
189
|
+
export function engineRecordStore(root, files) {
|
|
190
|
+
const out = [];
|
|
191
|
+
for (const file of files) {
|
|
192
|
+
if (!scanned(file) || file.startsWith(RECORD_STORE_OWNER)) continue;
|
|
193
|
+
const text = textOf(root, file);
|
|
194
|
+
if (text === null || !text.includes(`${RECORD_STORE_READER}(`)) continue;
|
|
195
|
+
out.push({ file, subject: RECORD_STORE_READER, problem: `${file}:${listed(matchLines(text, RECORD_STORE_CALL))} calls ${RECORD_STORE_READER}: the engine reads the owner's record store itself`, fix: 'have the owner binary hand the engine a snapshot through rightkit_credentials::handoff instead (rightkit-credentials README "Helpers, sidecars and CLIs")' });
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
package/test-command.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `right-release test [--config <file>] [--suite <name>[,<name>]]... [--runner <name>] [--list] [--json] [-- <args>]`
|
|
3
|
-
// runs the app's declared test suites from `right-release.config.
|
|
3
|
+
// runs the app's declared test suites from its release config (`right-release.config.json`), one after another, and stops at the first failure.
|
|
4
4
|
//
|
|
5
5
|
// tests: {
|
|
6
6
|
// eventPrefix: "e2e", // optional; events are `<prefix>_suite_started` / `<prefix>_suite_completed`
|
|
@@ -26,6 +26,7 @@ import path from "node:path";
|
|
|
26
26
|
import { pathToFileURL } from "node:url";
|
|
27
27
|
import { spawn } from "./background-process.mjs";
|
|
28
28
|
import { CommandSpecError, loadAppConfig, resolveInvocation, validateStep } from "./command-spec.mjs";
|
|
29
|
+
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
29
30
|
|
|
30
31
|
const fail = (message) => { throw new CommandSpecError(message); };
|
|
31
32
|
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -34,7 +35,7 @@ const PREFIX = /^[a-z][a-z0-9_]*$/;
|
|
|
34
35
|
const uniq = (...lists) => [...new Set(lists.flatMap((list) => list ?? []))];
|
|
35
36
|
|
|
36
37
|
export function parseTestArgs(argv) {
|
|
37
|
-
const opts = { config:
|
|
38
|
+
const opts = { config: defaultReleaseConfigFile(), suites: [], runner: undefined, list: false, json: false, passthrough: [] };
|
|
38
39
|
for (let i = 0; i < argv.length; i += 1) {
|
|
39
40
|
const arg = argv[i];
|
|
40
41
|
if (arg === "--") { opts.passthrough = argv.slice(i + 1); break; }
|
package/upload-release.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
|
|
6
|
+
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
6
7
|
import { authorizeGitHubUpload } from "./github-upload-gate.mjs";
|
|
7
8
|
import { recordPublishedVersion } from "./artifact-storage.mjs";
|
|
8
9
|
import { admitInstalledQa } from "./release-qa-audit.mjs";
|
|
@@ -12,7 +13,7 @@ let platform = process.platform === "win32" ? "win" : process.platform === "darw
|
|
|
12
13
|
let releaseId = "";
|
|
13
14
|
let tier = "";
|
|
14
15
|
let repo = "";
|
|
15
|
-
let configName =
|
|
16
|
+
let configName = defaultReleaseConfigFile();
|
|
16
17
|
let dryRun = false;
|
|
17
18
|
let apply = false;
|
|
18
19
|
|
|
@@ -87,7 +88,7 @@ function writeJson(file, value) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function usage(code) {
|
|
90
|
-
console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.
|
|
91
|
+
console.log("usage: right-release upload --release <sealed-id> --platform win|mac --tier patch|update [--config right-release.config.json] [--repo owner/repo] [--dry-run]\n right-release upload --platform android --release <sealed-id> [--config right-release.config.json] [--apply] (Play internal track; dry run unless --apply)\n right-release upload --platform ios --release <sealed-id> [--config right-release.config.json] [--apply] (TestFlight; dry run unless --apply)");
|
|
91
92
|
process.exit(code);
|
|
92
93
|
}
|
|
93
94
|
|
package/web-build.mjs
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
import { pathToFileURL } from "node:url";
|
|
16
16
|
import { spawnSync } from "./background-process.mjs";
|
|
17
17
|
import { CommandSpecError, loadAppConfig, resolveInvocation, validateStep } from "./command-spec.mjs";
|
|
18
|
+
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
18
19
|
|
|
19
20
|
export function parseWebBuildArgs(argv) {
|
|
20
|
-
const opts = { config:
|
|
21
|
+
const opts = { config: defaultReleaseConfigFile(), dryRun: false };
|
|
21
22
|
for (let i = 0; i < argv.length; i += 1) {
|
|
22
23
|
const arg = argv[i];
|
|
23
24
|
if (arg === "--config") {
|