atris 3.72.4 → 3.73.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/task.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const http = require('http');
8
+ const crypto = require('crypto');
8
9
  const { execSync } = require('node:child_process');
9
10
  const path = require('path');
10
11
  const os = require('os');
@@ -12482,7 +12483,7 @@ function taskBoardTemplate(model) {
12482
12483
  async function api(path, options = {}) {
12483
12484
  const res = await fetch(path, {
12484
12485
  ...options,
12485
- headers: { 'content-type': 'application/json', ...(options.headers || {}) }
12486
+ headers: { 'content-type': 'application/json', 'x-atris-token': new URLSearchParams(location.search).get('t') || '', ...(options.headers || {}) }
12486
12487
  });
12487
12488
  const data = await res.json();
12488
12489
  if (!res.ok || data.ok === false) throw new Error(data.detail || data.reason || 'request failed');
@@ -12798,18 +12799,30 @@ function taskBoardHtml() {
12798
12799
  }
12799
12800
 
12800
12801
  function readJsonBody(req) {
12802
+ if (String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') {
12803
+ return Promise.reject(Object.assign(new Error('json required'), { statusCode: 415 }));
12804
+ }
12801
12805
  return new Promise((resolve, reject) => {
12802
- let body = '';
12806
+ let size = 0;
12807
+ const chunks = [];
12803
12808
  req.on('data', chunk => {
12804
- body += chunk;
12805
- if (body.length > 1_000_000) {
12806
- reject(new Error('body too large'));
12807
- req.destroy();
12809
+ if (size > 1_000_000) return;
12810
+ size += chunk.length;
12811
+ if (size > 1_000_000) {
12812
+ reject(Object.assign(new Error('body too large'), { statusCode: 413 }));
12813
+ return;
12808
12814
  }
12815
+ chunks.push(chunk);
12809
12816
  });
12810
12817
  req.on('end', () => {
12818
+ if (size > 1_000_000) return;
12819
+ const body = Buffer.concat(chunks).toString('utf8');
12811
12820
  if (!body.trim()) return resolve({});
12812
- try { resolve(JSON.parse(body)); } catch (e) { reject(e); }
12821
+ try {
12822
+ const value = JSON.parse(body);
12823
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('object required');
12824
+ resolve(value);
12825
+ } catch (e) { reject(Object.assign(e, { statusCode: 400 })); }
12813
12826
  });
12814
12827
  req.on('error', reject);
12815
12828
  });
@@ -12818,14 +12831,16 @@ function readJsonBody(req) {
12818
12831
  function sendJson(res, status, value, headers = {}) {
12819
12832
  res.writeHead(status, {
12820
12833
  'content-type': 'application/json; charset=utf-8',
12821
- 'access-control-allow-origin': 'http://localhost',
12834
+ 'cache-control': 'no-store',
12835
+ 'referrer-policy': 'no-referrer',
12836
+ 'x-content-type-options': 'nosniff',
12822
12837
  ...headers,
12823
12838
  });
12824
12839
  res.end(JSON.stringify(value, null, 2));
12825
12840
  }
12826
12841
 
12827
12842
  function sendHtml(res, value) {
12828
- res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
12843
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'referrer-policy': 'no-referrer', 'x-content-type-options': 'nosniff' });
12829
12844
  res.end(value);
12830
12845
  }
12831
12846
 
@@ -13469,8 +13484,24 @@ function matchTaskApiRoute(req, url) {
13469
13484
  return null;
13470
13485
  }
13471
13486
 
13472
- async function handleTaskApi(req, res, taskDb, db) {
13473
- const url = new URL(req.url, 'http://127.0.0.1');
13487
+ async function handleTaskApi(req, res, taskDb, db, token) {
13488
+ let url;
13489
+ try {
13490
+ const target = req.url || '/';
13491
+ url = new URL(target, 'http://127.0.0.1');
13492
+ if (!target.startsWith('/') || target.startsWith('//') || /[\\#\0]/.test(target) || decodeURIComponent(url.pathname).includes('\0')) throw new Error('url');
13493
+ decodeURIComponent(target);
13494
+ } catch {
13495
+ return sendJson(res, 400, { ok: false, reason: 'bad_url' });
13496
+ }
13497
+ const host = String(req.headers.host || '').toLowerCase();
13498
+ const port = req.socket.localPort;
13499
+ if (![`127.0.0.1:${port}`, `localhost:${port}`].includes(host)) return sendJson(res, 403, { ok: false, reason: 'host' });
13500
+ const provided = Buffer.from(String(req.headers['x-atris-token'] || (req.method === 'GET' && url.searchParams.get('t')) || ''));
13501
+ const expected = Buffer.from(token);
13502
+ if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) return sendJson(res, 403, { ok: false, reason: 'token' });
13503
+ if (req.method === 'GET' && (req.headers['transfer-encoding'] || Number(req.headers['content-length'] || 0) > 0)) return sendJson(res, 400, { ok: false, reason: 'body' });
13504
+ if (req.method !== 'GET' && req.headers.origin !== undefined && req.headers.origin !== `http://${host}`) return sendJson(res, 403, { ok: false, reason: 'origin' });
13474
13505
  const matchedRoute = matchTaskApiRoute(req, url);
13475
13506
  if (!matchedRoute) return sendJson(res, 404, { ok: false, reason: 'not_found' });
13476
13507
  return matchedRoute.route.serve({
@@ -13486,26 +13517,32 @@ async function handleTaskApi(req, res, taskDb, db) {
13486
13517
  function createTaskApiServer(taskApi = {}) {
13487
13518
  const taskDb = taskApi.taskDb || getTaskDb();
13488
13519
  const db = taskApi.db || taskDb.open();
13489
- return http.createServer((req, res) => {
13490
- handleTaskApi(req, res, taskDb, db).catch((error) => {
13491
- sendJson(res, 500, { ok: false, reason: 'server_error', detail: String(error && error.message || error) });
13492
- });
13520
+ const token = crypto.randomBytes(32).toString('base64url');
13521
+ const server = http.createServer((req, res) => {
13522
+ handleTaskApi(req, res, taskDb, db, token).catch((error) => {
13523
+ if (res.headersSent || res.destroyed) return res.destroy();
13524
+ const status = error.statusCode || 500;
13525
+ sendJson(res, status, { ok: false, reason: status < 500 ? 'bad_request' : 'server_error' });
13526
+ }).catch(() => res.destroy());
13493
13527
  });
13528
+ server.taskToken = token;
13529
+ return server;
13494
13530
  }
13495
13531
 
13496
13532
  function cmdServe(args) {
13497
13533
  const host = String(flag(args, '--host') || '127.0.0.1');
13534
+ if (!['127.0.0.1', 'localhost'].includes(host)) throw new Error('task board only listens on 127.0.0.1');
13498
13535
  const port = Number(flag(args, '--port') || process.env.PORT || 8787);
13499
13536
  const taskDb = getTaskDb();
13500
13537
  const db = taskDb.open();
13501
13538
  const server = createTaskApiServer({ taskDb, db });
13502
13539
  return new Promise((resolve, reject) => {
13503
13540
  server.on('error', reject);
13504
- server.listen(port, host, () => {
13541
+ server.listen(port, '127.0.0.1', () => {
13505
13542
  const addr = server.address();
13506
13543
  const actualPort = addr && addr.port || port;
13507
- console.log(`Task board: http://${host}:${actualPort}`);
13508
- console.log(`Workspace: ${taskDb.workspaceRoot()}`);
13544
+ console.log(`task board: http://${host}:${actualPort}/?t=${server.taskToken}`);
13545
+ console.log(`workspace: ${taskDb.workspaceRoot()}`);
13509
13546
  });
13510
13547
 
13511
13548
  const shutdown = () => {
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Trust commands for Atris CLI, see who trusts an account and vouch for people.
3
+ *
4
+ * Usage:
5
+ * atris trust Your own trust
6
+ * atris trust <handle> Someone else's trust, and what you share
7
+ * atris trust vouch <handle> Vouch for someone (the same as following them)
8
+ * atris trust unvouch <handle> Take a vouch back
9
+ *
10
+ * <handle> can be @handle, handle, a user id, or an email. A leading @ is
11
+ * stripped before it is sent.
12
+ *
13
+ * Backend: GET /api/trust/me, GET /api/trust/{handle_or_id},
14
+ * POST and DELETE /api/trust/vouch/{handle_or_id}.
15
+ */
16
+
17
+ const { isHelpToken, argsWantHelp } = require('../lib/noninteractive');
18
+ const { cleanTarget, targetLabel, createIo, call, reportError } = require('./pay');
19
+
20
+ function splitFlags(args) {
21
+ const list = Array.isArray(args) ? args : [];
22
+ return {
23
+ json: list.includes('--json'),
24
+ rest: list.filter((a) => a !== '--json'),
25
+ };
26
+ }
27
+
28
+ function yesNo(value) {
29
+ if (value === true) return 'yes';
30
+ if (value === false) return 'no';
31
+ return null;
32
+ }
33
+
34
+ function trustLines(view, isMe) {
35
+ const v = view || {};
36
+ const name = v.handle ? `@${String(v.handle).replace(/^@+/, '')}` : (v.user_id || 'this account');
37
+ const friends = Number(v.friends) || 0;
38
+ const lines = [isMe ? `${name} (you)` : name];
39
+ lines.push(` Trust score ${v.trust_score ?? 0}, from ${friends} ${friends === 1 ? 'friend' : 'friends'}`);
40
+ const trusted = yesNo(v.trusted_with_me);
41
+ if (trusted) lines.push(` Trusted with you: ${trusted}`);
42
+ if (v.shared_friends !== null && v.shared_friends !== undefined) {
43
+ lines.push(` Friends you share: ${v.shared_friends}`);
44
+ }
45
+ return lines;
46
+ }
47
+
48
+ async function view(io, who, json) {
49
+ const isMe = !who;
50
+ const path = isMe ? '/trust/me' : `/trust/${encodeURIComponent(cleanTarget(who))}`;
51
+ const res = await call(io, path);
52
+ if (!res.ok) return reportError(io, res, `Could not load trust (${res.status})`);
53
+ if (json) {
54
+ io.log(JSON.stringify(res.data || {}, null, 2));
55
+ return 0;
56
+ }
57
+ for (const line of trustLines(res.data, isMe)) io.log(line);
58
+ if (isMe && !(Number(res.data && res.data.friends) > 0)) {
59
+ io.log('\nTrust grows when people vouch for you. Find people: atris people <name>');
60
+ }
61
+ return 0;
62
+ }
63
+
64
+ async function vouch(io, who, remove, json) {
65
+ const verb = remove ? 'unvouch' : 'vouch';
66
+ const target = cleanTarget(who);
67
+ if (!target || isHelpToken(target)) {
68
+ io.error(`Usage: atris trust ${verb} <handle>`);
69
+ return 2;
70
+ }
71
+ const res = await call(io, `/trust/vouch/${encodeURIComponent(target)}`, {
72
+ method: remove ? 'DELETE' : 'POST',
73
+ });
74
+ if (!res.ok) return reportError(io, res, `Could not ${verb} (${res.status})`);
75
+ if (json) {
76
+ io.log(JSON.stringify(res.data || {}, null, 2));
77
+ return 0;
78
+ }
79
+ const label = targetLabel(who);
80
+ if (remove) {
81
+ io.log(`You took back your vouch for ${label}.`);
82
+ } else if (res.data && res.data.is_mutual) {
83
+ io.log(`You vouched for ${label}. You vouch for each other now.`);
84
+ } else {
85
+ io.log(`You vouched for ${label}.`);
86
+ }
87
+ return 0;
88
+ }
89
+
90
+ function printHelp(log = console.log) {
91
+ log(`atris trust, who trusts an account and who you vouch for
92
+
93
+ atris trust Your trust
94
+ atris trust <handle> Their trust, and the friends you share
95
+ atris trust vouch <handle> Vouch for someone (same as following them)
96
+ atris trust unvouch <handle> Take your vouch back
97
+
98
+ <handle> is @handle, a user id, or an email.
99
+ Trust score is how many friends an account has (people who follow each other).
100
+ Add --json for the raw answer.
101
+ Pay someone: atris pay <handle> <amount>`);
102
+ }
103
+
104
+ async function trustCommand(argv, ioOverrides) {
105
+ const io = createIo(ioOverrides);
106
+ const { json, rest } = splitFlags(argv || process.argv.slice(3));
107
+ const sub = rest[0];
108
+ if (isHelpToken(sub) || argsWantHelp(rest)) {
109
+ printHelp(io.log);
110
+ return 0;
111
+ }
112
+ if (sub === 'vouch') return vouch(io, rest[1], false, json);
113
+ if (sub === 'unvouch') return vouch(io, rest[1], true, json);
114
+ return view(io, sub, json);
115
+ }
116
+
117
+ module.exports = { trustCommand, trustLines };
package/commands/zen.js CHANGED
@@ -3,9 +3,11 @@
3
3
  const { abandonedDaysFromEnv, checkZen, sideCopyLines } = require('../lib/zen');
4
4
  const { applyZen } = require('../lib/zen-apply');
5
5
  const { workspaceZen } = require('../lib/zen-workspace');
6
+ const { fleetFile, fleetRepos, fleetZen, printFleet } = require('../lib/zen-fleet');
6
7
 
7
8
  function showZenHelp() {
8
9
  console.log('usage: atris zen [--check] [--no-fetch] [--json]');
10
+ console.log(' atris zen --all [repo ...]');
9
11
  console.log('');
10
12
  console.log('Brings this repo to zen: code matches origin, nothing left lying around.');
11
13
  console.log('Old stashes are archived, idle side copies put away, finished branches');
@@ -21,6 +23,9 @@ function showZenHelp() {
21
23
  console.log(' --check Only look. Report what is out of place and change nothing.');
22
24
  console.log(' --no-fetch Use the last fetched origin instead of fetching.');
23
25
  console.log(' --json Print machine-readable results.');
26
+ console.log(' --all Zen every repo you list, one line each. Repos come from the');
27
+ console.log(' command line or ~/.atris/zen.json: {"repos": ["~/code/app"]}.');
28
+ console.log(' A repo with unsaved live data or secrets is skipped for you.');
24
29
  console.log('');
25
30
  console.log('Add more team-state paths in .atris/zen.json: {"team_state": ["logs/"]}');
26
31
  }
@@ -151,6 +156,18 @@ function zenCommand(args = [], cwd = process.cwd()) {
151
156
  const fetch = !args.includes('--no-fetch');
152
157
  const json = args.includes('--json');
153
158
 
159
+ if (args.includes('--all')) {
160
+ const repos = fleetRepos(args.filter((a) => !a.startsWith('--')));
161
+ if (!repos.length) {
162
+ console.log(`zen: no repos listed. Name them (atris zen --all ~/code/app ~/code/site) or list them in ${fleetFile()}.`);
163
+ return 2;
164
+ }
165
+ const results = fleetZen({ repos, fetch });
166
+ if (json) console.log(JSON.stringify(results, null, 2));
167
+ else printFleet(results);
168
+ return results.every((r) => r.state === 'zen' || r.state === 'skipped') ? 0 : 1;
169
+ }
170
+
154
171
  if (args.includes('--check')) {
155
172
  const result = checkZen({ cwd, fetch });
156
173
  if (json) {