archive-labs 1.0.8 → 1.0.9
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/dist/cli.js +91 -19
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -1074,12 +1074,65 @@ const resolveEnvironmentAuthSession = () => {
|
|
|
1074
1074
|
};
|
|
1075
1075
|
};
|
|
1076
1076
|
const authSecretStoreService = "archive-labs-cli";
|
|
1077
|
+
const authSecretChunkManifestFormat = "archive-cli-auth-secret-chunks-v1";
|
|
1078
|
+
const defaultAuthSecretChunkSize = 1000;
|
|
1079
|
+
const maxAuthSecretChunks = 64;
|
|
1077
1080
|
const resolveTestSecretStoreDir = () => process.env.ARCHIVE_CLI_TEST_SECRET_STORE_DIR?.trim() || null;
|
|
1078
1081
|
const isTestSecretStoreUnavailable = () => /^(1|true|yes)$/i.test(String(process.env.ARCHIVE_CLI_TEST_SECRET_STORE_UNAVAILABLE ?? ""));
|
|
1082
|
+
const resolveAuthSecretChunkSize = () => {
|
|
1083
|
+
const configured = Number.parseInt(process.env.ARCHIVE_CLI_AUTH_SECRET_CHUNK_SIZE ?? "", 10);
|
|
1084
|
+
return Number.isInteger(configured) && configured >= 256 && configured <= 1800
|
|
1085
|
+
? configured
|
|
1086
|
+
: defaultAuthSecretChunkSize;
|
|
1087
|
+
};
|
|
1079
1088
|
const authSecretFilePath = (accountId) => {
|
|
1080
1089
|
const encoded = Buffer.from(accountId).toString("base64url");
|
|
1081
1090
|
return path.join(resolveTestSecretStoreDir() ?? "", `${encoded}.json`);
|
|
1082
1091
|
};
|
|
1092
|
+
const authSecretChunkAccountId = (accountId, index) => `${accountId}__chunk_${index}`;
|
|
1093
|
+
const parseAuthSecretChunkManifest = (raw) => {
|
|
1094
|
+
try {
|
|
1095
|
+
const parsed = JSON.parse(raw);
|
|
1096
|
+
if (parsed?.format !== authSecretChunkManifestFormat ||
|
|
1097
|
+
typeof parsed.chunkCount !== "number" ||
|
|
1098
|
+
!Number.isInteger(parsed.chunkCount) ||
|
|
1099
|
+
parsed.chunkCount <= 0 ||
|
|
1100
|
+
parsed.chunkCount > maxAuthSecretChunks) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
return { chunkCount: parsed.chunkCount };
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
const readAuthSecretEntry = async (accountId) => {
|
|
1110
|
+
const testDir = resolveTestSecretStoreDir();
|
|
1111
|
+
return testDir
|
|
1112
|
+
? await readFile(authSecretFilePath(accountId), "utf8")
|
|
1113
|
+
: new Entry(authSecretStoreService, accountId).getPassword();
|
|
1114
|
+
};
|
|
1115
|
+
const writeAuthSecretEntry = async (accountId, value) => {
|
|
1116
|
+
const testDir = resolveTestSecretStoreDir();
|
|
1117
|
+
if (testDir) {
|
|
1118
|
+
await mkdir(testDir, { mode: 0o700, recursive: true });
|
|
1119
|
+
await writeFile(authSecretFilePath(accountId), value, "utf8");
|
|
1120
|
+
if (process.platform !== "win32") {
|
|
1121
|
+
await Promise.allSettled([chmod(testDir, 0o700), chmod(authSecretFilePath(accountId), 0o600)]);
|
|
1122
|
+
}
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
new Entry(authSecretStoreService, accountId).setPassword(value);
|
|
1126
|
+
};
|
|
1127
|
+
const deleteAuthSecretEntry = async (accountId) => {
|
|
1128
|
+
const testDir = resolveTestSecretStoreDir();
|
|
1129
|
+
if (testDir) {
|
|
1130
|
+
const { rm } = await import("node:fs/promises");
|
|
1131
|
+
await rm(authSecretFilePath(accountId), { force: true });
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
new Entry(authSecretStoreService, accountId).deletePassword();
|
|
1135
|
+
};
|
|
1083
1136
|
const createCredentialStoreUnavailableError = (operation, error) => createCliError({
|
|
1084
1137
|
category: "setup",
|
|
1085
1138
|
code: "credential_store_unavailable",
|
|
@@ -1109,15 +1162,16 @@ const readAuthSecrets = async (accountId) => {
|
|
|
1109
1162
|
if (isTestSecretStoreUnavailable()) {
|
|
1110
1163
|
throw createCredentialStoreUnavailableError("read");
|
|
1111
1164
|
}
|
|
1112
|
-
const testDir = resolveTestSecretStoreDir();
|
|
1113
1165
|
try {
|
|
1114
|
-
const raw =
|
|
1115
|
-
? await readFile(authSecretFilePath(accountId), "utf8")
|
|
1116
|
-
: new Entry(authSecretStoreService, accountId).getPassword();
|
|
1166
|
+
const raw = await readAuthSecretEntry(accountId);
|
|
1117
1167
|
if (!raw) {
|
|
1118
1168
|
return null;
|
|
1119
1169
|
}
|
|
1120
|
-
|
|
1170
|
+
const manifest = parseAuthSecretChunkManifest(raw);
|
|
1171
|
+
const secretPayload = manifest
|
|
1172
|
+
? await Promise.all(Array.from({ length: manifest.chunkCount }, (_, index) => readAuthSecretEntry(authSecretChunkAccountId(accountId, index)))).then((chunks) => chunks.join(""))
|
|
1173
|
+
: raw;
|
|
1174
|
+
return validateAuthSecretPayload(JSON.parse(secretPayload));
|
|
1121
1175
|
}
|
|
1122
1176
|
catch (error) {
|
|
1123
1177
|
if (error?.code === "ENOENT") {
|
|
@@ -1132,16 +1186,28 @@ const writeAuthSecrets = async (accountId, secrets) => {
|
|
|
1132
1186
|
}
|
|
1133
1187
|
try {
|
|
1134
1188
|
const raw = JSON.stringify(secrets);
|
|
1135
|
-
const
|
|
1136
|
-
if (
|
|
1137
|
-
await
|
|
1138
|
-
await
|
|
1139
|
-
if (process.platform !== "win32") {
|
|
1140
|
-
await Promise.allSettled([chmod(testDir, 0o700), chmod(authSecretFilePath(accountId), 0o600)]);
|
|
1141
|
-
}
|
|
1189
|
+
const chunkSize = resolveAuthSecretChunkSize();
|
|
1190
|
+
if (raw.length <= chunkSize) {
|
|
1191
|
+
await writeAuthSecretEntry(accountId, raw);
|
|
1192
|
+
await Promise.allSettled(Array.from({ length: maxAuthSecretChunks }, (_, index) => deleteAuthSecretEntry(authSecretChunkAccountId(accountId, index))));
|
|
1142
1193
|
return;
|
|
1143
1194
|
}
|
|
1144
|
-
new
|
|
1195
|
+
const chunks = raw.match(new RegExp(`.{1,${chunkSize}}`, "g")) ?? [];
|
|
1196
|
+
if (chunks.length > maxAuthSecretChunks) {
|
|
1197
|
+
throw new Error("Archive login token payload is too large for this credential store.");
|
|
1198
|
+
}
|
|
1199
|
+
try {
|
|
1200
|
+
await Promise.all(chunks.map((chunk, index) => writeAuthSecretEntry(authSecretChunkAccountId(accountId, index), chunk)));
|
|
1201
|
+
await writeAuthSecretEntry(accountId, JSON.stringify({
|
|
1202
|
+
chunkCount: chunks.length,
|
|
1203
|
+
format: authSecretChunkManifestFormat,
|
|
1204
|
+
}));
|
|
1205
|
+
await Promise.allSettled(Array.from({ length: maxAuthSecretChunks - chunks.length }, (_, offset) => deleteAuthSecretEntry(authSecretChunkAccountId(accountId, chunks.length + offset))));
|
|
1206
|
+
}
|
|
1207
|
+
catch (error) {
|
|
1208
|
+
await Promise.allSettled(chunks.map((_, index) => deleteAuthSecretEntry(authSecretChunkAccountId(accountId, index))));
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1145
1211
|
}
|
|
1146
1212
|
catch (error) {
|
|
1147
1213
|
throw createCredentialStoreUnavailableError("store", error);
|
|
@@ -1155,13 +1221,19 @@ const deleteAuthSecrets = async (accountId) => {
|
|
|
1155
1221
|
throw createCredentialStoreUnavailableError("delete");
|
|
1156
1222
|
}
|
|
1157
1223
|
try {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1224
|
+
let chunkCount = maxAuthSecretChunks;
|
|
1225
|
+
try {
|
|
1226
|
+
const raw = await readAuthSecretEntry(accountId);
|
|
1227
|
+
const manifest = raw ? parseAuthSecretChunkManifest(raw) : null;
|
|
1228
|
+
if (manifest) {
|
|
1229
|
+
chunkCount = manifest.chunkCount;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
// Best-effort cleanup below also covers missing or unreadable manifests.
|
|
1163
1234
|
}
|
|
1164
|
-
|
|
1235
|
+
await Promise.allSettled(Array.from({ length: chunkCount }, (_, index) => deleteAuthSecretEntry(authSecretChunkAccountId(accountId, index))));
|
|
1236
|
+
await deleteAuthSecretEntry(accountId);
|
|
1165
1237
|
}
|
|
1166
1238
|
catch (error) {
|
|
1167
1239
|
if (error?.code !== "ENOENT") {
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archive-labs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Terminal CLI for Archive that manages login, repository status, sync, checks, impact analysis, and release risk.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"preferGlobal": true,
|
|
7
7
|
"packageManager": "pnpm@10.33.0",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"bin": {
|
|
10
|
-
"archive": "
|
|
11
|
-
"archive-labs": "
|
|
12
|
-
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"archive": "dist/cli.js",
|
|
11
|
+
"archive-labs": "dist/cli.js"
|
|
12
|
+
},
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
15
|
"logo.png",
|