@orbit-work/oem-sdk 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.
- package/bin/orbit-audit-brand.mjs +3 -0
- package/dist/audit-brand.js +721 -0
- package/dist/audit-brand.js.map +1 -0
- package/dist/audit.d.ts +91 -0
- package/dist/audit.js +704 -0
- package/dist/audit.js.map +1 -0
- package/dist/chunk-5MBEDX73.js +543 -0
- package/dist/chunk-5MBEDX73.js.map +1 -0
- package/dist/chunk-YMKYAM23.js +194 -0
- package/dist/chunk-YMKYAM23.js.map +1 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +139 -0
- package/dist/manifest.js +3 -0
- package/dist/manifest.js.map +1 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +4 -0
- package/dist/runtime.js.map +1 -0
- package/dist/vite.d.ts +21 -0
- package/dist/vite.js +19 -0
- package/dist/vite.js.map +1 -0
- package/package.json +62 -0
- package/schema/orbit-brand.schema.json +314 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpath, readFile, stat } from 'fs/promises';
|
|
3
|
+
import { resolve, extname, sep, dirname } from 'path';
|
|
4
|
+
import { gzipSync } from 'zlib';
|
|
5
|
+
import { init, parse } from 'es-module-lexer';
|
|
6
|
+
import { transform } from 'esbuild';
|
|
7
|
+
import { validateDictionary } from '@orbit-work/ui/locale';
|
|
8
|
+
import { UNIFIED_NEUTRAL_TOKENS, BRAND_ACTION_TOKENS, ds2ContrastRatio } from '@orbit-work/ui/appearance';
|
|
9
|
+
import { WORKBENCH_SLOT_DECLARATIONS, WORKBENCH_KEYED_SLOT_KEYS } from '@orbit-work/ui/slot/core';
|
|
10
|
+
|
|
11
|
+
var BRAND_MANIFEST_SCHEMA = "@orbit-work/oem-sdk/manifest@1";
|
|
12
|
+
var ROOT_KEYS = /* @__PURE__ */ new Set([
|
|
13
|
+
"$schema",
|
|
14
|
+
"id",
|
|
15
|
+
"description",
|
|
16
|
+
"assistantName",
|
|
17
|
+
"homepage",
|
|
18
|
+
"permissions",
|
|
19
|
+
"entry",
|
|
20
|
+
"identity",
|
|
21
|
+
"tokens",
|
|
22
|
+
"assets",
|
|
23
|
+
"branding",
|
|
24
|
+
"locales",
|
|
25
|
+
"network",
|
|
26
|
+
"contributes"
|
|
27
|
+
]);
|
|
28
|
+
var IDENTITY_KEYS = /* @__PURE__ */ new Set(["productName", "appId", "versionChannel", "protocolScheme", "userDataNamespace"]);
|
|
29
|
+
var ASSET_KEYS = /* @__PURE__ */ new Set(["appIcon", "tray", "logo"]);
|
|
30
|
+
var MODE_KEYS = /* @__PURE__ */ new Set(["light", "dark"]);
|
|
31
|
+
var CONTRIBUTES_KEYS = /* @__PURE__ */ new Set(["slots"]);
|
|
32
|
+
var NETWORK_KEYS = /* @__PURE__ */ new Set(["allowedOrigins"]);
|
|
33
|
+
var PERMISSIONS_KEYS = /* @__PURE__ */ new Set(["microphoneUsage"]);
|
|
34
|
+
var BRANDING_KEYS = /* @__PURE__ */ new Set(["productName", "companyName", "claim", "claimNote", "slogan"]);
|
|
35
|
+
var SLOT_COMMON_KEYS = /* @__PURE__ */ new Set(["name"]);
|
|
36
|
+
var BRAND_TOKEN_NAMES = /* @__PURE__ */ new Set([...UNIFIED_NEUTRAL_TOKENS, ...BRAND_ACTION_TOKENS]);
|
|
37
|
+
var SLOT_DECLARATIONS = WORKBENCH_SLOT_DECLARATIONS;
|
|
38
|
+
var HEX = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
39
|
+
var KEBAB_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
40
|
+
var APP_ID = /^[a-zA-Z][a-zA-Z0-9]*(?:\.[a-zA-Z0-9][a-zA-Z0-9-]*){2,}$/;
|
|
41
|
+
var PROTOCOL_SCHEME = /^[a-z][a-z0-9+.-]*$/;
|
|
42
|
+
var SOURCE_ENTRY = /^\.\/src\/.+\.(?:[cm]?js|[jt]sx?)$/;
|
|
43
|
+
var WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
44
|
+
var OFFICIAL_APP_ID = "ai.orbit.desktop";
|
|
45
|
+
var OFFICIAL_PRODUCT_NAMES = /* @__PURE__ */ new Set(["orbitwork"]);
|
|
46
|
+
var RESERVED_VERSION_CHANNELS = /* @__PURE__ */ new Set([
|
|
47
|
+
"stable",
|
|
48
|
+
"preview",
|
|
49
|
+
"uat",
|
|
50
|
+
"latest",
|
|
51
|
+
"desktop",
|
|
52
|
+
"macos",
|
|
53
|
+
"windows",
|
|
54
|
+
"linux"
|
|
55
|
+
]);
|
|
56
|
+
var RESERVED_PROTOCOL_SCHEMES = /* @__PURE__ */ new Set([
|
|
57
|
+
"about",
|
|
58
|
+
"blob",
|
|
59
|
+
"chrome",
|
|
60
|
+
"chrome-extension",
|
|
61
|
+
"data",
|
|
62
|
+
"devtools",
|
|
63
|
+
"electron",
|
|
64
|
+
"file",
|
|
65
|
+
"ftp",
|
|
66
|
+
"geo",
|
|
67
|
+
"http",
|
|
68
|
+
"https",
|
|
69
|
+
"javascript",
|
|
70
|
+
"mailto",
|
|
71
|
+
"orbit",
|
|
72
|
+
"sms",
|
|
73
|
+
"tel",
|
|
74
|
+
"urn",
|
|
75
|
+
"ws",
|
|
76
|
+
"wss"
|
|
77
|
+
]);
|
|
78
|
+
function isRecord(value) {
|
|
79
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
function hasUnsafeProductNameCharacter(value) {
|
|
82
|
+
return [...value].some((character) => {
|
|
83
|
+
const code = character.charCodeAt(0);
|
|
84
|
+
return code <= 31 || code === 127 || '<>:"/\\|?*'.includes(character);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function productNameNamespace(value) {
|
|
88
|
+
return value.normalize("NFKD").replace(/\p{M}+/gu, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
89
|
+
}
|
|
90
|
+
function assertKnownKeys(value, allowed, path) {
|
|
91
|
+
const unknown = Object.keys(value).find((key) => !allowed.has(key));
|
|
92
|
+
if (unknown) throw new Error(`${path} contains unsupported field: ${unknown}`);
|
|
93
|
+
}
|
|
94
|
+
function text(value, path, limit = 100) {
|
|
95
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${path} must be a non-empty string`);
|
|
96
|
+
const result = value.trim();
|
|
97
|
+
if (result.length > limit) throw new Error(`${path} exceeds ${limit} characters`);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function canonicalLocale(value, path) {
|
|
101
|
+
try {
|
|
102
|
+
const locale = Intl.getCanonicalLocales(value)[0];
|
|
103
|
+
if (!locale || locale !== value) throw new Error();
|
|
104
|
+
return locale;
|
|
105
|
+
} catch {
|
|
106
|
+
throw new Error(`${path} must be a canonical BCP 47 locale`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function relativePath(value, path) {
|
|
110
|
+
const result = text(value, path, 240);
|
|
111
|
+
if (!result.startsWith("./") || result.includes("\\") || result.split("/").includes("..")) {
|
|
112
|
+
throw new Error(`${path} must be a package-relative ./ path without traversal`);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function parseIdentity(value, id, platform) {
|
|
117
|
+
if (!isRecord(value)) throw new Error("identity must be an object");
|
|
118
|
+
assertKnownKeys(value, IDENTITY_KEYS, "identity");
|
|
119
|
+
const productName = text(value.productName, "identity.productName", 64);
|
|
120
|
+
const appId = text(value.appId, "identity.appId", 160);
|
|
121
|
+
const versionChannel = text(value.versionChannel, "identity.versionChannel", 64);
|
|
122
|
+
const protocolScheme = text(value.protocolScheme, "identity.protocolScheme", 64);
|
|
123
|
+
if (value.userDataNamespace !== void 0 && !KEBAB_ID.test(text(value.userDataNamespace, "identity.userDataNamespace", 64))) {
|
|
124
|
+
throw new Error("identity.userDataNamespace must be lowercase kebab-case");
|
|
125
|
+
}
|
|
126
|
+
if (!APP_ID.test(appId)) throw new Error("identity.appId must be a reverse-DNS identifier");
|
|
127
|
+
if (!KEBAB_ID.test(versionChannel)) throw new Error("identity.versionChannel must be kebab-case");
|
|
128
|
+
if (!platform && RESERVED_VERSION_CHANNELS.has(versionChannel)) {
|
|
129
|
+
throw new Error(`identity.versionChannel is reserved: ${versionChannel}`);
|
|
130
|
+
}
|
|
131
|
+
if (!PROTOCOL_SCHEME.test(protocolScheme)) {
|
|
132
|
+
throw new Error("identity.protocolScheme must be a lowercase URI scheme beginning with a letter");
|
|
133
|
+
}
|
|
134
|
+
if (RESERVED_PROTOCOL_SCHEMES.has(protocolScheme)) {
|
|
135
|
+
throw new Error(`identity.protocolScheme is reserved: ${protocolScheme}`);
|
|
136
|
+
}
|
|
137
|
+
if (hasUnsafeProductNameCharacter(productName) || /[. ]$/.test(productName) || productName === "." || productName === ".." || WINDOWS_DEVICE_NAME.test(productName)) {
|
|
138
|
+
throw new Error("identity.productName must be safe for macOS, Windows, Linux, and release artifact paths");
|
|
139
|
+
}
|
|
140
|
+
if (OFFICIAL_PRODUCT_NAMES.has(productName.toLowerCase())) {
|
|
141
|
+
throw new Error(`identity.productName is reserved for the official application: ${productName}`);
|
|
142
|
+
}
|
|
143
|
+
if (appId === OFFICIAL_APP_ID) {
|
|
144
|
+
throw new Error(`identity.appId is reserved for the official application: ${appId}`);
|
|
145
|
+
}
|
|
146
|
+
if (!platform && productNameNamespace(productName) !== id) {
|
|
147
|
+
throw new Error("identity.productName must normalize to manifest id so installation names cannot collide");
|
|
148
|
+
}
|
|
149
|
+
if (!platform && versionChannel !== id) {
|
|
150
|
+
throw new Error("identity.versionChannel must equal manifest id to keep OEM update feeds namespaced");
|
|
151
|
+
}
|
|
152
|
+
if (!platform && protocolScheme !== id) {
|
|
153
|
+
throw new Error("identity.protocolScheme must equal manifest id to keep OEM deep links namespaced");
|
|
154
|
+
}
|
|
155
|
+
if (!platform && !appId.endsWith(`.oem.${id}`)) {
|
|
156
|
+
throw new Error("identity.appId must end with .oem.<manifest id> to isolate installation and user data");
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
productName,
|
|
160
|
+
appId,
|
|
161
|
+
versionChannel,
|
|
162
|
+
protocolScheme,
|
|
163
|
+
...value.userDataNamespace !== void 0 ? { userDataNamespace: text(value.userDataNamespace, "identity.userDataNamespace", 64) } : {}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function parseAssets(value) {
|
|
167
|
+
if (!isRecord(value)) throw new Error("assets must be an object");
|
|
168
|
+
assertKnownKeys(value, ASSET_KEYS, "assets");
|
|
169
|
+
const tray = value.tray === void 0 ? void 0 : relativePath(value.tray, "assets.tray");
|
|
170
|
+
return {
|
|
171
|
+
appIcon: relativePath(value.appIcon, "assets.appIcon"),
|
|
172
|
+
...tray ? { tray } : {},
|
|
173
|
+
logo: relativePath(value.logo, "assets.logo")
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function parseBrandCopy(value, path) {
|
|
177
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object`);
|
|
178
|
+
assertKnownKeys(value, BRANDING_KEYS, path);
|
|
179
|
+
const result = {
|
|
180
|
+
productName: text(value.productName, `${path}.productName`, 64),
|
|
181
|
+
companyName: text(value.companyName, `${path}.companyName`, 80)
|
|
182
|
+
};
|
|
183
|
+
for (const [key, limit] of [
|
|
184
|
+
["claim", 40],
|
|
185
|
+
["claimNote", 48],
|
|
186
|
+
["slogan", 68]
|
|
187
|
+
]) {
|
|
188
|
+
if (value[key] !== void 0) result[key] = text(value[key], `${path}.${key}`, limit);
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
function parseBranding(value) {
|
|
193
|
+
if (!isRecord(value)) throw new Error("branding must be a locale map");
|
|
194
|
+
const result = {};
|
|
195
|
+
for (const [rawLocale, copy] of Object.entries(value)) {
|
|
196
|
+
const locale = canonicalLocale(rawLocale, `branding.${rawLocale}`);
|
|
197
|
+
result[locale] = parseBrandCopy(copy, `branding.${locale}`);
|
|
198
|
+
}
|
|
199
|
+
if (!result["en-US"]) throw new Error("branding.en-US is required");
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
function parseLocales(value) {
|
|
203
|
+
if (value === void 0) return void 0;
|
|
204
|
+
if (!isRecord(value)) throw new Error("locales must be a locale map");
|
|
205
|
+
const result = {};
|
|
206
|
+
for (const [rawLocale, source] of Object.entries(value)) {
|
|
207
|
+
const locale = canonicalLocale(rawLocale, `locales.${rawLocale}`);
|
|
208
|
+
result[locale] = relativePath(source, `locales.${locale}`);
|
|
209
|
+
}
|
|
210
|
+
if (!result["en-US"]) throw new Error("locales.en-US is required when locales are declared");
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
function isDevelopmentHostname(hostname) {
|
|
214
|
+
const lower = hostname.toLowerCase();
|
|
215
|
+
if (lower === "localhost" || lower.endsWith(".localhost") || lower.endsWith(".local")) return true;
|
|
216
|
+
if (["::1", "[::1]"].includes(lower) || /^(?:fc|fd|fe8|fe9|fea|feb)/.test(lower.replace(/[\[\]]/g, ""))) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const ipv4 = lower.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)?.slice(1).map(Number);
|
|
220
|
+
if (ipv4) {
|
|
221
|
+
const [a, b] = ipv4;
|
|
222
|
+
if (a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168) {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return lower.split(/[.-]/).some((label) => ["dev", "development", "test", "testing", "stage", "staging", "uat"].includes(label));
|
|
227
|
+
}
|
|
228
|
+
function parseNetwork(value) {
|
|
229
|
+
if (value === void 0) return void 0;
|
|
230
|
+
if (!isRecord(value)) throw new Error("network must be an object");
|
|
231
|
+
assertKnownKeys(value, NETWORK_KEYS, "network");
|
|
232
|
+
if (!Array.isArray(value.allowedOrigins) || value.allowedOrigins.length === 0) {
|
|
233
|
+
throw new Error("network.allowedOrigins must be a non-empty array");
|
|
234
|
+
}
|
|
235
|
+
const allowedOrigins = value.allowedOrigins.map((raw, index) => {
|
|
236
|
+
const path = `network.allowedOrigins[${index}]`;
|
|
237
|
+
const origin = text(raw, path, 240);
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = new URL(origin);
|
|
241
|
+
} catch {
|
|
242
|
+
throw new Error(`${path} must be an exact HTTPS origin`);
|
|
243
|
+
}
|
|
244
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.origin !== origin) {
|
|
245
|
+
throw new Error(`${path} must be a canonical exact HTTPS origin without credentials, path, query, or fragment`);
|
|
246
|
+
}
|
|
247
|
+
if (isDevelopmentHostname(parsed.hostname)) {
|
|
248
|
+
throw new Error(`${path} must be a production origin, not localhost, private network, dev, test, staging, or UAT`);
|
|
249
|
+
}
|
|
250
|
+
return origin;
|
|
251
|
+
});
|
|
252
|
+
if (new Set(allowedOrigins).size !== allowedOrigins.length) {
|
|
253
|
+
throw new Error("network.allowedOrigins must not contain duplicates");
|
|
254
|
+
}
|
|
255
|
+
return { allowedOrigins };
|
|
256
|
+
}
|
|
257
|
+
function parseHomepage(value) {
|
|
258
|
+
const homepage = text(value, "homepage", 200);
|
|
259
|
+
let parsed;
|
|
260
|
+
try {
|
|
261
|
+
parsed = new URL(homepage);
|
|
262
|
+
} catch {
|
|
263
|
+
throw new Error("homepage must be an https URL");
|
|
264
|
+
}
|
|
265
|
+
if (parsed.protocol !== "https:" || !parsed.hostname) {
|
|
266
|
+
throw new Error("homepage must be an https URL");
|
|
267
|
+
}
|
|
268
|
+
return homepage;
|
|
269
|
+
}
|
|
270
|
+
function parsePermissions(value) {
|
|
271
|
+
if (!isRecord(value)) throw new Error("permissions must be an object");
|
|
272
|
+
assertKnownKeys(value, PERMISSIONS_KEYS, "permissions");
|
|
273
|
+
return { microphoneUsage: text(value.microphoneUsage, "permissions.microphoneUsage", 120) };
|
|
274
|
+
}
|
|
275
|
+
function parseModeTokens(value, path) {
|
|
276
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object`);
|
|
277
|
+
const result = {};
|
|
278
|
+
for (const [name, raw] of Object.entries(value)) {
|
|
279
|
+
if (!BRAND_TOKEN_NAMES.has(name)) throw new Error(`${path} contains forbidden token: ${name}`);
|
|
280
|
+
if (typeof raw !== "string" || !HEX.test(raw)) throw new Error(`${path}.${name} must be a hex color`);
|
|
281
|
+
result[name] = raw.toLowerCase();
|
|
282
|
+
}
|
|
283
|
+
const touchedNeutral = UNIFIED_NEUTRAL_TOKENS.some((name) => result[name] !== void 0);
|
|
284
|
+
if (touchedNeutral) {
|
|
285
|
+
const missing = UNIFIED_NEUTRAL_TOKENS.filter((name) => result[name] === void 0);
|
|
286
|
+
if (missing.length) throw new Error(`${path} neutral group is incomplete: ${missing.join(", ")}`);
|
|
287
|
+
for (const textToken of ["--ow-text", "--ow-text-2"]) {
|
|
288
|
+
for (const surfaceToken of ["--ow-bg", "--ow-surface", "--ow-surface-2"]) {
|
|
289
|
+
const ratio = ds2ContrastRatio(result[textToken], result[surfaceToken]);
|
|
290
|
+
if (ratio < 4.5) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`${path} contrast ${textToken} on ${surfaceToken} is ${ratio.toFixed(2)}:1; expected >= 4.5:1`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const touchedAction = BRAND_ACTION_TOKENS.some((name) => result[name] !== void 0);
|
|
299
|
+
if (touchedAction) {
|
|
300
|
+
const missing = BRAND_ACTION_TOKENS.filter((name) => result[name] === void 0);
|
|
301
|
+
if (missing.length) throw new Error(`${path} action pole group is incomplete: ${missing.join(", ")}`);
|
|
302
|
+
for (const background of ["--ow-btn-primary-bg", "--ow-btn-primary-hover", "--ow-btn-primary-press"]) {
|
|
303
|
+
const ratio = ds2ContrastRatio(result[background], result["--ow-btn-primary-fg"]);
|
|
304
|
+
if (ratio < 4.5) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`${path} contrast --ow-btn-primary-fg on ${background} is ${ratio.toFixed(2)}:1; expected >= 4.5:1`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
function parseTokens(value) {
|
|
314
|
+
if (value === void 0) return void 0;
|
|
315
|
+
if (!isRecord(value)) throw new Error("tokens must be an object");
|
|
316
|
+
assertKnownKeys(value, MODE_KEYS, "tokens");
|
|
317
|
+
const light = value.light === void 0 ? void 0 : parseModeTokens(value.light, "tokens.light");
|
|
318
|
+
const dark = value.dark === void 0 ? void 0 : parseModeTokens(value.dark, "tokens.dark");
|
|
319
|
+
if (!light && !dark) throw new Error("tokens must define light or dark");
|
|
320
|
+
return { ...light ? { light } : {}, ...dark ? { dark } : {} };
|
|
321
|
+
}
|
|
322
|
+
function slotAllowedKeys(kind) {
|
|
323
|
+
return /* @__PURE__ */ new Set([
|
|
324
|
+
...SLOT_COMMON_KEYS,
|
|
325
|
+
...kind === "list" ? ["id", "order"] : [],
|
|
326
|
+
...kind === "keyed" ? ["key"] : []
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
329
|
+
function parseSlotContribution(value, index) {
|
|
330
|
+
const path = `contributes.slots[${index}]`;
|
|
331
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object`);
|
|
332
|
+
const name = text(value.name, `${path}.name`, 120);
|
|
333
|
+
const declaration = SLOT_DECLARATIONS[name];
|
|
334
|
+
if (!declaration) throw new Error(`${path}.name is not in the host slot whitelist: ${name}`);
|
|
335
|
+
assertKnownKeys(value, slotAllowedKeys(declaration.kind), path);
|
|
336
|
+
const common = { name };
|
|
337
|
+
if (declaration.kind === "list") {
|
|
338
|
+
const id = text(value.id, `${path}.id`, 100);
|
|
339
|
+
if (typeof value.order !== "number" || !Number.isFinite(value.order)) {
|
|
340
|
+
throw new Error(`${path}.order must be a finite number`);
|
|
341
|
+
}
|
|
342
|
+
return { ...common, id, order: value.order };
|
|
343
|
+
}
|
|
344
|
+
if (declaration.kind === "keyed") {
|
|
345
|
+
const key = text(value.key, `${path}.key`, 100);
|
|
346
|
+
const allowed = WORKBENCH_KEYED_SLOT_KEYS[name];
|
|
347
|
+
if (!allowed?.includes(key)) throw new Error(`${path}.key is not reachable by the host: ${key}`);
|
|
348
|
+
return { ...common, key };
|
|
349
|
+
}
|
|
350
|
+
return common;
|
|
351
|
+
}
|
|
352
|
+
function contributionCell(value) {
|
|
353
|
+
if ("id" in value) return value.id;
|
|
354
|
+
if ("key" in value) return value.key;
|
|
355
|
+
return "single";
|
|
356
|
+
}
|
|
357
|
+
function parseContributes(value) {
|
|
358
|
+
if (value === void 0) return void 0;
|
|
359
|
+
if (!isRecord(value)) throw new Error("contributes must be an object");
|
|
360
|
+
assertKnownKeys(value, CONTRIBUTES_KEYS, "contributes");
|
|
361
|
+
if (!Array.isArray(value.slots)) throw new Error("contributes.slots must be an array");
|
|
362
|
+
const slots = value.slots.map(parseSlotContribution);
|
|
363
|
+
const seen = /* @__PURE__ */ new Set();
|
|
364
|
+
for (const slot of slots) {
|
|
365
|
+
const identity = `${slot.name}\0${contributionCell(slot)}`;
|
|
366
|
+
if (seen.has(identity)) throw new Error(`duplicate slot contribution: ${slot.name} / ${contributionCell(slot)}`);
|
|
367
|
+
seen.add(identity);
|
|
368
|
+
}
|
|
369
|
+
return { slots };
|
|
370
|
+
}
|
|
371
|
+
function parseBrandManifest(input, options) {
|
|
372
|
+
try {
|
|
373
|
+
const platform = options?.platform === true;
|
|
374
|
+
if (!isRecord(input)) throw new Error("brand manifest must be a JSON object");
|
|
375
|
+
assertKnownKeys(input, ROOT_KEYS, "manifest");
|
|
376
|
+
if (input.$schema !== BRAND_MANIFEST_SCHEMA) throw new Error(`$schema must equal ${BRAND_MANIFEST_SCHEMA}`);
|
|
377
|
+
const id = text(input.id, "id", 64);
|
|
378
|
+
if (!KEBAB_ID.test(id)) throw new Error("id must be lowercase kebab-case");
|
|
379
|
+
const entry = input.entry === void 0 ? void 0 : relativePath(input.entry, "entry");
|
|
380
|
+
if (entry && !SOURCE_ENTRY.test(entry)) {
|
|
381
|
+
throw new Error("entry must point to reviewed source under ./src using .js, .mjs, .jsx, .ts, or .tsx");
|
|
382
|
+
}
|
|
383
|
+
const contributes = parseContributes(input.contributes);
|
|
384
|
+
if ((contributes?.slots.length ?? 0) > 0 && !entry)
|
|
385
|
+
throw new Error("entry is required when contributes.slots is not empty");
|
|
386
|
+
const tokens = parseTokens(input.tokens);
|
|
387
|
+
const locales = parseLocales(input.locales);
|
|
388
|
+
const network = parseNetwork(input.network);
|
|
389
|
+
const identity = parseIdentity(input.identity, id, platform);
|
|
390
|
+
if (!platform && productNameNamespace(identity.productName) !== id) {
|
|
391
|
+
throw new Error("identity.productName must normalize to manifest id so installation names cannot collide");
|
|
392
|
+
}
|
|
393
|
+
if (!platform && identity.versionChannel !== id) {
|
|
394
|
+
throw new Error("identity.versionChannel must equal manifest id to keep OEM update feeds namespaced");
|
|
395
|
+
}
|
|
396
|
+
if (!platform && identity.protocolScheme !== id) {
|
|
397
|
+
throw new Error("identity.protocolScheme must equal manifest id to keep OEM deep links namespaced");
|
|
398
|
+
}
|
|
399
|
+
if (!platform && !identity.appId.endsWith(`.oem.${id}`)) {
|
|
400
|
+
throw new Error("identity.appId must end with .oem.<manifest id> to isolate installation and user data");
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
ok: true,
|
|
404
|
+
manifest: {
|
|
405
|
+
$schema: BRAND_MANIFEST_SCHEMA,
|
|
406
|
+
id,
|
|
407
|
+
...input.description !== void 0 ? { description: text(input.description, "description", 200) } : {},
|
|
408
|
+
...input.assistantName !== void 0 ? { assistantName: text(input.assistantName, "assistantName", 48) } : {},
|
|
409
|
+
...input.homepage !== void 0 ? { homepage: parseHomepage(input.homepage) } : {},
|
|
410
|
+
...input.permissions !== void 0 ? { permissions: parsePermissions(input.permissions) } : {},
|
|
411
|
+
...entry ? { entry } : {},
|
|
412
|
+
identity,
|
|
413
|
+
...tokens ? { tokens } : {},
|
|
414
|
+
assets: parseAssets(input.assets),
|
|
415
|
+
branding: parseBranding(input.branding),
|
|
416
|
+
...locales ? { locales } : {},
|
|
417
|
+
...network ? { network } : {},
|
|
418
|
+
...contributes ? { contributes } : {}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
} catch (cause) {
|
|
422
|
+
return { ok: false, error: cause instanceof Error ? cause.message : String(cause) };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/audit-brand.ts
|
|
427
|
+
var ENTRY_GZIP_BUDGET = 256 * 1024;
|
|
428
|
+
var ASSET_BUDGET = 5 * 1024 * 1024;
|
|
429
|
+
var LOCALE_BUDGET = 256 * 1024;
|
|
430
|
+
var IMPORT_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
431
|
+
"@orbit-work/oem-sdk",
|
|
432
|
+
"@orbit-work/oem-sdk/runtime",
|
|
433
|
+
"@orbit-work/ui",
|
|
434
|
+
"react",
|
|
435
|
+
"react-dom",
|
|
436
|
+
"react/jsx-runtime",
|
|
437
|
+
"react/jsx-dev-runtime"
|
|
438
|
+
]);
|
|
439
|
+
var CREDENTIAL_ASSIGNMENT = /\b(?:api[_-]?key|client[_-]?secret|access[_-]?token|password)\b\s*[:=]\s*([^\n;]+)/gi;
|
|
440
|
+
var STATIC_STRING = /"([^"\n]*(?:\\.[^"\n]*)*)"|'([^'\n]*(?:\\.[^'\n]*)*)'|`([^`\n]*(?:\\.[^`\n]*)*)`/g;
|
|
441
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"]);
|
|
442
|
+
var ASSET_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".svg", ".webp", ".jpg", ".jpeg", ".gif", ".woff", ".woff2"]);
|
|
443
|
+
var DATA_EXTENSIONS = /* @__PURE__ */ new Set([".json"]);
|
|
444
|
+
var CSS_IMPORT = /@import\s+(?:url\(\s*)?["']([^"']+)["']\s*\)?/g;
|
|
445
|
+
var CSS_URL = /url\(\s*["']?([^"')]+)["']?\s*\)/g;
|
|
446
|
+
var STATIC_HTTP_URL = /https?:\/\/[^\s"'`<>)]+/g;
|
|
447
|
+
Object.freeze([...IMPORT_ALLOWLIST].sort());
|
|
448
|
+
function containsLikelyHardCodedCredential(source) {
|
|
449
|
+
CREDENTIAL_ASSIGNMENT.lastIndex = 0;
|
|
450
|
+
for (const assignment of source.matchAll(CREDENTIAL_ASSIGNMENT)) {
|
|
451
|
+
const rightHandSide = assignment[1] ?? "";
|
|
452
|
+
const literalValue = [...rightHandSide.matchAll(STATIC_STRING)].map((match) => match[1] ?? match[2] ?? match[3] ?? "").join("");
|
|
453
|
+
if (literalValue.length >= 8) return true;
|
|
454
|
+
}
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
function parseLocaleDictionary(source, label) {
|
|
458
|
+
let value;
|
|
459
|
+
try {
|
|
460
|
+
value = JSON.parse(source);
|
|
461
|
+
} catch {
|
|
462
|
+
throw new Error(`${label} must contain valid JSON`);
|
|
463
|
+
}
|
|
464
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
465
|
+
throw new Error(`${label} must contain a JSON object`);
|
|
466
|
+
}
|
|
467
|
+
const dictionary = value;
|
|
468
|
+
if (Object.keys(dictionary).length === 0) throw new Error(`${label} must not be empty`);
|
|
469
|
+
const invalidKey = Object.keys(dictionary).find((key) => !/^[A-Za-z][A-Za-z0-9]*(?:[._-][A-Za-z0-9]+)*$/.test(key));
|
|
470
|
+
if (invalidKey) throw new Error(`${label} contains an invalid translation key: ${invalidKey}`);
|
|
471
|
+
const invalidValue = Object.entries(dictionary).find(([, text2]) => typeof text2 !== "string");
|
|
472
|
+
if (invalidValue) throw new Error(`${label}.${invalidValue[0]} must be a string`);
|
|
473
|
+
return dictionary;
|
|
474
|
+
}
|
|
475
|
+
async function auditLocaleDictionaries(packageRoot, locales) {
|
|
476
|
+
const dictionaries = /* @__PURE__ */ new Map();
|
|
477
|
+
for (const [locale, relative] of Object.entries(locales)) {
|
|
478
|
+
const file = await resolvePackageFile(packageRoot, relative, `locales.${locale}`);
|
|
479
|
+
const info = await stat(file);
|
|
480
|
+
if (!info.isFile()) throw new Error(`locales.${locale} must point to a file`);
|
|
481
|
+
if (info.size > LOCALE_BUDGET) throw new Error(`locales.${locale} exceeds 256 KiB`);
|
|
482
|
+
dictionaries.set(locale, parseLocaleDictionary(await readFile(file, "utf8"), `locales.${locale}`));
|
|
483
|
+
}
|
|
484
|
+
const baseline = dictionaries.get("en-US");
|
|
485
|
+
for (const [locale, dictionary] of dictionaries) {
|
|
486
|
+
const result = validateDictionary(baseline, dictionary);
|
|
487
|
+
if (!result.ok) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`locales.${locale} must match en-US keys (missing: ${result.missing.join(", ") || "none"}; unknown: ${result.unknown.join(", ") || "none"}; invalid: ${result.invalid.join(", ") || "none"})`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return Object.keys(locales);
|
|
494
|
+
}
|
|
495
|
+
async function resolvePackageFile(packageRoot, relative, label) {
|
|
496
|
+
const candidate = resolve(packageRoot, relative);
|
|
497
|
+
if (candidate !== packageRoot && !candidate.startsWith(`${packageRoot}${sep}`)) {
|
|
498
|
+
throw new Error(`${label} escapes the brand package`);
|
|
499
|
+
}
|
|
500
|
+
const actual = await realpath(candidate);
|
|
501
|
+
if (actual !== packageRoot && !actual.startsWith(`${packageRoot}${sep}`)) {
|
|
502
|
+
throw new Error(`${label} resolves outside the brand package`);
|
|
503
|
+
}
|
|
504
|
+
return actual;
|
|
505
|
+
}
|
|
506
|
+
async function assertAsset(packageRoot, relative, kind) {
|
|
507
|
+
const file = await resolvePackageFile(packageRoot, relative, `assets.${kind}`);
|
|
508
|
+
const info = await stat(file);
|
|
509
|
+
if (!info.isFile()) throw new Error(`assets.${kind} must point to a file`);
|
|
510
|
+
if (info.size > ASSET_BUDGET) throw new Error(`assets.${kind} exceeds 5 MiB`);
|
|
511
|
+
const extension = extname(file).toLowerCase();
|
|
512
|
+
const allowed = kind === "logo" ? /* @__PURE__ */ new Set([".svg", ".png", ".webp"]) : /* @__PURE__ */ new Set([".png"]);
|
|
513
|
+
if (!allowed.has(extension)) throw new Error(`assets.${kind} has unsupported extension: ${extension}`);
|
|
514
|
+
if (kind === "appIcon" || kind === "tray") {
|
|
515
|
+
const bytes = await readFile(file);
|
|
516
|
+
if (bytes.length < 24 || bytes.toString("hex", 0, 8) !== "89504e470d0a1a0a") {
|
|
517
|
+
throw new Error(`assets.${kind} must be a valid PNG`);
|
|
518
|
+
}
|
|
519
|
+
const width = bytes.readUInt32BE(16);
|
|
520
|
+
const height = bytes.readUInt32BE(20);
|
|
521
|
+
if (kind === "appIcon") {
|
|
522
|
+
if (width !== height || width < 512) throw new Error("assets.appIcon must be square and at least 512x512");
|
|
523
|
+
} else if (width !== 32 || height !== 32) {
|
|
524
|
+
throw new Error("assets.tray must be a 32x32 PNG (single asset used on macOS, Windows, and Linux)");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return file;
|
|
528
|
+
}
|
|
529
|
+
function moduleKind(file, specifier) {
|
|
530
|
+
const extension = extname(file).toLowerCase();
|
|
531
|
+
if (CODE_EXTENSIONS.has(extension)) return "code";
|
|
532
|
+
if (extension === ".css") {
|
|
533
|
+
if (!file.endsWith(".module.css")) {
|
|
534
|
+
throw new Error(`entry styles must use CSS Modules (*.module.css): ${specifier}`);
|
|
535
|
+
}
|
|
536
|
+
return "css";
|
|
537
|
+
}
|
|
538
|
+
if (ASSET_EXTENSIONS.has(extension)) return "asset";
|
|
539
|
+
if (DATA_EXTENSIONS.has(extension)) return "data";
|
|
540
|
+
throw new Error(`entry imports an unsupported module type: ${specifier}`);
|
|
541
|
+
}
|
|
542
|
+
async function resolveRelativeModule(packageRoot, importer, specifier) {
|
|
543
|
+
if (specifier.includes("?") || specifier.includes("#")) {
|
|
544
|
+
throw new Error(`entry relative import must not contain a query or fragment: ${specifier}`);
|
|
545
|
+
}
|
|
546
|
+
const base = resolve(dirname(importer), specifier);
|
|
547
|
+
const candidates = extname(base) ? [base] : [base, ...[".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx"].map((extension) => `${base}${extension}`)];
|
|
548
|
+
for (const candidate of candidates) {
|
|
549
|
+
try {
|
|
550
|
+
const actual = await realpath(candidate);
|
|
551
|
+
if (actual !== packageRoot && !actual.startsWith(`${packageRoot}${sep}`)) {
|
|
552
|
+
throw new Error(`entry import resolves outside the brand package: ${specifier}`);
|
|
553
|
+
}
|
|
554
|
+
const info = await stat(actual);
|
|
555
|
+
if (!info.isFile()) throw new Error(`entry import must resolve to a file: ${specifier}`);
|
|
556
|
+
if (info.size > ASSET_BUDGET) throw new Error(`entry import exceeds 5 MiB: ${specifier}`);
|
|
557
|
+
return { file: actual, kind: moduleKind(actual, specifier) };
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof Error && !error.message.includes("ENOENT")) throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
throw new Error(`entry relative import cannot be resolved: ${specifier}`);
|
|
563
|
+
}
|
|
564
|
+
function assertScopedCss(source, file) {
|
|
565
|
+
const withoutComments = source.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
566
|
+
if (/:global\b/.test(withoutComments)) throw new Error(`CSS Module uses forbidden :global selector: ${file}`);
|
|
567
|
+
if (/(?:^|[,{]\s*)(?:html|body|:root|\*)\b/m.test(withoutComments)) {
|
|
568
|
+
throw new Error(`CSS Module contains a global root selector: ${file}`);
|
|
569
|
+
}
|
|
570
|
+
if (/(?:^|[\s:(,])(?:#(?:[0-9a-f]{3,8})|rgba?\(|hsla?\()/i.test(withoutComments)) {
|
|
571
|
+
throw new Error(`CSS Module colors must use DS2 --ow-* tokens: ${file}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function cssRelativeSpecifiers(source, file) {
|
|
575
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
576
|
+
for (const pattern of [CSS_IMPORT, CSS_URL]) {
|
|
577
|
+
pattern.lastIndex = 0;
|
|
578
|
+
for (const match of source.matchAll(pattern)) {
|
|
579
|
+
const specifier = match[1]?.trim();
|
|
580
|
+
if (!specifier) continue;
|
|
581
|
+
if (/^(?:data:|https?:|\/\/|\/)/i.test(specifier)) {
|
|
582
|
+
throw new Error(`CSS Module contains a non-package-relative URL: ${file}: ${specifier}`);
|
|
583
|
+
}
|
|
584
|
+
specifiers.add(specifier);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return [...specifiers];
|
|
588
|
+
}
|
|
589
|
+
function esbuildLoader(file) {
|
|
590
|
+
switch (extname(file).toLowerCase()) {
|
|
591
|
+
case ".ts":
|
|
592
|
+
return "ts";
|
|
593
|
+
case ".tsx":
|
|
594
|
+
return "tsx";
|
|
595
|
+
case ".jsx":
|
|
596
|
+
return "jsx";
|
|
597
|
+
default:
|
|
598
|
+
return "js";
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function assertDeclaredNetworkOrigins(source, file, allowedOrigins) {
|
|
602
|
+
STATIC_HTTP_URL.lastIndex = 0;
|
|
603
|
+
for (const match of source.matchAll(STATIC_HTTP_URL)) {
|
|
604
|
+
let origin;
|
|
605
|
+
try {
|
|
606
|
+
origin = new URL(match[0]).origin;
|
|
607
|
+
} catch {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (!allowedOrigins.has(origin)) {
|
|
611
|
+
throw new Error(`entry module references undeclared network origin ${origin}: ${file}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function auditEntryModuleGraph(packageRoot, entryFile, allowedOrigins) {
|
|
616
|
+
await init;
|
|
617
|
+
const pending = [{ file: entryFile, kind: "code" }];
|
|
618
|
+
const visited = /* @__PURE__ */ new Set();
|
|
619
|
+
const sources = [];
|
|
620
|
+
while (pending.length) {
|
|
621
|
+
const { file, kind } = pending.pop();
|
|
622
|
+
if (visited.has(file)) continue;
|
|
623
|
+
visited.add(file);
|
|
624
|
+
const bytes = await readFile(file);
|
|
625
|
+
sources.push(Buffer.from(`// ${file.slice(packageRoot.length)}
|
|
626
|
+
`), bytes);
|
|
627
|
+
if (kind === "asset" || kind === "data") continue;
|
|
628
|
+
const source = bytes.toString("utf8");
|
|
629
|
+
if (kind === "css") {
|
|
630
|
+
assertScopedCss(source, file);
|
|
631
|
+
for (const specifier of cssRelativeSpecifiers(source, file)) {
|
|
632
|
+
pending.push(await resolveRelativeModule(packageRoot, file, specifier));
|
|
633
|
+
}
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (containsLikelyHardCodedCredential(source)) {
|
|
637
|
+
throw new Error(`entry module contains a likely hard-coded credential: ${file}`);
|
|
638
|
+
}
|
|
639
|
+
assertDeclaredNetworkOrigins(source, file, allowedOrigins);
|
|
640
|
+
if (/\brequire\s*\(/.test(source))
|
|
641
|
+
throw new Error(`entry module uses require(), which is not browser-safe: ${file}`);
|
|
642
|
+
let imports;
|
|
643
|
+
try {
|
|
644
|
+
const transformed = await transform(source, {
|
|
645
|
+
loader: esbuildLoader(file),
|
|
646
|
+
format: "esm",
|
|
647
|
+
jsx: "automatic",
|
|
648
|
+
sourcemap: false,
|
|
649
|
+
sourcefile: file
|
|
650
|
+
});
|
|
651
|
+
[imports] = parse(transformed.code);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`entry module cannot be parsed: ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
for (const imported of imports) {
|
|
658
|
+
const specifier = imported.n;
|
|
659
|
+
if (!specifier) throw new Error(`entry module uses a non-literal dynamic import: ${file}`);
|
|
660
|
+
if (imported.d >= 0) throw new Error(`entry module uses dynamic import(), which is not supported: ${file}`);
|
|
661
|
+
if (specifier.startsWith("node:")) throw new Error(`entry imports Node.js module: ${specifier}`);
|
|
662
|
+
if (specifier.startsWith(".")) {
|
|
663
|
+
pending.push(await resolveRelativeModule(packageRoot, file, specifier));
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (!IMPORT_ALLOWLIST.has(specifier)) throw new Error(`entry imports non-public dependency: ${specifier}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return { gzipBytes: gzipSync(Buffer.concat(sources)).byteLength, files: [...visited] };
|
|
670
|
+
}
|
|
671
|
+
async function auditBrandPackage(manifestFile2, options = {}) {
|
|
672
|
+
const absoluteManifest = await realpath(resolve(manifestFile2));
|
|
673
|
+
const packageRoot = await realpath(resolve(absoluteManifest, ".."));
|
|
674
|
+
const raw = JSON.parse(await readFile(absoluteManifest, "utf8"));
|
|
675
|
+
const parsed = parseBrandManifest(raw, { platform: options.platform === true });
|
|
676
|
+
if (!parsed.ok) throw new Error(`manifest contract failed: ${parsed.error}`);
|
|
677
|
+
const manifest = parsed.manifest;
|
|
678
|
+
const checkedAssets = [];
|
|
679
|
+
for (const [kind, relative] of Object.entries(manifest.assets)) {
|
|
680
|
+
await assertAsset(packageRoot, relative, kind);
|
|
681
|
+
checkedAssets.push(relative);
|
|
682
|
+
}
|
|
683
|
+
let entryGzipBytes = 0;
|
|
684
|
+
let checkedModules = [];
|
|
685
|
+
const checkedLocales = manifest.locales ? await auditLocaleDictionaries(packageRoot, manifest.locales) : [];
|
|
686
|
+
if (manifest.entry) {
|
|
687
|
+
const entryFile = await resolvePackageFile(packageRoot, manifest.entry, "entry");
|
|
688
|
+
const graph = await auditEntryModuleGraph(
|
|
689
|
+
packageRoot,
|
|
690
|
+
entryFile,
|
|
691
|
+
/* @__PURE__ */ new Set([...manifest.network?.allowedOrigins ?? [], ...options.developmentAllowedOrigins ?? []])
|
|
692
|
+
);
|
|
693
|
+
entryGzipBytes = graph.gzipBytes;
|
|
694
|
+
checkedModules = graph.files.map((file) => `.${file.slice(packageRoot.length)}`);
|
|
695
|
+
if (entryGzipBytes > ENTRY_GZIP_BUDGET) {
|
|
696
|
+
throw new Error(`entry gzip size ${entryGzipBytes} exceeds ${ENTRY_GZIP_BUDGET} byte budget`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return { manifest, entryGzipBytes, checkedAssets, checkedModules, checkedLocales };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/cli.ts
|
|
703
|
+
var manifestFile = process.argv[2];
|
|
704
|
+
if (manifestFile === "--help" || manifestFile === "-h") {
|
|
705
|
+
console.log("Usage: orbit-audit-brand <path/to/orbit-brand.json>");
|
|
706
|
+
} else if (!manifestFile) {
|
|
707
|
+
console.error("Usage: orbit-audit-brand <path/to/orbit-brand.json>");
|
|
708
|
+
process.exitCode = 2;
|
|
709
|
+
} else {
|
|
710
|
+
try {
|
|
711
|
+
const report = await auditBrandPackage(manifestFile);
|
|
712
|
+
console.log(
|
|
713
|
+
`\u2713 ${report.manifest.id}: ${report.checkedAssets.length} assets, ${report.checkedLocales.length} locale packs, ${report.checkedModules.length} entry modules, ${report.entryGzipBytes} module-graph gzip bytes`
|
|
714
|
+
);
|
|
715
|
+
} catch (cause) {
|
|
716
|
+
console.error(`\u2717 brand audit failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
717
|
+
process.exitCode = 1;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=audit-brand.js.map
|
|
721
|
+
//# sourceMappingURL=audit-brand.js.map
|