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.
Files changed (2) hide show
  1. package/dist/cli.js +91 -19
  2. 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 = testDir
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
- return validateAuthSecretPayload(JSON.parse(raw));
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 testDir = resolveTestSecretStoreDir();
1136
- if (testDir) {
1137
- await mkdir(testDir, { mode: 0o700, recursive: true });
1138
- await writeFile(authSecretFilePath(accountId), raw, "utf8");
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 Entry(authSecretStoreService, accountId).setPassword(raw);
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
- const testDir = resolveTestSecretStoreDir();
1159
- if (testDir) {
1160
- const { rm } = await import("node:fs/promises");
1161
- await rm(authSecretFilePath(accountId), { force: true });
1162
- return;
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
- new Entry(authSecretStoreService, accountId).deletePassword();
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.8",
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": "./dist/cli.js",
11
- "archive-labs": "./dist/cli.js"
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",