atris 3.78.0 → 3.80.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/README.md +7 -0
- package/atris/atris.md +66 -167
- package/atris/skills/security-audit/SKILL.md +77 -0
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris.md +66 -167
- package/bin/atris.js +6 -0
- package/commands/brief.js +8 -2
- package/commands/business.js +44 -1984
- package/commands/gm.js +3 -0
- package/commands/member.js +189 -966
- package/commands/mission.js +229 -1409
- package/commands/pack.js +112 -1476
- package/commands/rating.js +2 -0
- package/commands/security-review.js +125 -0
- package/commands/task.js +137 -1359
- package/lib/business-core.js +2022 -0
- package/lib/engine-ask.js +1 -0
- package/lib/fleet.js +10 -0
- package/lib/handoff-wake.js +8 -6
- package/lib/held-autosweep.js +559 -0
- package/lib/held-pull-requests.js +42 -15
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +21 -2
- package/lib/member-board.js +12 -10
- package/lib/member-core.js +1014 -0
- package/lib/member-up.js +46 -3
- package/lib/mission-core.js +1559 -0
- package/lib/pack-core.js +1588 -0
- package/lib/security-audit-classes.js +300 -0
- package/lib/security-audit.js +1519 -0
- package/lib/task-core.js +1412 -0
- package/lib/task-db.js +70 -27
- package/lib/task-decision.js +66 -0
- package/lib/task-explanation.js +20 -0
- package/lib/yes-lock.js +1 -1
- package/package.json +1 -1
package/commands/pack.js
CHANGED
|
@@ -4,12 +4,11 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { isUtf8 } = require('buffer');
|
|
7
|
-
const { createHash } = require('crypto');
|
|
8
7
|
const { spawnSync } = require('child_process');
|
|
9
8
|
const { version: CLI_VERSION } = require('../package.json');
|
|
10
|
-
const {
|
|
11
|
-
const { loadCredentials
|
|
12
|
-
const { createZipBuffer, readZipBuffer
|
|
9
|
+
const { getApiBaseUrl, getAppBaseUrl, httpRequest } = require('../utils/api');
|
|
10
|
+
const { loadCredentials } = require('../utils/auth');
|
|
11
|
+
const { createZipBuffer, readZipBuffer } = require('../lib/zip');
|
|
13
12
|
const { craftPack } = require('./pack-craft');
|
|
14
13
|
const { gatherAtrisContext } = require('./console');
|
|
15
14
|
const {
|
|
@@ -29,134 +28,115 @@ const {
|
|
|
29
28
|
claimPackRecoveryParent,
|
|
30
29
|
confirmPackRecoveryClaim,
|
|
31
30
|
} = require('../lib/pack-capabilities');
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const SECRET_PATTERNS = [
|
|
142
|
-
{ label: 'jwt', regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g },
|
|
143
|
-
{ label: 'api key', regex: /\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{16,}|gho_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16})/g },
|
|
144
|
-
{ label: 'private key', regex: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g },
|
|
145
|
-
{ label: 'database url with password', regex: /\b(?:postgres(?:ql)?|mysql):\/\/[^\s:/@]+:[^\s/@]{3,}@\S+/g },
|
|
146
|
-
{ label: 'inline credential', regex: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*(\S{12,})/gi },
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
// Docs are full of examples. A value that is obviously a stand-in is not a leak.
|
|
150
|
-
const SECRET_PLACEHOLDER = /^(?:<|\{|\$|\.\.\.|x{6,}|\*{4,}|-{4,}|your[-_ ]|my[-_]|some[-_]|placeholder|example|redacted|changeme|dummy|fake|test[-_]|null|true|false|undefined)/i;
|
|
151
|
-
|
|
152
|
-
// The generic `token = ...` rule fires on code samples far more often than on
|
|
153
|
-
// real leaks, so a hit must also look like an opaque credential: one run of
|
|
154
|
-
// credential characters, not an expression, an env lookup, or a CONSTANT_NAME.
|
|
155
|
-
const CREDENTIAL_VALUE = /^[A-Za-z0-9_\-.+/=:~]{16,}$/;
|
|
156
|
-
const CREDENTIAL_REFERENCE = /process\.env|os\.environ|os\.getenv|getenv|ENV\[|env\.|config\.|settings\.|self\.|this\.|_el\./i;
|
|
157
|
-
const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
158
|
-
const FILE_PATH_VALUE = /^[~/]|^\.{1,2}\//;
|
|
159
|
-
const ENV_ASSIGNMENT = /=(?!=*$)/;
|
|
31
|
+
const {
|
|
32
|
+
REGISTRY_TIMEOUT_MS,
|
|
33
|
+
PACK_RUN_INPUT_MAX_BYTES,
|
|
34
|
+
PACK_VISIBILITIES,
|
|
35
|
+
DEFAULT_SHARE_DAYS,
|
|
36
|
+
MAX_SHARE_DAYS,
|
|
37
|
+
MAX_SHARE_RECIPIENT_LENGTH,
|
|
38
|
+
SHARE_NONCE_RULE,
|
|
39
|
+
PACK_BROWSE_LIMIT,
|
|
40
|
+
REGISTRY_LIMITS,
|
|
41
|
+
SLUG_RULE,
|
|
42
|
+
PACKET,
|
|
43
|
+
SECRET_PATTERNS,
|
|
44
|
+
slugify,
|
|
45
|
+
titleFromSlug,
|
|
46
|
+
takeValue,
|
|
47
|
+
takeValues,
|
|
48
|
+
takeFlag,
|
|
49
|
+
parseSemver,
|
|
50
|
+
bumpVersion,
|
|
51
|
+
comparePackVersions,
|
|
52
|
+
readJson,
|
|
53
|
+
writeJson,
|
|
54
|
+
buildManifest,
|
|
55
|
+
packetDirectories,
|
|
56
|
+
packetDeniedSegments,
|
|
57
|
+
isRunningStateName,
|
|
58
|
+
classifyPacketPath,
|
|
59
|
+
isUtf8Text,
|
|
60
|
+
collectPacketEntries,
|
|
61
|
+
sha256,
|
|
62
|
+
canonicalZipEntryName,
|
|
63
|
+
canonicalContentPath,
|
|
64
|
+
contentEntries,
|
|
65
|
+
buildContentHashes,
|
|
66
|
+
parseContentHashes,
|
|
67
|
+
verifyArchiveContentHashes,
|
|
68
|
+
contentHashFingerprint,
|
|
69
|
+
redactSecret,
|
|
70
|
+
trimSecretValue,
|
|
71
|
+
looksLikePlaceholder,
|
|
72
|
+
looksLikeCredentialValue,
|
|
73
|
+
scanTextForSecrets,
|
|
74
|
+
scanEntriesForSecrets,
|
|
75
|
+
parseJsonBody,
|
|
76
|
+
responseErrorText,
|
|
77
|
+
registryUrl,
|
|
78
|
+
registryOrigin,
|
|
79
|
+
optionalAuthHeaders,
|
|
80
|
+
refreshApiWithoutProviderHint,
|
|
81
|
+
refreshRegistryCredentials,
|
|
82
|
+
requestRegistryZip,
|
|
83
|
+
registryNotFoundError,
|
|
84
|
+
requiredAuthHeaders,
|
|
85
|
+
assertPublishableSlug,
|
|
86
|
+
assertPublishableAuthor,
|
|
87
|
+
postPackToRegistry,
|
|
88
|
+
paidPackBarrierFromResponse,
|
|
89
|
+
formatSalesDollars,
|
|
90
|
+
printPaidPackBarrier,
|
|
91
|
+
throwPaidPackBarrier,
|
|
92
|
+
fetchRegistryZip,
|
|
93
|
+
fetchRegistryZipForUser,
|
|
94
|
+
sanitizePersonalizationName,
|
|
95
|
+
parsePackShareArgs,
|
|
96
|
+
requestRegistryJson,
|
|
97
|
+
mintPackShare,
|
|
98
|
+
listPackShares,
|
|
99
|
+
revokePackShares,
|
|
100
|
+
revokePackShare,
|
|
101
|
+
registryPackItems,
|
|
102
|
+
packManifestFromItem,
|
|
103
|
+
isPublicPackPayload,
|
|
104
|
+
registryConfirmsPublicPack,
|
|
105
|
+
legacyPersonalizedShareUrl,
|
|
106
|
+
formatShareExpiry,
|
|
107
|
+
cleanTableCell,
|
|
108
|
+
formatPackShareLinksTable,
|
|
109
|
+
browseStarValue,
|
|
110
|
+
browsePriceCents,
|
|
111
|
+
packBrowseRows,
|
|
112
|
+
formatPackBrowseTable,
|
|
113
|
+
packSalesUrl,
|
|
114
|
+
packPurchasesUrl,
|
|
115
|
+
formatPackSaleDate,
|
|
116
|
+
isPackTransactionRefunded,
|
|
117
|
+
formatPackTransactionsTable,
|
|
118
|
+
formatPackSalesTable,
|
|
119
|
+
formatPackPurchasesTable,
|
|
120
|
+
formatBytes,
|
|
121
|
+
printPacketComposition,
|
|
122
|
+
printPacketTree,
|
|
123
|
+
printSkipped,
|
|
124
|
+
printPacketSummary,
|
|
125
|
+
registryLimitFailures,
|
|
126
|
+
reportSecretFindings,
|
|
127
|
+
shellQuote,
|
|
128
|
+
fallbackSlugFromZipPath,
|
|
129
|
+
loadZipPayload,
|
|
130
|
+
parseManifest,
|
|
131
|
+
manifestVersion,
|
|
132
|
+
upstreamStatePath,
|
|
133
|
+
packStatePath,
|
|
134
|
+
remoteCheckTime,
|
|
135
|
+
recordRemoteCheck,
|
|
136
|
+
buildUpstreamState,
|
|
137
|
+
writeUpstreamState,
|
|
138
|
+
hasStagedUpstream,
|
|
139
|
+
} = require('../lib/pack-core');
|
|
160
140
|
|
|
161
141
|
function showHelp() {
|
|
162
142
|
console.log('usage: atris pack craft "<topic>" [--dir <target>] [--force]');
|
|
@@ -186,958 +166,6 @@ function showHelp() {
|
|
|
186
166
|
console.log('declared permissions are enforced locally; declared-capability cloud runs fail closed.');
|
|
187
167
|
}
|
|
188
168
|
|
|
189
|
-
function slugify(value, fallback = 'atris-pack') {
|
|
190
|
-
return String(value || fallback)
|
|
191
|
-
.toLowerCase()
|
|
192
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
193
|
-
.replace(/^-+|-+$/g, '') || fallback;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function titleFromSlug(slug) {
|
|
197
|
-
return String(slug || 'atris-pack')
|
|
198
|
-
.split('-')
|
|
199
|
-
.filter(Boolean)
|
|
200
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
201
|
-
.join(' ') || 'Atris Pack';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function takeValue(args, name) {
|
|
205
|
-
const prefix = `${name}=`;
|
|
206
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
207
|
-
const arg = String(args[i]);
|
|
208
|
-
if (arg.startsWith(prefix)) {
|
|
209
|
-
args.splice(i, 1);
|
|
210
|
-
return arg.slice(prefix.length);
|
|
211
|
-
}
|
|
212
|
-
if (arg === name) {
|
|
213
|
-
const value = args[i + 1];
|
|
214
|
-
if (value === undefined || String(value).startsWith('--')) {
|
|
215
|
-
throw new Error(`${name} requires a value`);
|
|
216
|
-
}
|
|
217
|
-
args.splice(i, 2);
|
|
218
|
-
return value;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function takeValues(args, name) {
|
|
225
|
-
const values = [];
|
|
226
|
-
while (args.some((arg) => String(arg) === name || String(arg).startsWith(`${name}=`))) {
|
|
227
|
-
values.push(takeValue(args, name));
|
|
228
|
-
}
|
|
229
|
-
return values;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function takeFlag(args, name, options = {}) {
|
|
233
|
-
const index = args.indexOf(name);
|
|
234
|
-
if (index === -1) return false;
|
|
235
|
-
const next = args[index + 1];
|
|
236
|
-
if (options.optionalValue && next !== undefined && !String(next).startsWith('--')) {
|
|
237
|
-
args.splice(index, 2);
|
|
238
|
-
return next;
|
|
239
|
-
}
|
|
240
|
-
args.splice(index, 1);
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function parseSemver(value) {
|
|
245
|
-
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(value || ''));
|
|
246
|
-
if (!match) return null;
|
|
247
|
-
return match.slice(1).map((part) => Number(part));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function bumpVersion(current, bump) {
|
|
251
|
-
const parsed = parseSemver(current);
|
|
252
|
-
if (!parsed) return '0.1.0';
|
|
253
|
-
const [major, minor, patch] = parsed;
|
|
254
|
-
if (bump === 'major') return `${major + 1}.0.0`;
|
|
255
|
-
if (bump === 'minor') return `${major}.${minor + 1}.0`;
|
|
256
|
-
return `${major}.${minor}.${patch + 1}`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function comparePackVersions(left, right) {
|
|
260
|
-
const leftParsed = parseSemver(left);
|
|
261
|
-
const rightParsed = parseSemver(right);
|
|
262
|
-
if (!leftParsed || !rightParsed) {
|
|
263
|
-
return String(left || '') === String(right || '') ? 0 : null;
|
|
264
|
-
}
|
|
265
|
-
for (let i = 0; i < leftParsed.length; i += 1) {
|
|
266
|
-
if (leftParsed[i] > rightParsed[i]) return 1;
|
|
267
|
-
if (leftParsed[i] < rightParsed[i]) return -1;
|
|
268
|
-
}
|
|
269
|
-
return 0;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function readJson(filePath) {
|
|
273
|
-
if (!fs.existsSync(filePath)) return null;
|
|
274
|
-
try {
|
|
275
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
276
|
-
} catch (error) {
|
|
277
|
-
throw new Error(`could not read ${filePath}: ${error.message}`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function writeJson(filePath, value) {
|
|
282
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
283
|
-
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function buildManifest(existing, options) {
|
|
287
|
-
const existingManifest = existing && typeof existing === 'object' ? existing : {};
|
|
288
|
-
const hasExistingManifest = existing && typeof existing === 'object';
|
|
289
|
-
const priceCents = existingManifest.priceCents;
|
|
290
|
-
if (priceCents !== undefined && (!Number.isSafeInteger(priceCents) || priceCents < 0)) {
|
|
291
|
-
throw new Error('pack.json priceCents must be a non-negative integer');
|
|
292
|
-
}
|
|
293
|
-
const slug = slugify(options.slug || existingManifest.slug || options.fallbackSlug);
|
|
294
|
-
const version = existingManifest.version
|
|
295
|
-
? bumpVersion(existingManifest.version, options.bump)
|
|
296
|
-
: '0.1.0';
|
|
297
|
-
const title = existingManifest.title || titleFromSlug(slug);
|
|
298
|
-
let visibility;
|
|
299
|
-
if (options.visibility !== null && options.visibility !== undefined) {
|
|
300
|
-
visibility = String(options.visibility);
|
|
301
|
-
} else if (Object.prototype.hasOwnProperty.call(existingManifest, 'visibility')) {
|
|
302
|
-
visibility = existingManifest.visibility;
|
|
303
|
-
} else if (!hasExistingManifest) {
|
|
304
|
-
visibility = 'unlisted';
|
|
305
|
-
}
|
|
306
|
-
if (visibility !== undefined && !PACK_VISIBILITIES.has(visibility)) {
|
|
307
|
-
throw new Error('pack visibility must be public, unlisted, or private');
|
|
308
|
-
}
|
|
309
|
-
const manifest = {
|
|
310
|
-
name: existingManifest.name || slug,
|
|
311
|
-
slug,
|
|
312
|
-
title,
|
|
313
|
-
description: existingManifest.description || `Atris pack for ${title}.`,
|
|
314
|
-
author: (options.author && String(options.author).trim()) || existingManifest.author || '',
|
|
315
|
-
tags: Array.isArray(existingManifest.tags) ? existingManifest.tags : [],
|
|
316
|
-
version,
|
|
317
|
-
versions: Array.isArray(existingManifest.versions) ? [...existingManifest.versions] : [],
|
|
318
|
-
};
|
|
319
|
-
if (visibility !== undefined) manifest.visibility = visibility;
|
|
320
|
-
if (priceCents !== undefined) manifest.priceCents = priceCents;
|
|
321
|
-
// Entry-contract and provenance fields must survive republish, or inspect's
|
|
322
|
-
// trust surface goes blank the moment a pack ships.
|
|
323
|
-
for (const key of [
|
|
324
|
-
'type', 'entrypoint', 'verifier', 'permissions', 'origin',
|
|
325
|
-
'created-in', 'createdIn', 'learned-at', 'learnedAt',
|
|
326
|
-
'source-urls', 'sourceUrls', 'sources',
|
|
327
|
-
]) {
|
|
328
|
-
if (existingManifest[key] !== undefined) manifest[key] = existingManifest[key];
|
|
329
|
-
}
|
|
330
|
-
if (manifest.permissions !== undefined) assertPackCapabilityPolicy(manifest.permissions);
|
|
331
|
-
manifest.versions.push({
|
|
332
|
-
version,
|
|
333
|
-
date: new Date().toISOString(),
|
|
334
|
-
// the registry rejects entries with empty notes, so never write one
|
|
335
|
-
notes: options.notes || 'version bump',
|
|
336
|
-
});
|
|
337
|
-
return manifest;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function packetDirectories(includeLogs) {
|
|
341
|
-
return includeLogs ? [...PACKET.directories, 'logs', 'journal'] : PACKET.directories;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function packetDeniedSegments(includeLogs) {
|
|
345
|
-
if (!includeLogs) return PACKET.deniedSegments;
|
|
346
|
-
return PACKET.deniedSegments.filter((segment) => segment !== 'logs' && segment !== 'journal');
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Definitions travel, running state stays home. Judged on the filename alone
|
|
350
|
-
// so the rule holds in any workspace, whatever its folders are called.
|
|
351
|
-
function isRunningStateName(lowerBase) {
|
|
352
|
-
if (PACKET.runningStateNames.includes(lowerBase)) return true;
|
|
353
|
-
return PACKET.runningStatePatterns.some((pattern) => pattern.test(lowerBase));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Paths are judged relative to the pack root, so `atris/` is transparent: the
|
|
357
|
-
// same table decides `wiki/page.md` in a crafted pack and `atris/wiki/page.md`
|
|
358
|
-
// in a workspace publish.
|
|
359
|
-
function classifyPacketPath(relativePath, { includeLogs = false, isDirectory = false } = {}) {
|
|
360
|
-
const normalized = String(relativePath).replace(/\\/g, '/');
|
|
361
|
-
const parts = normalized.split('/').filter(Boolean);
|
|
362
|
-
const basename = parts[parts.length - 1] || '';
|
|
363
|
-
const lowerBase = basename.toLowerCase();
|
|
364
|
-
const denied = packetDeniedSegments(includeLogs);
|
|
365
|
-
|
|
366
|
-
for (const part of parts) {
|
|
367
|
-
if (denied.includes(part)) {
|
|
368
|
-
const reason = PACKET.segmentReasons[part];
|
|
369
|
-
return { ok: false, reason: reason || `runtime exhaust or dependency (${part}/)` };
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (PACKET.deniedNames.includes(lowerBase)) return { ok: false, reason: 'lockfile or editor junk' };
|
|
373
|
-
if (PACKET.deniedPatterns.some((pattern) => pattern.test(basename))) {
|
|
374
|
-
return { ok: false, reason: 'secret-shaped filename' };
|
|
375
|
-
}
|
|
376
|
-
if (!isDirectory && isRunningStateName(lowerBase)) {
|
|
377
|
-
return { ok: false, reason: PACKET.runningStateReason };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const inner = parts[0] === 'atris' ? parts.slice(1) : parts;
|
|
381
|
-
if (!inner.length) return { ok: true };
|
|
382
|
-
if (isDirectory) {
|
|
383
|
-
if (inner.length > 1) return { ok: true };
|
|
384
|
-
return packetDirectories(includeLogs).includes(inner[0])
|
|
385
|
-
? { ok: true }
|
|
386
|
-
: { ok: false, reason: `not in the pack allowlist (${inner[0]}/)` };
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const extension = path.extname(lowerBase);
|
|
390
|
-
if (inner.length === 1) {
|
|
391
|
-
if (PACKET.rootFiles.includes(lowerBase)) return { ok: true };
|
|
392
|
-
return PACKET.rootExtensions.includes(extension)
|
|
393
|
-
? { ok: true }
|
|
394
|
-
: { ok: false, reason: 'not a root document (.md/.txt)' };
|
|
395
|
-
}
|
|
396
|
-
if (!packetDirectories(includeLogs).includes(inner[0])) {
|
|
397
|
-
return { ok: false, reason: `not in the pack allowlist (${inner[0]}/)` };
|
|
398
|
-
}
|
|
399
|
-
return PACKET.extensions.includes(extension)
|
|
400
|
-
? { ok: true }
|
|
401
|
-
: { ok: false, reason: `not a text file type (${extension || 'no extension'})` };
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// The registry rejects non-UTF-8 members, and a stranger cannot read a binary
|
|
405
|
-
// anyway, so prove it decodes before shipping it.
|
|
406
|
-
function isUtf8Text(buffer) {
|
|
407
|
-
if (buffer.includes(0)) return false;
|
|
408
|
-
return Buffer.compare(Buffer.from(buffer.toString('utf8'), 'utf8'), buffer) === 0;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// A crafted pack keeps wiki/, README.md, and friends at the pack root, so a
|
|
412
|
-
// root publish walks the whole folder, not just atris/ (2026-07-09: the first
|
|
413
|
-
// dogfooded pack shipped without its research).
|
|
414
|
-
function collectPacketEntries(sourceDir, { prefix = '', includeLogs = false } = {}) {
|
|
415
|
-
const entries = [];
|
|
416
|
-
const skipped = [];
|
|
417
|
-
|
|
418
|
-
function record(relative, reason) {
|
|
419
|
-
skipped.push({ path: prefix ? `${prefix}/${relative}` : relative, reason });
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function walk(dir, relativeDir = '') {
|
|
423
|
-
const names = fs.readdirSync(dir).sort();
|
|
424
|
-
for (const name of names) {
|
|
425
|
-
const abs = path.join(dir, name);
|
|
426
|
-
const rel = (relativeDir ? `${relativeDir}/${name}` : name).replace(/\\/g, '/');
|
|
427
|
-
const scoped = prefix ? `${prefix}/${rel}` : rel;
|
|
428
|
-
let stat;
|
|
429
|
-
try {
|
|
430
|
-
stat = fs.statSync(abs);
|
|
431
|
-
} catch {
|
|
432
|
-
record(rel, 'unreadable');
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
if (stat.isDirectory()) {
|
|
436
|
-
const verdict = classifyPacketPath(scoped, { includeLogs, isDirectory: true });
|
|
437
|
-
if (!verdict.ok) {
|
|
438
|
-
record(`${rel}/`, verdict.reason);
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
walk(abs, rel);
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (!stat.isFile()) continue;
|
|
445
|
-
const verdict = classifyPacketPath(scoped, { includeLogs });
|
|
446
|
-
if (!verdict.ok) {
|
|
447
|
-
record(rel, verdict.reason);
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
const data = fs.readFileSync(abs);
|
|
451
|
-
if (!isUtf8Text(data)) {
|
|
452
|
-
record(rel, 'not valid utf-8 text');
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
entries.push({ name: scoped, data, mtime: stat.mtime });
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
walk(sourceDir);
|
|
460
|
-
return { entries, skipped };
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function sha256(data) {
|
|
464
|
-
return createHash('sha256').update(data).digest('hex');
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function canonicalContentPath(value) {
|
|
468
|
-
const raw = String(value || '');
|
|
469
|
-
if (!raw || raw.includes('\\') || raw.startsWith('/') || raw.endsWith('/')) return null;
|
|
470
|
-
const normalized = path.posix.normalize(raw);
|
|
471
|
-
if (normalized !== raw || normalized === '.' || normalized === 'pack.json') return null;
|
|
472
|
-
if (normalized.split('/').includes('..')) return null;
|
|
473
|
-
return normalized;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function contentEntries(entries) {
|
|
477
|
-
return entries.filter((entry) => {
|
|
478
|
-
const raw = String(entry.name || '').replace(/\\/g, '/');
|
|
479
|
-
return raw && !raw.endsWith('/') && canonicalZipEntryName(raw) !== 'pack.json';
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function buildContentHashes(entries) {
|
|
484
|
-
return Object.fromEntries(
|
|
485
|
-
contentEntries(entries)
|
|
486
|
-
.map((entry) => [canonicalZipEntryName(entry.name), sha256(entry.data)])
|
|
487
|
-
.sort(([left], [right]) => left.localeCompare(right)),
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function parseContentHashes(manifest) {
|
|
492
|
-
if (!Object.prototype.hasOwnProperty.call(manifest, 'content-hashes')) {
|
|
493
|
-
return { present: false, hashes: new Map() };
|
|
494
|
-
}
|
|
495
|
-
const declared = manifest['content-hashes'];
|
|
496
|
-
if (!declared || typeof declared !== 'object' || Array.isArray(declared)) {
|
|
497
|
-
throw new Error('pack.json content-hashes must be an object of path to lowercase SHA-256');
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const hashes = new Map();
|
|
501
|
-
for (const [rawPath, digest] of Object.entries(declared)) {
|
|
502
|
-
const contentPath = canonicalContentPath(rawPath);
|
|
503
|
-
if (!contentPath) {
|
|
504
|
-
throw new Error(`pack.json content-hashes has invalid path: ${rawPath}`);
|
|
505
|
-
}
|
|
506
|
-
if (typeof digest !== 'string' || !/^[0-9a-f]{64}$/.test(digest)) {
|
|
507
|
-
throw new Error(`pack.json content-hashes has invalid SHA-256 for ${rawPath}`);
|
|
508
|
-
}
|
|
509
|
-
hashes.set(contentPath, digest);
|
|
510
|
-
}
|
|
511
|
-
return { present: true, hashes };
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function verifyArchiveContentHashes(entries, manifest) {
|
|
515
|
-
const declared = parseContentHashes(manifest);
|
|
516
|
-
if (declared.present) {
|
|
517
|
-
for (const entry of entries) {
|
|
518
|
-
const raw = String(entry.name || '');
|
|
519
|
-
if (!raw.endsWith('/') && (raw.includes('\\') || canonicalZipEntryName(raw) !== raw)) {
|
|
520
|
-
throw new Error(`pack content-hashes requires canonical archive path: ${raw}`);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
const files = new Map(
|
|
525
|
-
contentEntries(entries).map((entry) => [canonicalZipEntryName(entry.name), entry.data]),
|
|
526
|
-
);
|
|
527
|
-
if (!declared.present) {
|
|
528
|
-
return { status: 'absent', declared: 0, files: files.size, hashes: declared.hashes };
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
for (const [contentPath, digest] of declared.hashes) {
|
|
532
|
-
const data = files.get(contentPath);
|
|
533
|
-
if (!data) throw new Error(`pack content-hashes claims missing file: ${contentPath}`);
|
|
534
|
-
if (sha256(data) !== digest) {
|
|
535
|
-
throw new Error(`pack content hash mismatch: ${contentPath}`);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
return {
|
|
539
|
-
status: declared.hashes.size === files.size ? 'verified' : 'partial',
|
|
540
|
-
declared: declared.hashes.size,
|
|
541
|
-
files: files.size,
|
|
542
|
-
hashes: declared.hashes,
|
|
543
|
-
};
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function contentHashFingerprint(manifest) {
|
|
547
|
-
const parsed = parseContentHashes(manifest);
|
|
548
|
-
if (!parsed.present) return null;
|
|
549
|
-
return JSON.stringify([...parsed.hashes.entries()].sort(([left], [right]) => left.localeCompare(right)));
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function redactSecret(value) {
|
|
553
|
-
const text = String(value || '');
|
|
554
|
-
if (text.length <= 8) return '*'.repeat(Math.max(text.length, 3));
|
|
555
|
-
const masked = '*'.repeat(Math.min(12, text.length - 6));
|
|
556
|
-
return `${text.slice(0, 4)}${masked}${text.slice(-2)}`;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function trimSecretValue(value) {
|
|
560
|
-
return String(value || '')
|
|
561
|
-
.replace(/^[`'"]+/, '')
|
|
562
|
-
.replace(/[`'"]+$/, '')
|
|
563
|
-
.replace(/[,;.)\]}>|]+$/, '');
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function looksLikePlaceholder(value) {
|
|
567
|
-
return SECRET_PLACEHOLDER.test(trimSecretValue(value));
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function looksLikeCredentialValue(value) {
|
|
571
|
-
const trimmed = trimSecretValue(value);
|
|
572
|
-
if (!CREDENTIAL_VALUE.test(trimmed)) return false;
|
|
573
|
-
if (CREDENTIAL_REFERENCE.test(trimmed)) return false;
|
|
574
|
-
if (CONSTANT_NAME.test(trimmed)) return false;
|
|
575
|
-
if (FILE_PATH_VALUE.test(trimmed)) return false;
|
|
576
|
-
if (ENV_ASSIGNMENT.test(trimmed)) return false;
|
|
577
|
-
// Real credentials carry entropy: a digit, or at least mixed case.
|
|
578
|
-
return /\d/.test(trimmed) || (/[a-z]/.test(trimmed) && /[A-Z]/.test(trimmed));
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function scanTextForSecrets(text) {
|
|
582
|
-
const findings = [];
|
|
583
|
-
const lines = String(text).split(/\r?\n/);
|
|
584
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
585
|
-
const line = lines[i];
|
|
586
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
587
|
-
pattern.regex.lastIndex = 0;
|
|
588
|
-
let match = pattern.regex.exec(line);
|
|
589
|
-
while (match) {
|
|
590
|
-
const value = match[1] !== undefined ? match[1] : match[0];
|
|
591
|
-
// The shaped patterns (jwt, sk-, AKIA, BEGIN PRIVATE KEY) are specific
|
|
592
|
-
// enough to stand on their own; only the generic rule needs filtering.
|
|
593
|
-
const generic = match[1] !== undefined;
|
|
594
|
-
if (!generic || (!looksLikePlaceholder(value) && looksLikeCredentialValue(value))) {
|
|
595
|
-
findings.push({ line: i + 1, label: pattern.label, redacted: redactSecret(trimSecretValue(value)) });
|
|
596
|
-
}
|
|
597
|
-
match = pattern.regex.exec(line);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return findings;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function scanEntriesForSecrets(entries) {
|
|
605
|
-
const findings = [];
|
|
606
|
-
for (const entry of entries) {
|
|
607
|
-
if (!entry || !entry.data) continue;
|
|
608
|
-
for (const hit of scanTextForSecrets(entry.data.toString('utf8'))) {
|
|
609
|
-
findings.push({ file: entry.name, ...hit });
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return findings;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function parseJsonBody(buffer) {
|
|
616
|
-
const text = Buffer.isBuffer(buffer) ? buffer.toString('utf8') : String(buffer || '');
|
|
617
|
-
if (!text.trim()) return { data: null, text };
|
|
618
|
-
try {
|
|
619
|
-
return { data: JSON.parse(text), text };
|
|
620
|
-
} catch {
|
|
621
|
-
return { data: null, text };
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function responseErrorText(response, fallback) {
|
|
626
|
-
const parsed = parseJsonBody(response.body);
|
|
627
|
-
const data = parsed.data;
|
|
628
|
-
if (data && typeof data === 'object') {
|
|
629
|
-
const message = data.error || data.message || data.detail;
|
|
630
|
-
if (message) return String(message);
|
|
631
|
-
}
|
|
632
|
-
if (parsed.text.trim()) return parsed.text.trim();
|
|
633
|
-
return fallback;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function registryUrl(pathname, deps = {}) {
|
|
637
|
-
const appBase = (deps.getAppBaseUrl || getAppBaseUrl)();
|
|
638
|
-
const cleanBase = String(appBase || '').replace(/\/+$/, '');
|
|
639
|
-
return `${cleanBase}${pathname}`;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// The web app's CSRF gate only trusts requests whose Origin matches its own
|
|
643
|
-
// url; bearer-token CLI posts are not a CSRF vector, so we present the app
|
|
644
|
-
// origin we are posting to (2026-07-09: prod publish --push failed 403 while
|
|
645
|
-
// every test passed against a bare local server).
|
|
646
|
-
function registryOrigin(deps = {}) {
|
|
647
|
-
const appBase = (deps.getAppBaseUrl || getAppBaseUrl)();
|
|
648
|
-
try {
|
|
649
|
-
return new URL(String(appBase)).origin;
|
|
650
|
-
} catch {
|
|
651
|
-
return '';
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function optionalAuthHeaders(deps = {}) {
|
|
656
|
-
const readCredentials = deps.loadCredentials || loadCredentials;
|
|
657
|
-
const credentials = readCredentials();
|
|
658
|
-
return credentials && credentials.token ? { Authorization: `Bearer ${credentials.token}` } : {};
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function refreshApiWithoutProviderHint(deps = {}) {
|
|
662
|
-
const requestJson = deps.apiRequestJson || apiRequestJson;
|
|
663
|
-
return (pathname, options = {}) => {
|
|
664
|
-
if (pathname !== '/auth/refresh') return requestJson(pathname, options);
|
|
665
|
-
const body = options.body && typeof options.body === 'object' ? { ...options.body } : {};
|
|
666
|
-
delete body.provider;
|
|
667
|
-
return requestJson(pathname, { ...options, body });
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async function refreshRegistryCredentials(credentials, deps = {}) {
|
|
672
|
-
const refresh = deps.performTokenRefresh || performTokenRefresh;
|
|
673
|
-
try {
|
|
674
|
-
const result = await refresh(credentials, refreshApiWithoutProviderHint(deps));
|
|
675
|
-
if (!result || !result.ok) return null;
|
|
676
|
-
return result.payload?.credentials || (deps.loadCredentials || loadCredentials)();
|
|
677
|
-
} catch {
|
|
678
|
-
return null;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async function requestRegistryZip(url, deps = {}, options = {}) {
|
|
683
|
-
const request = deps.httpRequest || httpRequest;
|
|
684
|
-
const readCredentials = deps.loadCredentials || loadCredentials;
|
|
685
|
-
const credentials = readCredentials();
|
|
686
|
-
const authenticated = Boolean(credentials && credentials.token);
|
|
687
|
-
const requestWithToken = (token) => request(url, {
|
|
688
|
-
...options,
|
|
689
|
-
method: 'GET',
|
|
690
|
-
headers: token ? { ...(options.headers || {}), Authorization: `Bearer ${token}` } : { ...(options.headers || {}) },
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
let response = await requestWithToken(credentials?.token);
|
|
694
|
-
if (response.status === 404 && authenticated) {
|
|
695
|
-
const refreshedCredentials = await refreshRegistryCredentials(credentials, deps);
|
|
696
|
-
if (refreshedCredentials && refreshedCredentials.token) {
|
|
697
|
-
response = await requestWithToken(refreshedCredentials.token);
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return { response, authenticated };
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function registryNotFoundError(slug, authenticated) {
|
|
704
|
-
if (authenticated) {
|
|
705
|
-
return new Error('pack not found or you do not have access (your login may be stale; try atris login)');
|
|
706
|
-
}
|
|
707
|
-
return new Error(`pack not found: ${slug}`);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function requiredAuthHeaders(deps = {}, purpose = 'publish packs') {
|
|
711
|
-
const headers = optionalAuthHeaders(deps);
|
|
712
|
-
if (!headers.Authorization) {
|
|
713
|
-
throw new Error(`not logged in. run atris login first to ${purpose}.`);
|
|
714
|
-
}
|
|
715
|
-
return headers;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async function postPackToRegistry(manifest, zipBuffer, deps = {}) {
|
|
719
|
-
const request = deps.httpRequest || httpRequest;
|
|
720
|
-
const url = registryUrl('/api/pack/registry', deps);
|
|
721
|
-
const body = JSON.stringify({ manifest, zipBase64: zipBuffer.toString('base64') });
|
|
722
|
-
const authHeaders = requiredAuthHeaders(deps);
|
|
723
|
-
assertPublishableAuthor(manifest);
|
|
724
|
-
let response;
|
|
725
|
-
try {
|
|
726
|
-
response = await request(url, {
|
|
727
|
-
method: 'POST',
|
|
728
|
-
timeoutMs: REGISTRY_TIMEOUT_MS,
|
|
729
|
-
headers: {
|
|
730
|
-
...authHeaders,
|
|
731
|
-
Accept: 'application/json',
|
|
732
|
-
'Content-Type': 'application/json',
|
|
733
|
-
...(registryOrigin(deps) ? { Origin: registryOrigin(deps) } : {}),
|
|
734
|
-
},
|
|
735
|
-
body,
|
|
736
|
-
});
|
|
737
|
-
} catch {
|
|
738
|
-
throw new Error('could not reach pack registry. check your connection and try again.');
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const parsed = parseJsonBody(response.body);
|
|
742
|
-
if (response.status < 200 || response.status >= 300) {
|
|
743
|
-
throw new Error(responseErrorText(response, `registry publish failed with status ${response.status}`));
|
|
744
|
-
}
|
|
745
|
-
if (!parsed.data || typeof parsed.data !== 'object' || parsed.data.ok === false) {
|
|
746
|
-
throw new Error(responseErrorText(response, 'registry publish failed'));
|
|
747
|
-
}
|
|
748
|
-
return parsed.data;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
function paidPackBarrierFromResponse(response, slug) {
|
|
752
|
-
const status = Number(response && response.status) || 0;
|
|
753
|
-
if (status === 404) return null;
|
|
754
|
-
|
|
755
|
-
const rawBody = response && response.body;
|
|
756
|
-
const body = !rawBody
|
|
757
|
-
? Buffer.alloc(0)
|
|
758
|
-
: (Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(String(rawBody)));
|
|
759
|
-
const empty = body.length === 0;
|
|
760
|
-
const parsed = parseJsonBody(body);
|
|
761
|
-
const data = parsed.data && typeof parsed.data === 'object' && !Array.isArray(parsed.data)
|
|
762
|
-
? parsed.data
|
|
763
|
-
: {};
|
|
764
|
-
const priceRaw = data.price_cents ?? data.priceCents;
|
|
765
|
-
const priceCents = Number(priceRaw);
|
|
766
|
-
const hasPrice = Number.isFinite(priceCents) && priceCents > 0;
|
|
767
|
-
const nameSource = [data.name, data.title, data.slug, slug]
|
|
768
|
-
.find((value) => typeof value === 'string' && value.trim());
|
|
769
|
-
const details = {
|
|
770
|
-
name: String(nameSource || slug || 'this pack').trim().toLowerCase(),
|
|
771
|
-
priceCents: hasPrice ? Math.round(priceCents) : null,
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
const entitled = data.entitled === true
|
|
775
|
-
|| data.has_access === true
|
|
776
|
-
|| data.hasAccess === true
|
|
777
|
-
|| data.is_owner === true
|
|
778
|
-
|| data.isOwner === true;
|
|
779
|
-
const unentitled = data.entitled === false
|
|
780
|
-
|| data.has_access === false
|
|
781
|
-
|| data.hasAccess === false
|
|
782
|
-
|| data.entitlement === false
|
|
783
|
-
|| data.entitlement === null;
|
|
784
|
-
const message = String(data.error || data.message || data.detail || '');
|
|
785
|
-
const looksLikeZip = body.length >= 4 && body[0] === 0x50 && body[1] === 0x4b;
|
|
786
|
-
const paid = status === 402
|
|
787
|
-
|| data.paid === true
|
|
788
|
-
|| data.is_paid === true
|
|
789
|
-
|| data.isPaid === true
|
|
790
|
-
|| data.purchase_required === true
|
|
791
|
-
|| typeof data.checkout_hint === 'string'
|
|
792
|
-
|| /purchase required/i.test(message)
|
|
793
|
-
|| (hasPrice && (unentitled || !looksLikeZip));
|
|
794
|
-
|
|
795
|
-
if (entitled && status >= 200 && status < 300 && looksLikeZip) return null;
|
|
796
|
-
if (status === 402 || status === 403) return details;
|
|
797
|
-
if (status >= 200 && status < 300 && empty) return details;
|
|
798
|
-
if (paid && !entitled && !looksLikeZip) return details;
|
|
799
|
-
return null;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
function printPaidPackBarrier(slug, details = {}, deps = {}) {
|
|
803
|
-
const name = String(details.name || slug || 'this pack').trim().toLowerCase() || 'this pack';
|
|
804
|
-
const cents = Number(details.priceCents);
|
|
805
|
-
const price = Number.isFinite(cents) && cents > 0 ? ` (${formatSalesDollars(cents)})` : '';
|
|
806
|
-
console.error(`this pack is paid: ${name}${price}.`);
|
|
807
|
-
console.error('buy it on its page, then run this again.');
|
|
808
|
-
console.error(registryUrl(`/packs/${encodeURIComponent(slug)}`, deps));
|
|
809
|
-
console.error('already bought it? run atris login');
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function throwPaidPackBarrier(barrier, status) {
|
|
813
|
-
const error = new Error('this pack is paid');
|
|
814
|
-
error.status = status;
|
|
815
|
-
error.paidPack = barrier;
|
|
816
|
-
throw error;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
async function fetchRegistryZip(slug, deps = {}) {
|
|
820
|
-
const url = registryUrl(`/api/pack/registry/${encodeURIComponent(slug)}`, deps);
|
|
821
|
-
let result;
|
|
822
|
-
try {
|
|
823
|
-
result = await requestRegistryZip(url, deps, {
|
|
824
|
-
timeoutMs: REGISTRY_TIMEOUT_MS,
|
|
825
|
-
});
|
|
826
|
-
} catch {
|
|
827
|
-
throw new Error('could not reach pack registry. check your connection and try again.');
|
|
828
|
-
}
|
|
829
|
-
const { response, authenticated } = result;
|
|
830
|
-
if (response.status === 404) throw registryNotFoundError(slug, authenticated);
|
|
831
|
-
const barrier = paidPackBarrierFromResponse(response, slug);
|
|
832
|
-
if (barrier) throwPaidPackBarrier(barrier, response.status);
|
|
833
|
-
if (response.status < 200 || response.status >= 300) {
|
|
834
|
-
const error = new Error(responseErrorText(response, `registry lookup failed for ${slug} with status ${response.status}`));
|
|
835
|
-
error.status = response.status;
|
|
836
|
-
throw error;
|
|
837
|
-
}
|
|
838
|
-
if (!response.body || response.body.length === 0) {
|
|
839
|
-
throw new Error(`registry returned an empty zip for ${slug}`);
|
|
840
|
-
}
|
|
841
|
-
return response.body;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
async function fetchRegistryZipForUser(slug, deps = {}) {
|
|
845
|
-
try {
|
|
846
|
-
return await fetchRegistryZip(slug, deps);
|
|
847
|
-
} catch (error) {
|
|
848
|
-
if (!error || !error.paidPack) throw error;
|
|
849
|
-
printPaidPackBarrier(slug, error.paidPack, deps);
|
|
850
|
-
return null;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function assertPublishableSlug(slug) {
|
|
855
|
-
if (!SLUG_RULE.test(String(slug || ''))) {
|
|
856
|
-
throw new Error(
|
|
857
|
-
`pack slug "${slug}" is not viewable on the web. use 3-40 characters of a-z, 0-9 and dashes: --slug <slug>`,
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
function assertPublishableAuthor(manifest) {
|
|
863
|
-
if (!manifest.author || !String(manifest.author).trim()) {
|
|
864
|
-
throw new Error('publishing needs an author. re-run with --author "<your name>" (or set "author" in pack.json).');
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
// ── pack share ──────────────────────────────────────────────────────────────
|
|
869
|
-
// Signed links are the primary handoff. The old /packs/<slug>?for=<name> link
|
|
870
|
-
// is retained only when minting fails for a pack the public registry confirms
|
|
871
|
-
// is public.
|
|
872
|
-
const PERSONALIZATION_MAX_LENGTH = 40;
|
|
873
|
-
const PERSONALIZATION_MARKUP = /[<>&"`\\/]/;
|
|
874
|
-
const PERSONALIZATION_DISALLOWED = /[^\p{L}\p{M} '’.\-]/gu;
|
|
875
|
-
const PERSONALIZATION_HAS_LETTER = /\p{L}/u;
|
|
876
|
-
|
|
877
|
-
function sanitizePersonalizationName(raw) {
|
|
878
|
-
if (typeof raw !== 'string') return null;
|
|
879
|
-
if (PERSONALIZATION_MARKUP.test(raw)) return null;
|
|
880
|
-
const cleaned = raw
|
|
881
|
-
.normalize('NFC')
|
|
882
|
-
.replace(/\s/g, ' ')
|
|
883
|
-
.replace(PERSONALIZATION_DISALLOWED, '')
|
|
884
|
-
.replace(/ +/g, ' ')
|
|
885
|
-
.trim();
|
|
886
|
-
if (!cleaned) return null;
|
|
887
|
-
const capped = cleaned.slice(0, PERSONALIZATION_MAX_LENGTH).trim();
|
|
888
|
-
if (!capped || !PERSONALIZATION_HAS_LETTER.test(capped)) return null;
|
|
889
|
-
return capped;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function parsePackShareArgs(rawArgs) {
|
|
893
|
-
const args = [...rawArgs];
|
|
894
|
-
const forName = takeValue(args, '--for');
|
|
895
|
-
const daysValue = takeValue(args, '--days');
|
|
896
|
-
const list = takeFlag(args, '--list');
|
|
897
|
-
const revoke = takeFlag(args, '--revoke', { optionalValue: true });
|
|
898
|
-
const slug = args.shift();
|
|
899
|
-
if (!slug) throw new Error('pack share needs a slug: atris pack share <slug> --for "<Name>"');
|
|
900
|
-
if (args.length) throw new Error(`unknown pack share argument: ${args.join(' ')}`);
|
|
901
|
-
assertPublishableSlug(slug);
|
|
902
|
-
|
|
903
|
-
if (list) {
|
|
904
|
-
if (forName !== null || daysValue !== null || revoke !== false) {
|
|
905
|
-
throw new Error('pack share --list cannot be combined with --for, --days, or --revoke');
|
|
906
|
-
}
|
|
907
|
-
return { mode: 'list', slug };
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (revoke !== false) {
|
|
911
|
-
if (forName !== null || daysValue !== null) {
|
|
912
|
-
throw new Error('pack share --revoke cannot be combined with --for or --days');
|
|
913
|
-
}
|
|
914
|
-
if (typeof revoke === 'string' && !SHARE_NONCE_RULE.test(revoke)) {
|
|
915
|
-
throw new Error('--revoke link id must be a 22-character base64url value');
|
|
916
|
-
}
|
|
917
|
-
return {
|
|
918
|
-
mode: 'revoke',
|
|
919
|
-
slug,
|
|
920
|
-
...(typeof revoke === 'string' ? { nonce: revoke } : {}),
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (forName === null) {
|
|
925
|
-
if (daysValue !== null) throw new Error('pack share --days requires --for "<Name>"');
|
|
926
|
-
return { mode: 'plain', slug };
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
const recipientLabel = String(forName).trim();
|
|
930
|
-
if (!recipientLabel || recipientLabel.length > MAX_SHARE_RECIPIENT_LENGTH) {
|
|
931
|
-
throw new Error(
|
|
932
|
-
`--for needs a recipient label between 1 and ${MAX_SHARE_RECIPIENT_LENGTH} characters`,
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
const expiresInDays = daysValue === null ? DEFAULT_SHARE_DAYS : Number(daysValue);
|
|
937
|
-
if (!Number.isSafeInteger(expiresInDays) || expiresInDays < 1 || expiresInDays > MAX_SHARE_DAYS) {
|
|
938
|
-
throw new Error(`--days must be a whole number from 1 to ${MAX_SHARE_DAYS}`);
|
|
939
|
-
}
|
|
940
|
-
return { mode: 'mint', slug, recipientLabel, expiresInDays };
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
async function requestRegistryJson(pathname, requestOptions = {}, deps = {}) {
|
|
944
|
-
const request = deps.httpRequest || httpRequest;
|
|
945
|
-
const method = requestOptions.method || 'GET';
|
|
946
|
-
const headers = {
|
|
947
|
-
Accept: 'application/json',
|
|
948
|
-
...(requestOptions.authPurpose
|
|
949
|
-
? requiredAuthHeaders(deps, requestOptions.authPurpose)
|
|
950
|
-
: {}),
|
|
951
|
-
};
|
|
952
|
-
if (requestOptions.sendOrigin) {
|
|
953
|
-
const origin = registryOrigin(deps);
|
|
954
|
-
if (origin) headers.Origin = origin;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
let body;
|
|
958
|
-
if (requestOptions.body !== undefined) {
|
|
959
|
-
headers['Content-Type'] = 'application/json';
|
|
960
|
-
body = JSON.stringify(requestOptions.body);
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
let response;
|
|
964
|
-
try {
|
|
965
|
-
response = await request(registryUrl(pathname, deps), {
|
|
966
|
-
method,
|
|
967
|
-
timeoutMs: REGISTRY_TIMEOUT_MS,
|
|
968
|
-
headers,
|
|
969
|
-
...(body !== undefined ? { body } : {}),
|
|
970
|
-
});
|
|
971
|
-
} catch {
|
|
972
|
-
throw new Error(requestOptions.unreachableMessage || 'could not reach pack registry. check your connection and try again.');
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
if (response.status < 200 || response.status >= 300) {
|
|
976
|
-
const error = new Error(responseErrorText(response, `registry request failed with status ${response.status}`));
|
|
977
|
-
error.status = response.status;
|
|
978
|
-
throw error;
|
|
979
|
-
}
|
|
980
|
-
const parsed = parseJsonBody(response.body);
|
|
981
|
-
if (!parsed.data || typeof parsed.data !== 'object') {
|
|
982
|
-
throw new Error(requestOptions.invalidMessage || 'pack registry returned an invalid response');
|
|
983
|
-
}
|
|
984
|
-
return parsed.data;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
async function mintPackShare(slug, recipientLabel, expiresInDays, deps = {}) {
|
|
988
|
-
const data = await requestRegistryJson(
|
|
989
|
-
`/api/pack/registry/${encodeURIComponent(slug)}/share`,
|
|
990
|
-
{
|
|
991
|
-
method: 'POST',
|
|
992
|
-
authPurpose: 'share packs',
|
|
993
|
-
sendOrigin: true,
|
|
994
|
-
body: { recipientLabel, expiresInDays },
|
|
995
|
-
unreachableMessage: 'could not mint a pack share link. check your connection and try again.',
|
|
996
|
-
invalidMessage: 'pack registry did not return a share link',
|
|
997
|
-
},
|
|
998
|
-
deps,
|
|
999
|
-
);
|
|
1000
|
-
if (typeof data.shareUrl !== 'string' || !data.shareUrl.trim()) {
|
|
1001
|
-
throw new Error('pack registry did not return a share link');
|
|
1002
|
-
}
|
|
1003
|
-
if (!Number.isFinite(Number(data.expiresAt))) {
|
|
1004
|
-
throw new Error('pack registry did not return the share link expiry');
|
|
1005
|
-
}
|
|
1006
|
-
return {
|
|
1007
|
-
shareUrl: data.shareUrl,
|
|
1008
|
-
expiresAt: Number(data.expiresAt),
|
|
1009
|
-
...(typeof data.nonce === 'string' && SHARE_NONCE_RULE.test(data.nonce)
|
|
1010
|
-
? { nonce: data.nonce }
|
|
1011
|
-
: {}),
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
async function listPackShares(slug, deps = {}) {
|
|
1016
|
-
const data = await requestRegistryJson(
|
|
1017
|
-
`/api/pack/registry/${encodeURIComponent(slug)}/share`,
|
|
1018
|
-
{
|
|
1019
|
-
authPurpose: 'list personal links for packs',
|
|
1020
|
-
unreachableMessage: 'could not list pack share links. check your connection and try again.',
|
|
1021
|
-
invalidMessage: 'pack share links returned an invalid response',
|
|
1022
|
-
},
|
|
1023
|
-
deps,
|
|
1024
|
-
);
|
|
1025
|
-
if (!Array.isArray(data.links)) {
|
|
1026
|
-
throw new Error('pack share links returned an invalid response');
|
|
1027
|
-
}
|
|
1028
|
-
return data.links;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function revokePackShares(slug, deps = {}) {
|
|
1032
|
-
return requestRegistryJson(
|
|
1033
|
-
`/api/pack/registry/${encodeURIComponent(slug)}/share`,
|
|
1034
|
-
{
|
|
1035
|
-
method: 'DELETE',
|
|
1036
|
-
authPurpose: 'revoke personal links for packs',
|
|
1037
|
-
sendOrigin: true,
|
|
1038
|
-
unreachableMessage: 'could not revoke pack share links. check your connection and try again.',
|
|
1039
|
-
},
|
|
1040
|
-
deps,
|
|
1041
|
-
);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
async function revokePackShare(slug, nonce, deps = {}) {
|
|
1045
|
-
return requestRegistryJson(
|
|
1046
|
-
`/api/pack/registry/${encodeURIComponent(slug)}/share`,
|
|
1047
|
-
{
|
|
1048
|
-
method: 'DELETE',
|
|
1049
|
-
authPurpose: 'revoke a personal link for a pack',
|
|
1050
|
-
sendOrigin: true,
|
|
1051
|
-
body: { nonce },
|
|
1052
|
-
unreachableMessage: 'could not revoke that pack share link. check your connection and try again.',
|
|
1053
|
-
},
|
|
1054
|
-
deps,
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function registryPackItems(payload) {
|
|
1059
|
-
if (Array.isArray(payload)) return payload;
|
|
1060
|
-
if (payload && Array.isArray(payload.packs)) return payload.packs;
|
|
1061
|
-
return [];
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
function packManifestFromItem(item) {
|
|
1065
|
-
if (item && item.manifest && typeof item.manifest === 'object') return item.manifest;
|
|
1066
|
-
return item && typeof item === 'object' ? item : {};
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function isPublicPackPayload(payload, slug) {
|
|
1070
|
-
return registryPackItems(payload).some((item) => {
|
|
1071
|
-
const manifest = packManifestFromItem(item);
|
|
1072
|
-
return manifest.slug === slug
|
|
1073
|
-
&& (manifest.visibility === undefined || manifest.visibility === 'public');
|
|
1074
|
-
});
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
async function registryConfirmsPublicPack(slug, deps = {}) {
|
|
1078
|
-
try {
|
|
1079
|
-
const payload = await requestRegistryJson(
|
|
1080
|
-
'/api/pack/registry',
|
|
1081
|
-
{
|
|
1082
|
-
unreachableMessage: 'could not check public pack visibility',
|
|
1083
|
-
invalidMessage: 'pack registry returned an invalid public listing',
|
|
1084
|
-
},
|
|
1085
|
-
deps,
|
|
1086
|
-
);
|
|
1087
|
-
return isPublicPackPayload(payload, slug);
|
|
1088
|
-
} catch {
|
|
1089
|
-
return false;
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
function legacyPersonalizedShareUrl(slug, recipientLabel, deps = {}) {
|
|
1094
|
-
const name = sanitizePersonalizationName(recipientLabel);
|
|
1095
|
-
if (!name) return null;
|
|
1096
|
-
const base = registryUrl(`/packs/${encodeURIComponent(slug)}`, deps);
|
|
1097
|
-
return `${base}?for=${encodeURIComponent(name)}`;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function formatShareExpiry(expiresAt) {
|
|
1101
|
-
if (expiresAt === undefined || expiresAt === null || expiresAt === '') return 'unknown';
|
|
1102
|
-
const numeric = Number(expiresAt);
|
|
1103
|
-
const date = Number.isFinite(numeric)
|
|
1104
|
-
? new Date(numeric < 1000000000000 ? numeric * 1000 : numeric)
|
|
1105
|
-
: new Date(expiresAt);
|
|
1106
|
-
if (Number.isNaN(date.getTime())) return 'unknown';
|
|
1107
|
-
return date.toISOString().replace('T', ' ').replace('.000Z', ' utc');
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function formatPackShareLinksTable(links) {
|
|
1111
|
-
if (!Array.isArray(links) || !links.length) return '';
|
|
1112
|
-
const rows = links.map((link) => ({
|
|
1113
|
-
label: cleanTableCell(link && link.label, 'unlabeled', 40),
|
|
1114
|
-
expires: formatShareExpiry(link && link.expiresAt),
|
|
1115
|
-
state: link && link.revoked ? 'revoked' : 'active',
|
|
1116
|
-
nonce: cleanTableCell(link && link.nonce, 'unknown', 64),
|
|
1117
|
-
}));
|
|
1118
|
-
const columns = [
|
|
1119
|
-
{ key: 'label', label: 'label' },
|
|
1120
|
-
{ key: 'expires', label: 'expires' },
|
|
1121
|
-
{ key: 'state', label: 'state' },
|
|
1122
|
-
{ key: 'nonce', label: 'nonce' },
|
|
1123
|
-
];
|
|
1124
|
-
const widths = columns.map((column) => Math.max(
|
|
1125
|
-
column.label.length,
|
|
1126
|
-
...rows.map((row) => row[column.key].length),
|
|
1127
|
-
));
|
|
1128
|
-
const render = (row) => columns
|
|
1129
|
-
.map((column, index) => (
|
|
1130
|
-
index === columns.length - 1
|
|
1131
|
-
? row[column.key]
|
|
1132
|
-
: row[column.key].padEnd(widths[index])
|
|
1133
|
-
))
|
|
1134
|
-
.join(' ');
|
|
1135
|
-
return [
|
|
1136
|
-
render(Object.fromEntries(columns.map((column) => [column.key, column.label]))),
|
|
1137
|
-
...rows.map(render),
|
|
1138
|
-
].join('\n');
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
169
|
async function sharePack(rawArgs, cwd = process.cwd(), options = {}) {
|
|
1142
170
|
const parsed = parsePackShareArgs(rawArgs);
|
|
1143
171
|
const deps = options.deps || {};
|
|
@@ -1199,99 +227,6 @@ async function sharePack(rawArgs, cwd = process.cwd(), options = {}) {
|
|
|
1199
227
|
}
|
|
1200
228
|
}
|
|
1201
229
|
|
|
1202
|
-
function browseStarValue(item, payload, slug) {
|
|
1203
|
-
const manifest = packManifestFromItem(item);
|
|
1204
|
-
const summary = payload && payload.summaries && payload.summaries[slug];
|
|
1205
|
-
const mapped = payload && payload.stars && payload.stars[slug];
|
|
1206
|
-
const candidates = [
|
|
1207
|
-
item && item.stars,
|
|
1208
|
-
item && item.starCount,
|
|
1209
|
-
manifest.stars,
|
|
1210
|
-
manifest.starCount,
|
|
1211
|
-
summary && summary.stars,
|
|
1212
|
-
summary && summary.starCount,
|
|
1213
|
-
mapped,
|
|
1214
|
-
];
|
|
1215
|
-
for (const candidate of candidates) {
|
|
1216
|
-
if (Number.isFinite(candidate)) return Number(candidate);
|
|
1217
|
-
if (candidate && typeof candidate === 'object' && Number.isFinite(candidate.count)) {
|
|
1218
|
-
return Number(candidate.count);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
return null;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function cleanTableCell(value, fallback, maxLength) {
|
|
1225
|
-
const clean = String(value === undefined || value === null || value === '' ? fallback : value)
|
|
1226
|
-
.replace(/\s+/g, ' ')
|
|
1227
|
-
.trim();
|
|
1228
|
-
if (clean.length <= maxLength) return clean;
|
|
1229
|
-
return `${clean.slice(0, Math.max(1, maxLength - 3))}...`;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
function browsePriceCents(item) {
|
|
1233
|
-
const manifestPriceCents = item
|
|
1234
|
-
&& item.manifest
|
|
1235
|
-
&& typeof item.manifest === 'object'
|
|
1236
|
-
? item.manifest.priceCents
|
|
1237
|
-
: undefined;
|
|
1238
|
-
if (manifestPriceCents !== undefined && manifestPriceCents !== null) {
|
|
1239
|
-
return manifestPriceCents;
|
|
1240
|
-
}
|
|
1241
|
-
return item && typeof item === 'object' ? item.priceCents : undefined;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
function packBrowseRows(payload, { mine = false, limit = PACK_BROWSE_LIMIT } = {}) {
|
|
1245
|
-
return registryPackItems(payload).slice(0, limit).map((item) => {
|
|
1246
|
-
const manifest = packManifestFromItem(item);
|
|
1247
|
-
const priceCents = browsePriceCents(item);
|
|
1248
|
-
return {
|
|
1249
|
-
slug: cleanTableCell(manifest.slug, 'unknown', 40),
|
|
1250
|
-
title: cleanTableCell(manifest.title || manifest.name, manifest.slug || 'untitled', 42),
|
|
1251
|
-
version: cleanTableCell(manifest.version, 'unknown', 20),
|
|
1252
|
-
...(mine
|
|
1253
|
-
? { visibility: cleanTableCell(manifest.visibility, 'public', 10) }
|
|
1254
|
-
: {}),
|
|
1255
|
-
price: Number(priceCents) > 0 ? formatSalesDollars(priceCents) : 'free',
|
|
1256
|
-
stars: browseStarValue(item, payload, manifest.slug),
|
|
1257
|
-
};
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
function formatPackBrowseTable(payload, options = {}) {
|
|
1262
|
-
const mine = Boolean(options.mine);
|
|
1263
|
-
const rows = packBrowseRows(payload, {
|
|
1264
|
-
mine,
|
|
1265
|
-
limit: options.limit || PACK_BROWSE_LIMIT,
|
|
1266
|
-
});
|
|
1267
|
-
if (!rows.length) return '';
|
|
1268
|
-
|
|
1269
|
-
const showPrice = rows.some((row) => row.price !== 'free');
|
|
1270
|
-
const showStars = rows.some((row) => row.stars !== null);
|
|
1271
|
-
const columns = [
|
|
1272
|
-
{ key: 'slug', label: 'slug' },
|
|
1273
|
-
{ key: 'title', label: 'title' },
|
|
1274
|
-
{ key: 'version', label: 'version' },
|
|
1275
|
-
...(mine ? [{ key: 'visibility', label: 'visibility' }] : []),
|
|
1276
|
-
...(showPrice ? [{ key: 'price', label: 'price' }] : []),
|
|
1277
|
-
...(showStars ? [{ key: 'stars', label: 'stars' }] : []),
|
|
1278
|
-
];
|
|
1279
|
-
const widths = columns.map((column) => Math.max(
|
|
1280
|
-
column.label.length,
|
|
1281
|
-
...rows.map((row) => String(row[column.key] === null ? '-' : row[column.key]).length),
|
|
1282
|
-
));
|
|
1283
|
-
const render = (row) => columns
|
|
1284
|
-
.map((column, index) => {
|
|
1285
|
-
const value = row[column.key] === null ? '-' : String(row[column.key]);
|
|
1286
|
-
return index === columns.length - 1 ? value : value.padEnd(widths[index]);
|
|
1287
|
-
})
|
|
1288
|
-
.join(' ');
|
|
1289
|
-
return [
|
|
1290
|
-
render(Object.fromEntries(columns.map((column) => [column.key, column.label]))),
|
|
1291
|
-
...rows.map(render),
|
|
1292
|
-
].join('\n');
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
230
|
async function browsePacks(rawArgs, cwd = process.cwd(), options = {}) {
|
|
1296
231
|
const args = [...rawArgs];
|
|
1297
232
|
const mine = takeFlag(args, '--mine');
|
|
@@ -1329,77 +264,6 @@ async function browsePacks(rawArgs, cwd = process.cwd(), options = {}) {
|
|
|
1329
264
|
// ── pack sales ──────────────────────────────────────────────────────────────
|
|
1330
265
|
const PACK_SALES_LOGIN_NUDGE = 'not logged in. run atris login first to view pack sales.';
|
|
1331
266
|
|
|
1332
|
-
function packSalesUrl(apiBaseUrl = getApiBaseUrl()) {
|
|
1333
|
-
return `${String(apiBaseUrl || '').replace(/\/+$/, '')}/pack/purchases/sales`;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
function formatSalesDollars(priceCents) {
|
|
1337
|
-
const cents = Math.round(Number(priceCents));
|
|
1338
|
-
if (!Number.isFinite(cents)) return '$0';
|
|
1339
|
-
const sign = cents < 0 ? '-' : '';
|
|
1340
|
-
const absoluteCents = Math.abs(cents);
|
|
1341
|
-
const dollars = Math.floor(absoluteCents / 100).toLocaleString('en-US');
|
|
1342
|
-
const remainder = absoluteCents % 100;
|
|
1343
|
-
return `${sign}$${dollars}${remainder ? `.${String(remainder).padStart(2, '0')}` : ''}`;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
function formatPackSaleDate(value) {
|
|
1347
|
-
const date = new Date(value);
|
|
1348
|
-
if (Number.isNaN(date.getTime())) return 'unknown';
|
|
1349
|
-
return new Intl.DateTimeFormat('en-US', {
|
|
1350
|
-
month: 'short',
|
|
1351
|
-
day: 'numeric',
|
|
1352
|
-
timeZone: 'UTC',
|
|
1353
|
-
}).format(date);
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
function isPackTransactionRefunded(item) {
|
|
1357
|
-
return Boolean(item && item.refunded === true);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
function formatPackTransactionsTable(items, personKey, personLabel) {
|
|
1361
|
-
const rows = items.map((item) => ({
|
|
1362
|
-
pack: cleanTableCell(item && item.slug, 'unknown', 40),
|
|
1363
|
-
person: cleanTableCell(item && item[personKey], 'unknown', 40),
|
|
1364
|
-
price: formatSalesDollars(item && item.price_cents),
|
|
1365
|
-
date: formatPackSaleDate(item && item.granted_at),
|
|
1366
|
-
status: isPackTransactionRefunded(item) ? 'refunded' : '',
|
|
1367
|
-
}));
|
|
1368
|
-
if (!rows.length) return '';
|
|
1369
|
-
|
|
1370
|
-
const showStatus = rows.some((row) => row.status === 'refunded');
|
|
1371
|
-
const columns = [
|
|
1372
|
-
{ key: 'pack', label: 'pack' },
|
|
1373
|
-
{ key: 'person', label: personLabel },
|
|
1374
|
-
{ key: 'price', label: 'price' },
|
|
1375
|
-
{ key: 'date', label: 'date' },
|
|
1376
|
-
...(showStatus ? [{ key: 'status', label: 'status' }] : []),
|
|
1377
|
-
];
|
|
1378
|
-
const widths = columns.map((column) => Math.max(
|
|
1379
|
-
column.label.length,
|
|
1380
|
-
...rows.map((row) => row[column.key].length),
|
|
1381
|
-
));
|
|
1382
|
-
const render = (row) => columns
|
|
1383
|
-
.map((column, index) => (
|
|
1384
|
-
index === columns.length - 1
|
|
1385
|
-
? row[column.key]
|
|
1386
|
-
: row[column.key].padEnd(widths[index])
|
|
1387
|
-
))
|
|
1388
|
-
.join(' ');
|
|
1389
|
-
return [
|
|
1390
|
-
render(Object.fromEntries(columns.map((column) => [column.key, column.label]))),
|
|
1391
|
-
...rows.map(render),
|
|
1392
|
-
].join('\n');
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
function formatPackSalesTable(sales) {
|
|
1396
|
-
return formatPackTransactionsTable(sales, 'buyer', 'buyer');
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
function formatPackPurchasesTable(purchases) {
|
|
1400
|
-
return formatPackTransactionsTable(purchases, 'seller', 'seller');
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
267
|
async function showPackSales(rawArgs, cwd = process.cwd(), options = {}) {
|
|
1404
268
|
const args = [...rawArgs];
|
|
1405
269
|
if (args.length) throw new Error(`unknown pack sales argument: ${args.join(' ')}`);
|
|
@@ -1451,10 +315,6 @@ async function showPackSales(rawArgs, cwd = process.cwd(), options = {}) {
|
|
|
1451
315
|
// ── pack purchases ──────────────────────────────────────────────────────────
|
|
1452
316
|
const PACK_PURCHASES_LOGIN_NUDGE = 'not logged in. run atris login first to view pack purchases.';
|
|
1453
317
|
|
|
1454
|
-
function packPurchasesUrl(apiBaseUrl = getApiBaseUrl()) {
|
|
1455
|
-
return `${String(apiBaseUrl || '').replace(/\/+$/, '')}/pack/purchases/mine`;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
318
|
async function showPackPurchases(rawArgs, cwd = process.cwd(), options = {}) {
|
|
1459
319
|
const args = [...rawArgs];
|
|
1460
320
|
if (args.length) throw new Error(`unknown pack purchases argument: ${args.join(' ')}`);
|
|
@@ -1503,102 +363,6 @@ async function showPackPurchases(rawArgs, cwd = process.cwd(), options = {}) {
|
|
|
1503
363
|
return 0;
|
|
1504
364
|
}
|
|
1505
365
|
|
|
1506
|
-
function formatBytes(value) {
|
|
1507
|
-
const bytes = Number(value) || 0;
|
|
1508
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
1509
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1510
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
// What a stranger actually receives, in one glance. The tree below is the
|
|
1514
|
-
// detail; this is the shape, enough to see that the wiki, the skills, the
|
|
1515
|
-
// members and the policies are all still in the box.
|
|
1516
|
-
function printPacketComposition(entries) {
|
|
1517
|
-
const byTop = new Map();
|
|
1518
|
-
for (const entry of entries) {
|
|
1519
|
-
const parts = entry.name.split('/');
|
|
1520
|
-
const inner = parts[0] === 'atris' ? parts.slice(1) : parts;
|
|
1521
|
-
const top = inner.length > 1 ? `${inner[0]}/` : '(root)';
|
|
1522
|
-
byTop.set(top, (byTop.get(top) || 0) + 1);
|
|
1523
|
-
}
|
|
1524
|
-
console.log('composition:');
|
|
1525
|
-
for (const [top, count] of [...byTop.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))) {
|
|
1526
|
-
console.log(` ${top.padEnd(16)} ${count}`);
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function printPacketTree(entries) {
|
|
1531
|
-
console.log('tree:');
|
|
1532
|
-
if (entries.length <= 200) {
|
|
1533
|
-
for (const entry of entries) console.log(` ${entry.name}`);
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
const byDir = new Map();
|
|
1537
|
-
for (const entry of entries) {
|
|
1538
|
-
const dir = entry.name.includes('/') ? `${entry.name.slice(0, entry.name.lastIndexOf('/'))}/` : './';
|
|
1539
|
-
const current = byDir.get(dir) || { files: 0, bytes: 0 };
|
|
1540
|
-
current.files += 1;
|
|
1541
|
-
current.bytes += entry.data.length;
|
|
1542
|
-
byDir.set(dir, current);
|
|
1543
|
-
}
|
|
1544
|
-
console.log(` (${entries.length} files across ${byDir.size} folders, rolled up by folder)`);
|
|
1545
|
-
for (const [dir, stats] of [...byDir.entries()].sort()) {
|
|
1546
|
-
console.log(` ${dir} ${stats.files} files ${formatBytes(stats.bytes)}`);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function printSkipped(skipped) {
|
|
1551
|
-
if (!skipped.length) return;
|
|
1552
|
-
const byReason = new Map();
|
|
1553
|
-
for (const item of skipped) {
|
|
1554
|
-
const list = byReason.get(item.reason) || [];
|
|
1555
|
-
list.push(item.path);
|
|
1556
|
-
byReason.set(item.reason, list);
|
|
1557
|
-
}
|
|
1558
|
-
console.log('skipped:');
|
|
1559
|
-
for (const [reason, paths] of [...byReason.entries()].sort((a, b) => b[1].length - a[1].length)) {
|
|
1560
|
-
console.log(` ${reason}: ${paths.length}`);
|
|
1561
|
-
for (const item of paths.slice(0, 5)) console.log(` ${item}`);
|
|
1562
|
-
if (paths.length > 5) console.log(` ... ${paths.length - 5} more`);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
function printPacketSummary(manifest, entries, skipped, zipBytes) {
|
|
1567
|
-
const unpacked = entries.reduce((total, entry) => total + entry.data.length, 0);
|
|
1568
|
-
console.log(`pack ${manifest.slug} ${manifest.version}`);
|
|
1569
|
-
console.log(` files ${entries.length} (limit ${REGISTRY_LIMITS.maxEntries})`);
|
|
1570
|
-
console.log(` unpacked ${formatBytes(unpacked)} (limit ${formatBytes(REGISTRY_LIMITS.maxUnpackedBytes)})`);
|
|
1571
|
-
console.log(` zip ${formatBytes(zipBytes)} (limit ${formatBytes(REGISTRY_LIMITS.maxZipBytes)})`);
|
|
1572
|
-
printPacketComposition(entries);
|
|
1573
|
-
printPacketTree(entries);
|
|
1574
|
-
printSkipped(skipped);
|
|
1575
|
-
return { unpacked, zipBytes, files: entries.length };
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
function registryLimitFailures(entries, zipBytes) {
|
|
1579
|
-
const unpacked = entries.reduce((total, entry) => total + entry.data.length, 0);
|
|
1580
|
-
const failures = [];
|
|
1581
|
-
if (entries.length > REGISTRY_LIMITS.maxEntries) {
|
|
1582
|
-
failures.push(`entry count: ${entries.length} files exceeds the ${REGISTRY_LIMITS.maxEntries} file limit`);
|
|
1583
|
-
}
|
|
1584
|
-
if (unpacked > REGISTRY_LIMITS.maxUnpackedBytes) {
|
|
1585
|
-
failures.push(`unpacked size: ${formatBytes(unpacked)} exceeds the ${formatBytes(REGISTRY_LIMITS.maxUnpackedBytes)} unpacked limit`);
|
|
1586
|
-
}
|
|
1587
|
-
if (zipBytes > REGISTRY_LIMITS.maxZipBytes) {
|
|
1588
|
-
failures.push(`zip size: ${formatBytes(zipBytes)} exceeds the ${formatBytes(REGISTRY_LIMITS.maxZipBytes)} zip limit`);
|
|
1589
|
-
}
|
|
1590
|
-
return failures;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
function reportSecretFindings(findings) {
|
|
1594
|
-
console.error(`refusing to publish: found ${findings.length} credential-shaped match${findings.length === 1 ? '' : 'es'}.`);
|
|
1595
|
-
for (const finding of findings.slice(0, 20)) {
|
|
1596
|
-
console.error(` ${finding.file}:${finding.line} ${finding.label} ${finding.redacted}`);
|
|
1597
|
-
}
|
|
1598
|
-
if (findings.length > 20) console.error(` ... ${findings.length - 20} more`);
|
|
1599
|
-
console.error('remove the credentials, or re-run with --allow-secrets if every match is a false positive.');
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
366
|
async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
|
|
1603
367
|
const args = [...rawArgs];
|
|
1604
368
|
const dirArg = takeValue(args, '--dir');
|
|
@@ -1713,134 +477,6 @@ async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
|
|
|
1713
477
|
return 0;
|
|
1714
478
|
}
|
|
1715
479
|
|
|
1716
|
-
function shellQuote(value) {
|
|
1717
|
-
const text = String(value || '.');
|
|
1718
|
-
if (/^[A-Za-z0-9_./:-]+$/.test(text)) return text;
|
|
1719
|
-
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
function fallbackSlugFromZipPath(zipPath) {
|
|
1723
|
-
return slugify(path.basename(zipPath, path.extname(zipPath)), 'atris-pack');
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
async function loadZipPayload(source, cwd, deps = {}) {
|
|
1727
|
-
const request = deps.httpRequest || httpRequest;
|
|
1728
|
-
if (/^https?:\/\//i.test(source)) {
|
|
1729
|
-
const response = await request(source, { method: 'GET' });
|
|
1730
|
-
if (response.status < 200 || response.status >= 300) {
|
|
1731
|
-
throw new Error(`download failed with status ${response.status}`);
|
|
1732
|
-
}
|
|
1733
|
-
const urlPath = new URL(source).pathname;
|
|
1734
|
-
return {
|
|
1735
|
-
buffer: response.body,
|
|
1736
|
-
fallbackSlug: fallbackSlugFromZipPath(urlPath || 'atris-pack.zip'),
|
|
1737
|
-
sourceType: 'url',
|
|
1738
|
-
sourceUrl: source,
|
|
1739
|
-
};
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
const localPath = path.resolve(cwd, source);
|
|
1743
|
-
if (fs.existsSync(localPath) || source.toLowerCase().endsWith('.zip')) {
|
|
1744
|
-
if (!fs.existsSync(localPath)) throw new Error(`zip file not found: ${source}`);
|
|
1745
|
-
return {
|
|
1746
|
-
buffer: fs.readFileSync(localPath),
|
|
1747
|
-
fallbackSlug: fallbackSlugFromZipPath(localPath),
|
|
1748
|
-
sourceType: 'file',
|
|
1749
|
-
sourcePath: localPath,
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
const slug = slugify(source);
|
|
1754
|
-
const buffer = await fetchRegistryZipForUser(slug, deps);
|
|
1755
|
-
if (!buffer) return null;
|
|
1756
|
-
return { buffer, fallbackSlug: slug, sourceType: 'registry', sourceSlug: slug };
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
function canonicalZipEntryName(name) {
|
|
1760
|
-
return path.posix.normalize(String(name || '').replace(/\\/g, '/'));
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
function parseManifest(entries) {
|
|
1764
|
-
const manifestEntries = entries.filter((entry) => (
|
|
1765
|
-
canonicalZipEntryName(entry.name) === 'pack.json'
|
|
1766
|
-
&& !String(entry.name || '').replace(/\\/g, '/').endsWith('/')
|
|
1767
|
-
));
|
|
1768
|
-
if (manifestEntries.length === 0) {
|
|
1769
|
-
throw new Error('pack archive is missing root pack.json');
|
|
1770
|
-
}
|
|
1771
|
-
if (manifestEntries.length > 1) {
|
|
1772
|
-
throw new Error('pack archive contains duplicate pack.json entries');
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
const manifestEntry = manifestEntries[0];
|
|
1776
|
-
let parsed;
|
|
1777
|
-
try {
|
|
1778
|
-
parsed = JSON.parse(manifestEntry.data.toString('utf8'));
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
throw new Error(`pack archive has invalid pack.json: ${error.message}`);
|
|
1781
|
-
}
|
|
1782
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1783
|
-
throw new Error('pack archive pack.json must contain an object');
|
|
1784
|
-
}
|
|
1785
|
-
if (typeof parsed.slug !== 'string' || !parsed.slug.trim()) {
|
|
1786
|
-
throw new Error('pack archive pack.json is missing slug');
|
|
1787
|
-
}
|
|
1788
|
-
return parsed;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
function manifestVersion(manifest) {
|
|
1792
|
-
return manifest.version
|
|
1793
|
-
|| (Array.isArray(manifest.versions) && manifest.versions[0] && manifest.versions[0].version)
|
|
1794
|
-
|| 'unknown';
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function upstreamStatePath(packDir) {
|
|
1798
|
-
return path.join(packDir, '.upstream', 'STATE.json');
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
function packStatePath(packDir) {
|
|
1802
|
-
return path.join(packDir, '.atris', 'state', 'pack.json');
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
function remoteCheckTime(deps = {}) {
|
|
1806
|
-
const value = typeof deps.now === 'function' ? deps.now() : new Date();
|
|
1807
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
1808
|
-
return date.toISOString();
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
function recordRemoteCheck(packDir, details, deps = {}) {
|
|
1812
|
-
const statePath = packStatePath(packDir);
|
|
1813
|
-
const existing = readJson(statePath);
|
|
1814
|
-
const state = {
|
|
1815
|
-
...(existing && typeof existing === 'object' ? existing : {}),
|
|
1816
|
-
slug: details.slug,
|
|
1817
|
-
origin: details.origin,
|
|
1818
|
-
remoteVersion: details.remoteVersion,
|
|
1819
|
-
lastRemoteCheckAt: remoteCheckTime(deps),
|
|
1820
|
-
};
|
|
1821
|
-
writeJson(statePath, state);
|
|
1822
|
-
return state;
|
|
1823
|
-
}
|
|
1824
|
-
|
|
1825
|
-
function buildUpstreamState(slug, localVersion, remoteVersion) {
|
|
1826
|
-
return {
|
|
1827
|
-
slug,
|
|
1828
|
-
localVersion,
|
|
1829
|
-
remoteVersion,
|
|
1830
|
-
pulledAt: new Date().toISOString(),
|
|
1831
|
-
};
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
function writeUpstreamState(packDir, state) {
|
|
1835
|
-
writeJson(upstreamStatePath(packDir), state);
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
function hasStagedUpstream(packDir) {
|
|
1839
|
-
const upstreamDir = path.join(packDir, '.upstream');
|
|
1840
|
-
if (!fs.existsSync(upstreamDir)) return false;
|
|
1841
|
-
return fs.readdirSync(upstreamDir).some((name) => name !== 'STATE.json');
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
480
|
function writeUpstreamZip(entries, packDir, state, writes = null) {
|
|
1845
481
|
const upstreamDir = path.join(packDir, '.upstream');
|
|
1846
482
|
const plannedWrites = writes || planZipWrites(entries, upstreamDir);
|