qualty 0.1.30 → 0.1.31

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.
@@ -0,0 +1,348 @@
1
+ import { gunzipSync, gzipSync } from "node:zlib";
2
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export const AUTH_BUNDLE_VERSION = 2;
7
+ export const GITHUB_SECRET_MAX_BYTES = 64 * 1024;
8
+ export const GITHUB_SECRET_SAFE_BYTES = GITHUB_SECRET_MAX_BYTES - 1024;
9
+ export const AUTH_SECRET_PRIMARY = "QUALTY_AUTH_STATE_B64";
10
+ export const AUTH_SHARDS_VARIABLE = "QUALTY_AUTH_SHARDS";
11
+ export const AUTH_PROFILES_VARIABLE = "QUALTY_AUTH_PROFILES";
12
+ export const MAX_AUTH_SHARDS = 10;
13
+
14
+ const QUALTY_SHARED_CREDS_DIR = join(homedir(), ".qualty");
15
+
16
+ export function safePathSegment(value, fallback = "default") {
17
+ const out = String(value || "")
18
+ .trim()
19
+ .replace(/[^a-zA-Z0-9._-]+/g, "_")
20
+ .replace(/^_+|_+$/g, "");
21
+ return out || fallback;
22
+ }
23
+
24
+ export function localAuthStatePath({ orgId, projectId, profile }) {
25
+ const orgSeg = safePathSegment(orgId || "default-org", "default-org");
26
+ const projectSeg = safePathSegment(projectId || "default-project", "default-project");
27
+ const profileSeg = safePathSegment(profile || "default", "default");
28
+ return join(QUALTY_SHARED_CREDS_DIR, "local-contexts", orgSeg, projectSeg, `${profileSeg}.json`);
29
+ }
30
+
31
+ export function authShardSecretName(shardIndex) {
32
+ const index = Number(shardIndex);
33
+ if (!Number.isInteger(index) || index < 1) {
34
+ throw new Error(`Invalid auth shard index: ${shardIndex}`);
35
+ }
36
+ return index === 1 ? AUTH_SECRET_PRIMARY : `${AUTH_SECRET_PRIMARY}_${index}`;
37
+ }
38
+
39
+ export function loadStorageStateFromPath(statePath) {
40
+ const raw = readFileSync(statePath, "utf8");
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(raw);
44
+ } catch (err) {
45
+ throw new Error(`Invalid auth JSON at ${statePath}: ${err.message}`);
46
+ }
47
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
48
+ throw new Error(`Invalid auth JSON at ${statePath}: expected an object`);
49
+ }
50
+ return parsed;
51
+ }
52
+
53
+ export function buildAuthBundleManifest({ orgId, projectId, profileEntries }) {
54
+ const profiles = {};
55
+ for (const entry of profileEntries) {
56
+ const name = String(entry.name || "").trim();
57
+ if (!name) continue;
58
+ const localProfileName =
59
+ String(entry.localProfileName || entry.local_profile_name || name).trim() || name;
60
+ const state =
61
+ entry.state && typeof entry.state === "object"
62
+ ? entry.state
63
+ : loadStorageStateFromPath(entry.statePath);
64
+ profiles[name] = {
65
+ local_profile_name: localProfileName,
66
+ state,
67
+ };
68
+ }
69
+ if (Object.keys(profiles).length === 0) {
70
+ throw new Error("No auth profiles to export.");
71
+ }
72
+ return {
73
+ version: AUTH_BUNDLE_VERSION,
74
+ org_id: String(orgId || "").trim(),
75
+ project_id: String(projectId || "").trim(),
76
+ profiles,
77
+ };
78
+ }
79
+
80
+ export function encodeAuthBundleManifest(manifest) {
81
+ const raw = Buffer.from(JSON.stringify(manifest), "utf8");
82
+ return {
83
+ encoding: "gzip+base64",
84
+ payload: gzipSync(raw).toString("base64"),
85
+ rawBytes: raw.length,
86
+ };
87
+ }
88
+
89
+ function isPlaywrightStorageState(value) {
90
+ return (
91
+ value &&
92
+ typeof value === "object" &&
93
+ !Array.isArray(value) &&
94
+ (Array.isArray(value.cookies) || Array.isArray(value.origins))
95
+ );
96
+ }
97
+
98
+ export function decodeAuthPayload(payloadB64) {
99
+ const trimmed = String(payloadB64 || "").trim();
100
+ if (!trimmed) {
101
+ throw new Error("Empty auth payload.");
102
+ }
103
+ let decoded;
104
+ try {
105
+ decoded = Buffer.from(trimmed, "base64");
106
+ } catch (err) {
107
+ throw new Error(`Invalid base64 auth payload: ${err.message}`);
108
+ }
109
+
110
+ let jsonText;
111
+ try {
112
+ jsonText = gunzipSync(decoded).toString("utf8");
113
+ } catch {
114
+ jsonText = decoded.toString("utf8");
115
+ }
116
+
117
+ let parsed;
118
+ try {
119
+ parsed = JSON.parse(jsonText);
120
+ } catch (err) {
121
+ throw new Error(`Auth payload is not valid JSON: ${err.message}`);
122
+ }
123
+
124
+ if (parsed?.version === AUTH_BUNDLE_VERSION && parsed?.profiles && typeof parsed.profiles === "object") {
125
+ return { format: "bundle", manifest: parsed };
126
+ }
127
+ if (isPlaywrightStorageState(parsed)) {
128
+ return { format: "legacy-single", state: parsed };
129
+ }
130
+ throw new Error("Unrecognized auth payload format.");
131
+ }
132
+
133
+ function manifestSubset(manifest, profileNames) {
134
+ const profiles = {};
135
+ for (const name of profileNames) {
136
+ if (manifest.profiles?.[name]) {
137
+ profiles[name] = manifest.profiles[name];
138
+ }
139
+ }
140
+ return {
141
+ version: AUTH_BUNDLE_VERSION,
142
+ org_id: manifest.org_id,
143
+ project_id: manifest.project_id,
144
+ profiles,
145
+ };
146
+ }
147
+
148
+ export function shardAuthBundleManifest(manifest) {
149
+ const profileNames = Object.keys(manifest.profiles || {});
150
+ if (profileNames.length === 0) {
151
+ throw new Error("Auth bundle has no profiles.");
152
+ }
153
+
154
+ const singles = profileNames.map((name) => {
155
+ const encoded = encodeAuthBundleManifest(manifestSubset(manifest, [name]));
156
+ return { name, encoded, bytes: Buffer.byteLength(encoded.payload, "utf8") };
157
+ });
158
+
159
+ const allEncoded = encodeAuthBundleManifest(manifest);
160
+ if (Buffer.byteLength(allEncoded.payload, "utf8") <= GITHUB_SECRET_SAFE_BYTES) {
161
+ return [
162
+ {
163
+ secretName: AUTH_SECRET_PRIMARY,
164
+ payload: allEncoded.payload,
165
+ encoding: allEncoded.encoding,
166
+ rawBytes: allEncoded.rawBytes,
167
+ profileNames,
168
+ },
169
+ ];
170
+ }
171
+
172
+ const shards = [];
173
+ let currentNames = [];
174
+ let currentBytes = 0;
175
+
176
+ const flush = () => {
177
+ if (currentNames.length === 0) return;
178
+ const encoded = encodeAuthBundleManifest(manifestSubset(manifest, currentNames));
179
+ const bytes = Buffer.byteLength(encoded.payload, "utf8");
180
+ if (bytes > GITHUB_SECRET_SAFE_BYTES) {
181
+ throw new Error(
182
+ `Profile "${currentNames.join(", ")}" is too large for a GitHub secret on its own ` +
183
+ `(${bytes} bytes; limit ~${GITHUB_SECRET_SAFE_BYTES}). Try a shorter-lived session.`
184
+ );
185
+ }
186
+ shards.push({
187
+ secretName: authShardSecretName(shards.length + 1),
188
+ payload: encoded.payload,
189
+ encoding: encoded.encoding,
190
+ rawBytes: encoded.rawBytes,
191
+ profileNames: [...currentNames],
192
+ });
193
+ currentNames = [];
194
+ currentBytes = 0;
195
+ };
196
+
197
+ for (const row of singles) {
198
+ if (row.bytes > GITHUB_SECRET_SAFE_BYTES) {
199
+ throw new Error(
200
+ `Profile "${row.name}" is too large for a GitHub secret (${row.bytes} bytes; limit ~${GITHUB_SECRET_SAFE_BYTES}).`
201
+ );
202
+ }
203
+ const nextNames = [...currentNames, row.name];
204
+ const nextEncoded = encodeAuthBundleManifest(manifestSubset(manifest, nextNames));
205
+ const nextBytes = Buffer.byteLength(nextEncoded.payload, "utf8");
206
+ if (nextBytes > GITHUB_SECRET_SAFE_BYTES && currentNames.length > 0) {
207
+ flush();
208
+ }
209
+ currentNames.push(row.name);
210
+ currentBytes = Buffer.byteLength(
211
+ encodeAuthBundleManifest(manifestSubset(manifest, currentNames)).payload,
212
+ "utf8"
213
+ );
214
+ }
215
+ flush();
216
+
217
+ if (shards.length > MAX_AUTH_SHARDS) {
218
+ throw new Error(
219
+ `Auth bundle requires ${shards.length} GitHub secrets (max ${MAX_AUTH_SHARDS}). ` +
220
+ "Export fewer profiles with --profiles or --suite-id / --ids."
221
+ );
222
+ }
223
+ return shards;
224
+ }
225
+
226
+ export function collectAuthShardPayloadsFromEnv(env = process.env) {
227
+ const payloads = [];
228
+ for (let index = 1; index <= MAX_AUTH_SHARDS; index += 1) {
229
+ const secretName = authShardSecretName(index);
230
+ const value = String(env[secretName] || "").trim();
231
+ if (!value) break;
232
+ payloads.push({ secretName, payload: value });
233
+ }
234
+ return payloads;
235
+ }
236
+
237
+ export function mergeAuthManifests(manifests) {
238
+ const mergedProfiles = {};
239
+ let orgId = "";
240
+ let projectId = "";
241
+ for (const manifest of manifests) {
242
+ orgId = orgId || String(manifest.org_id || "").trim();
243
+ projectId = projectId || String(manifest.project_id || "").trim();
244
+ for (const [name, entry] of Object.entries(manifest.profiles || {})) {
245
+ mergedProfiles[name] = entry;
246
+ }
247
+ }
248
+ return {
249
+ version: AUTH_BUNDLE_VERSION,
250
+ org_id: orgId,
251
+ project_id: projectId,
252
+ profiles: mergedProfiles,
253
+ };
254
+ }
255
+
256
+ export function restoreAuthBundleToDisk({
257
+ orgId,
258
+ projectId,
259
+ manifest,
260
+ fallbackProfile = "",
261
+ }) {
262
+ const restored = [];
263
+ for (const [name, entry] of Object.entries(manifest.profiles || {})) {
264
+ const localProfileName =
265
+ String(entry?.local_profile_name || name).trim() || name;
266
+ const state = entry?.state;
267
+ if (!isPlaywrightStorageState(state)) {
268
+ throw new Error(`Profile "${name}" has invalid storage state in auth bundle.`);
269
+ }
270
+ const authFile = localAuthStatePath({
271
+ orgId,
272
+ projectId,
273
+ profile: localProfileName,
274
+ });
275
+ mkdirSync(join(authFile, ".."), { recursive: true });
276
+ writeFileSync(authFile, `${JSON.stringify(state)}\n`, "utf8");
277
+ try {
278
+ chmodSync(authFile, 0o600);
279
+ } catch {
280
+ // ignore
281
+ }
282
+ restored.push({ name, localProfileName, path: authFile });
283
+ }
284
+ return restored;
285
+ }
286
+
287
+ export function restoreLegacySingleAuthToDisk({ orgId, projectId, state, fallbackProfile }) {
288
+ const profile = String(fallbackProfile || "").trim();
289
+ if (!profile) {
290
+ throw new Error(
291
+ "Legacy single-profile auth payload requires QUALTY_AUTH_PROFILE (or --profile)."
292
+ );
293
+ }
294
+ const authFile = localAuthStatePath({ orgId, projectId, profile });
295
+ mkdirSync(join(authFile, ".."), { recursive: true });
296
+ writeFileSync(authFile, `${JSON.stringify(state)}\n`, "utf8");
297
+ try {
298
+ chmodSync(authFile, 0o600);
299
+ } catch {
300
+ // ignore
301
+ }
302
+ return [{ name: profile, localProfileName: profile, path: authFile }];
303
+ }
304
+
305
+ export function restoreAuthPayloadsToDisk({
306
+ payloads,
307
+ orgId,
308
+ projectId,
309
+ fallbackProfile = "",
310
+ }) {
311
+ const manifests = [];
312
+ let legacySingle = null;
313
+
314
+ for (const row of payloads) {
315
+ const decoded = decodeAuthPayload(row.payload);
316
+ if (decoded.format === "bundle") {
317
+ manifests.push(decoded.manifest);
318
+ continue;
319
+ }
320
+ if (legacySingle) {
321
+ throw new Error("Multiple legacy single-profile auth payloads are not supported.");
322
+ }
323
+ legacySingle = decoded.state;
324
+ }
325
+
326
+ if (manifests.length > 0) {
327
+ const merged = mergeAuthManifests(manifests);
328
+ const effectiveOrgId = String(orgId || merged.org_id || "").trim();
329
+ const effectiveProjectId = String(projectId || merged.project_id || "").trim();
330
+ return restoreAuthBundleToDisk({
331
+ orgId: effectiveOrgId,
332
+ projectId: effectiveProjectId,
333
+ manifest: merged,
334
+ fallbackProfile,
335
+ });
336
+ }
337
+
338
+ if (legacySingle) {
339
+ return restoreLegacySingleAuthToDisk({
340
+ orgId,
341
+ projectId,
342
+ state: legacySingle,
343
+ fallbackProfile,
344
+ });
345
+ }
346
+
347
+ throw new Error("No auth payloads to restore.");
348
+ }
@@ -774,8 +774,9 @@ async function runLocalSingleCombination({
774
774
  explicitAuthProfile: String(authProfile || "").trim(),
775
775
  noAuthState: Boolean(noAuthState),
776
776
  });
777
+ const explicitAuthProfile = String(authProfile || "").trim();
777
778
  const claimProfile = localProfileLabel(claimSavedLogin);
778
- const effectiveProfile = String(authProfile || "").trim() || setupFromClaim?.profile || claimProfile;
779
+ const effectiveProfile = explicitAuthProfile || setupFromClaim?.profile || claimProfile;
779
780
  const resolvedStoragePath =
780
781
  !noAuthState && effectiveProfile
781
782
  ? localAuthStatePath({
@@ -787,17 +788,20 @@ async function runLocalSingleCombination({
787
788
  const finalStoragePath =
788
789
  !noAuthState && setupFromClaim?.path && existsSync(setupFromClaim.path)
789
790
  ? setupFromClaim.path
790
- : !noAuthState && storageStatePath && existsSync(storageStatePath)
791
- ? storageStatePath
792
- : !noAuthState && resolvedStoragePath && existsSync(resolvedStoragePath)
793
- ? resolvedStoragePath
794
- : null;
791
+ : !noAuthState &&
792
+ explicitAuthProfile &&
793
+ storageStatePath &&
794
+ existsSync(storageStatePath)
795
+ ? storageStatePath
796
+ : !noAuthState && resolvedStoragePath && existsSync(resolvedStoragePath)
797
+ ? resolvedStoragePath
798
+ : null;
795
799
 
796
800
  let authSource = "none";
797
801
  if (!noAuthState && finalStoragePath) {
798
802
  if (setupFromClaim?.path && finalStoragePath === setupFromClaim.path) {
799
803
  authSource = "interactive setup during run";
800
- } else if (storageStatePath && finalStoragePath === storageStatePath) {
804
+ } else if (explicitAuthProfile && storageStatePath && finalStoragePath === storageStatePath) {
801
805
  authSource = "--auth-profile flag";
802
806
  } else if (resolvedStoragePath && finalStoragePath === resolvedStoragePath) {
803
807
  authSource = claimProfile ? "dashboard saved login profile" : "resolved profile path";