atris 3.79.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/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 { apiRequestJson, getApiBaseUrl, getAppBaseUrl, httpRequest } = require('../utils/api');
11
- const { loadCredentials, performTokenRefresh } = require('../utils/auth');
12
- const { createZipBuffer, readZipBuffer, ZIP_LIMITS } = require('../lib/zip');
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
- const REGISTRY_TIMEOUT_MS = 60000;
34
- const PACK_RUN_INPUT_MAX_BYTES = 256 * 1024;
35
- const PACK_VISIBILITIES = new Set(['public', 'unlisted', 'private']);
36
- const DEFAULT_SHARE_DAYS = 30;
37
- const MAX_SHARE_DAYS = 365;
38
- const MAX_SHARE_RECIPIENT_LENGTH = 120;
39
- const SHARE_NONCE_RULE = /^[A-Za-z0-9_-]{22}$/;
40
- const PACK_BROWSE_LIMIT = 50;
41
-
42
- // The web registry caps every upload (atrisos-web app/api/pack/registry/route.ts).
43
- // The ZIP reader uses the same limits so inbound packs cannot expand past them.
44
- const REGISTRY_LIMITS = ZIP_LIMITS;
45
-
46
- // The web viewer rejects anything else (atrisos-web app/lib/pack/manifests.ts).
47
- const SLUG_RULE = /^[a-z0-9-]{3,40}$/;
48
-
49
- // ── The packet allowlist ────────────────────────────────────────────────────
50
- // A packet is the shareable knowledge spine of a workspace, not the folder it
51
- // lives in. Only what a stranger could read and learn from ships: the map and
52
- // readme layer, member and team definitions, the wiki, skills, policies, refs,
53
- // features. Runtime exhaust (runs/, logs/, journal/, status/, .atris/),
54
- // dependencies, binaries and lockfiles never ship.
55
- //
56
- // This table is the whole rule. Audit it here, not in scattered ifs.
57
- // (2026-07-27: the denylist that preceded it shipped a 94 MB zip containing
58
- // node_modules, customer folders and a live JWT.)
59
- const PACKET = {
60
- // Folders whose contents ship. Checked at the first level under the pack
61
- // root and under atris/. Anything not listed here is skipped.
62
- directories: [
63
- 'features', // what got built, and why
64
- 'wiki', // ingested knowledge
65
- 'skills', // reusable procedures
66
- 'team', // member and role definitions
67
- 'policies', // house rules
68
- 'refs', // stable reference tables
69
- 'reference',
70
- 'checklists',
71
- 'templates',
72
- 'guides',
73
- 'docs',
74
- 'briefs',
75
- 'playbooks',
76
- ],
77
- // Root-level files ship only as documents: the map, the readme, the persona.
78
- rootExtensions: ['.md', '.markdown', '.txt'],
79
- // ...plus the manifest itself.
80
- rootFiles: ['pack.json'],
81
- // Inside an allowed folder, only these extensions ship. Text only.
82
- extensions: ['.md', '.markdown', '.txt', '.yml', '.yaml', '.json', '.toml', '.csv'],
83
- // Path segments that never ship, at any depth. Runtime exhaust and deps.
84
- deniedSegments: [
85
- 'runs', 'logs', 'journal', 'status', 'missions', 'queue', 'tmp', 'cache',
86
- '.atris', '.git', '.upstream', '.venv', 'venv', 'node_modules',
87
- '__pycache__', 'dist', 'build', 'coverage', '_archive',
88
- // Proof folders are receipts of runs that already happened, not knowledge a
89
- // stranger can use. A packet ships the feature definition, not its history.
90
- // (2026-07-27: features/*/proof was 1,253 files and 76% of the unpacked
91
- // weight of the atrisos-backend workspace.)
92
- 'proof',
93
- // Same class as proof: a receipt records that a run happened. Kept as its
94
- // own entry so the principle stays legible rather than reading as one
95
- // grab-bag of excluded words.
96
- 'receipts',
97
- ],
98
- // Segments that get their own skip reason, because "runtime exhaust" does not
99
- // explain why a folder full of readable markdown was left out.
100
- segmentReasons: {
101
- proof: 'proof artifacts are excluded (receipts of past runs, not knowledge)',
102
- receipts: 'receipts are excluded (records of past runs, not knowledge)',
103
- },
104
-
105
- // ── definitions, not state ────────────────────────────────────────────────
106
- // A packet carries what a thing IS, never what it is currently DOING. The
107
- // definition files travel: MEMBER.md, MISSION.md, SKILL.md, SOUL.md,
108
- // README.md, wiki pages, policies. The running state a live workspace keeps
109
- // beside them stays home: a stranger cannot use someone else's standup notes,
110
- // and project-management exhaust is most of the file count.
111
- //
112
- // The rule is matched on FILENAME, not on folder path, so it generalizes to
113
- // any Atris workspace instead of naming the folders of one.
114
- // (2026-07-27, atrisos-backend/atris: team/ was 939 eligible files of which
115
- // only ~129 were definitions; features/ was 1,183 of which only 43 were
116
- // README.md. The rest were now.md/goals.md, dated journals, and the
117
- // idea/build/validate plan-do-review triplet.)
118
- runningStateNames: [
119
- 'now.md', // what this member/feature is doing right now
120
- 'goals.md', // this workspace's current targets
121
- 'idea.md', // the plan-do-review triplet: an internal tracker, not a
122
- 'build.md', // description of the thing. features/*/README.md is the
123
- 'validate.md', // definition and it still ships.
124
- ],
125
- // Dated journal entries, at any depth: 2026-07-06.md, 2026-07-06-retro.md.
126
- runningStatePatterns: [/^\d{4}-\d{2}-\d{2}(?:[-_][^.]*)?\.[a-z0-9]+$/i],
127
- runningStateReason: 'running state is excluded (a pack carries definitions, not state)',
128
- // Exact filenames that never ship.
129
- deniedNames: [
130
- 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'poetry.lock',
131
- 'cargo.lock', 'uv.lock', '.ds_store',
132
- ],
133
- // Secret-shaped filenames that never ship, at any depth.
134
- deniedPatterns: [
135
- /^credentials/i, /^\.env/i, /\.pem$/i, /\.key$/i, /^id_rsa/i, /^id_ed25519/i,
136
- ],
137
- };
138
-
139
- // Credential shapes. A hit blocks the publish; the match is redacted, never
140
- // echoed, because the point of the scan is to stop the secret from travelling.
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);