@idleware/ringi 0.7.0 → 0.7.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idleware/ringi",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Distributed agent coordinator",
5
5
  "os": [
6
6
  "darwin",
@@ -9,7 +9,7 @@ export declare class GitStore extends Marks implements Store {
9
9
  home: string;
10
10
  signing?: SigningContext;
11
11
  }, contract: Record<string, string>, text: string): GitStore;
12
- static rootFor(hubDir: string, d: Descriptor, home: string, signing?: SigningContext): string;
12
+ static rootFor(hubDir: string, d: Descriptor, home: string, signing: SigningContext): string;
13
13
  logIds(room: StoredRoom): string[];
14
14
  logHead(room: StoredRoom, id: string): string | null;
15
15
  messages(room: StoredRoom, id: string, head: string): StoredMessage[];
@@ -1,7 +1,7 @@
1
1
  import type { Handle, Room } from './types.ts';
2
- export declare const join: (H: Handle, R: Room, who: string, home: string) => {
2
+ export declare const join: (H: Handle, R: Room, who: string) => {
3
3
  publicKey: string;
4
4
  fingerprint: string;
5
5
  admitted: boolean;
6
6
  };
7
- export declare const roomAdmit: (H: Handle, R: Room, who: string, publicKey: string, home: string) => string;
7
+ export declare const roomAdmit: (H: Handle, R: Room, who: string, publicKey: string) => string;
@@ -3,5 +3,5 @@ export { checkHeartbeat, heartbeatObject } from './heartbeat-object.ts';
3
3
  export { heartbeatBody } from './heartbeat-facts.ts';
4
4
  export { HEARTBEAT_NAME, heartbeatName, heartbeatRef } from './heartbeat-name.ts';
5
5
  export declare const verifyHeartbeat: (H: Handle, R: Room, ref: string, sha: string) => string | null;
6
- type HeartbeatArgs = [H: Handle, R: Room, who: string, target: string, text: string, home: string];
7
- export declare const makeHeartbeat: (...[H, R, who, target, text, home]: HeartbeatArgs) => string;
6
+ type HeartbeatArgs = [H: Handle, R: Room, who: string, target: string, text: string];
7
+ export declare const makeHeartbeat: (...[H, R, who, target, text]: HeartbeatArgs) => string;
@@ -3,11 +3,11 @@ export declare const parseKey: (value: unknown) => Key;
3
3
  export declare const keyInput: (value: string) => Key;
4
4
  export declare const rootKey: (H: Handle) => Key;
5
5
  export declare const signerLine: (who: string, key: Key) => string;
6
- export declare const identityKey: (H: Handle, who: string, home: string, create?: boolean) => Key;
7
- export declare const setupSigned: (H: Handle, home: string, url?: string) => {
6
+ export declare const signingContext: (H: Pick<Handle, 'name' | 'signing'>) => import("../../../core/signing/index.ts").SigningContext;
7
+ export declare const identityKey: (H: Handle, who: string, create?: boolean) => Key;
8
+ export declare const setupSigned: (H: Handle, url?: string) => {
8
9
  root: string;
9
10
  allowedSigners: string;
10
11
  };
11
- export declare const sign: (H: Handle, who: string, home: string) => string[];
12
12
  export declare const rootRoster: (H: Handle) => string;
13
13
  export declare const checkUrl: (url: string) => void;
@@ -2,7 +2,7 @@ export type { Handle, Room, Branch } from './remote/types.ts';
2
2
  export { withVerification } from './remote/cache.ts';
3
3
  export { gitEnv, gitTimeout, checkGitTimeout } from './remote/process.ts';
4
4
  export { isRemote } from './remote/git.ts';
5
- export { parseKey, setupSigned, sign, rootRoster } from './remote/keys.ts';
5
+ export { parseKey, setupSigned, rootRoster } from './remote/keys.ts';
6
6
  export { verifyHub } from './remote/main.ts';
7
7
  export { readCommits } from './remote/commits.ts';
8
8
  export { branch, verifyRoom, verify } from './remote/branch.ts';
@@ -1,4 +1,4 @@
1
1
  import type { Handle } from './remote/types.ts';
2
- export declare const signBytes: (H: Handle, who: string, payload: Buffer | string, home: string) => string;
3
- export declare const signCommit: (H: Handle, who: string, sha: string, home: string) => string;
4
- export declare const signedTag: (H: Handle, who: string, tag: string, target: string, home: string) => string;
2
+ export declare const signBytes: (H: Handle, who: string, payload: Buffer | string) => string;
3
+ export declare const signCommit: (H: Handle, who: string, sha: string) => string;
4
+ export declare const signedTag: (H: Handle, who: string, tag: string, target: string) => string;
@@ -17,6 +17,7 @@ export declare const create: (name: string, { signed, url }?: {
17
17
  }) => {
18
18
  H: Hub;
19
19
  rootKey: string | null;
20
+ remote: string | null;
20
21
  add: string | null;
21
22
  };
22
23
  export declare const use: (name: string) => void;
@@ -1,3 +1,4 @@
1
+ export declare const withoutCredentials: (text: string) => string;
1
2
  export declare class Fail extends Error {
2
3
  }
3
4
  export declare const fail: (msg: string) => never;
@@ -22,10 +22,13 @@ import { CONTROL_FILES } from './branches.js';
22
22
  import { Marks } from './marks.js';
23
23
  export { runGit } from './run.js';
24
24
  import { TEXT_MAX, checkOutgoing } from '../../core/hub/protocol/index.js';
25
+ import { signingContext } from './remote/keys.js';
25
26
  export class GitStore extends Marks {
26
27
  type = 'git';
27
28
  static create(hubDir, d, connection, contract, text) {
28
29
  const { home, signing } = typeof connection === 'string' ? { home: connection, signing: undefined } : connection;
30
+ if (d.trust === 'signed')
31
+ signingContext({ name: d.name, signing });
29
32
  const dir = path.join(hubDir, d.store.path, '.git');
30
33
  // A signed hub's repository already exists: rootFor made it with the root key.
31
34
  if (!fs.existsSync(dir)) {
@@ -43,10 +46,11 @@ export class GitStore extends Marks {
43
46
  }
44
47
  // A new signed hub: the root key is generated on this machine and pinned in the descriptor.
45
48
  static rootFor(hubDir, d, home, signing) {
49
+ signingContext({ name: d.name, signing });
46
50
  const dir = path.join(hubDir, d.store.path, '.git');
47
51
  mkdirIn(home, path.dirname(dir));
48
52
  init(dir, home);
49
- return Remote.setupSigned({ name: d.name, dir, d, signing }, home, d.store.remote).root;
53
+ return Remote.setupSigned({ name: d.name, dir, d, signing }, d.store.remote).root;
50
54
  }
51
55
  logIds(room) { return [...new Set(this.live(room).map(e => e.author))]; }
52
56
  logHead(room, id) { return this.live(room).filter(e => e.author === id).at(-1)?.sha ?? null; }
@@ -114,8 +118,8 @@ export class GitStore extends Marks {
114
118
  .map(b => /^\d+ blob [0-9a-f]+ +(\d+)\t(.*)$/s.exec(b.toString('utf8'))).filter((x) => !!x)
115
119
  .filter(([, , p]) => wanted.has(p)).map(([, n, p]) => [p, Number(n)]));
116
120
  }
117
- admit(room, who, key) { return Remote.roomAdmit(this.h, this.remote(room), who, key, this.home); }
118
- joinKey(room, who) { return Remote.join(this.h, this.remote(room), who, this.home); }
121
+ admit(room, who, key) { return Remote.roomAdmit(this.h, this.remote(room), who, key); }
122
+ joinKey(room, who) { return Remote.join(this.h, this.remote(room), who); }
119
123
  // A room's transport alarm is the room's, whichever author the operator forgets.
120
124
  forgetLog(room, _id) { Remote.forget(this.h, this.remote(room)); }
121
125
  rootRoster() { return Remote.rootRoster(this.h); }
@@ -2,7 +2,7 @@
2
2
  // one mutable tag `<agent>.heartbeat.<genesis12>` per agent and room, replaced on every beat.
3
3
  import * as Remote from './remote.js';
4
4
  import { signedTag } from './signing.js';
5
- import { ZERO, frames, ident, runGitRaw } from './run.js';
5
+ import { ZERO, frames, runGitRaw } from './run.js';
6
6
  import { signalTags, tagObject } from './tags.js';
7
7
  import { Branches } from './branches.js';
8
8
  import { fail, note } from '../../lib/index.js';
@@ -15,7 +15,7 @@ export class Marks extends Branches {
15
15
  heartbeat(room, who, target, text) {
16
16
  const ref = Remote.heartbeatRef(who, room.genesis), name = ref.slice('refs/tags/'.length);
17
17
  const old = this.rev(`${ref}^{tag}`) ? this.git(['rev-parse', ref]).trim() : null;
18
- const obj = this.trust === 'signed' ? Remote.makeHeartbeat(this.h, this.remote(room), who, target, text, this.home)
18
+ const obj = this.trust === 'signed' ? Remote.makeHeartbeat(this.h, this.remote(room), who, target, text)
19
19
  : this.git(['mktag'], { input: `object ${target}\ntype commit\ntag ${name}\ntagger ${who} <${who}@ringi> ${Math.floor(Date.now() / 1000)} +0000\n\n${text}` }).trim();
20
20
  if (this.gitOk(['update-ref', ref, obj, old ?? ZERO]).status !== 0)
21
21
  fail(`${name} moved meanwhile; the next beat writes it`);
@@ -45,13 +45,7 @@ export class Marks extends Branches {
45
45
  Remote.push(this.h, ref);
46
46
  return;
47
47
  }
48
- if (this.h.signing)
49
- this.git(['update-ref', ref, signedTag(this.h, who, tag, target, this.home), ZERO]);
50
- else {
51
- const sign = Remote.sign(this.h, who, this.home);
52
- const key = sign[0]?.slice(2);
53
- this.git([...(key ? ['-c', `user.signingkey=${key}`] : []), 'tag', key ? '-s' : '-a', '-F', '-', tag, target], { input: '', env: ident(who) });
54
- }
48
+ this.git(['update-ref', ref, signedTag(this.h, who, tag, target), ZERO]);
55
49
  Remote.push(this.h, ref);
56
50
  }
57
51
  // The tags named `<actor>.<signal>.<message12>`, with the objects they name; a heartbeat is no signal (heartbeats()).
@@ -3,13 +3,13 @@ import * as C from '../../../lib/index.js';
3
3
  import * as P from '../../../core/hub/protocol/index.js';
4
4
  import { branch, verifyRoom } from './branch.js';
5
5
  import { git, id, ident, isRemote, locked, rev } from './git.js';
6
- import { keyInput, identityKey, sign, signerLine } from './keys.js';
6
+ import { keyInput, identityKey, signerLine } from './keys.js';
7
7
  import { signCommit } from '../signing.js';
8
8
  import { push } from './push.js';
9
9
  import { CONTROL_FILES, ROOT } from './types.js';
10
- export const join = (H, R, who, home) => {
10
+ export const join = (H, R, who) => {
11
11
  const room = verifyRoom(H, R);
12
- const key = locked(H, () => identityKey(H, who, home, true));
12
+ const key = locked(H, () => identityKey(H, who, true));
13
13
  const admitted = !!room.entries.find(e => e.principal === who && e.key.line === key.line);
14
14
  return { publicKey: key.line, fingerprint: key.fp, admitted };
15
15
  };
@@ -30,14 +30,14 @@ const treeWith = (H, base, control, patches) => {
30
30
  return git(H, ['mktree'], { input: [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, line]) => line).join('\n') + '\n' }).trim();
31
31
  };
32
32
  // The root's control record admitting a member, on top of the room's branch: the key and the member go in together.
33
- export const roomAdmit = (H, R, who, publicKey, home) => {
33
+ export const roomAdmit = (H, R, who, publicKey) => {
34
34
  const key = admissionKey(H, R, who, publicKey);
35
35
  for (let attempt = 0; attempt < 8; attempt++) {
36
36
  const tip = rev(H, R.ref) ?? C.fail('room branch is missing');
37
37
  const room = branch(H, R, tip), head = room.control.at(-1);
38
38
  if (room.entries.some(e => e.principal === who || e.key.fp === key.fp))
39
39
  C.fail('principal or key is already admitted; key rotation is not implemented');
40
- const { sha, members } = admissionCommit({ H, R, who, home, tip, head, room, key });
40
+ const { sha, members } = admissionCommit({ H, R, who, tip, head, room, key });
41
41
  if (git(H, ['update-ref', R.ref, sha, tip], { ok: true }).status !== 0)
42
42
  continue;
43
43
  if (!push(H, R.ref, { behind: true }))
@@ -48,14 +48,14 @@ export const roomAdmit = (H, R, who, publicKey, home) => {
48
48
  }
49
49
  return C.fail('room kept moving while admitting; retry');
50
50
  };
51
- const admissionCommit = ({ H, who, home, tip, head, room, key }) => {
51
+ const admissionCommit = ({ H, who, tip, head, room, key }) => {
52
52
  const members = { ...room.members, [who]: { role: 'member', since: new Date().toISOString() } };
53
53
  const roster = [...room.entries.map(e => signerLine(e.principal, e.key)), signerLine(who, key)].join('');
54
54
  const tree = treeWith(H, tip, head, { 'members.json': JSON.stringify(members, null, 2) + '\n', allowed_signers: roster });
55
- const sha = git(H, ['commit-tree', ...(H.signing ? [] : sign(H, ROOT, home)), tree, '-p', tip], {
55
+ const sha = git(H, ['commit-tree', tree, '-p', tip], {
56
56
  input: P.compose(`admit ${who}`, [['To', '*'], ['Kind', 'room'], ['Key', key.fp]]), env: ident(ROOT),
57
57
  }).trim();
58
- return { sha: signCommit(H, ROOT, sha, home), members };
58
+ return { sha: signCommit(H, ROOT, sha), members };
59
59
  };
60
60
  const admissionKey = (H, R, who, publicKey) => {
61
61
  id(who);
@@ -24,9 +24,9 @@ export const verifyHeartbeat = (H, R, ref, sha) => {
24
24
  return reason(e);
25
25
  }
26
26
  };
27
- // Keep the transport's existing call contract while object construction has one named input.
28
- export const makeHeartbeat = (...[H, R, who, target, text, home]) => createHeartbeat({ H, R, who, target, text, home });
29
- const createHeartbeat = ({ H, R, who, target, text, home }) => {
27
+ // Object construction has one named input; signing needs no custody path.
28
+ export const makeHeartbeat = (...[H, R, who, target, text]) => createHeartbeat({ H, R, who, target, text });
29
+ const createHeartbeat = ({ H, R, who, target, text }) => {
30
30
  const ref = heartbeatRef(who, R.genesis);
31
31
  if (!SHA.test(target))
32
32
  C.fail('heartbeat target must be a full commit id');
@@ -35,7 +35,7 @@ const createHeartbeat = ({ H, R, who, target, text, home }) => {
35
35
  const body = `Room: ${R.genesis}\n${text.trimEnd()}\n`;
36
36
  heartbeatBody(body);
37
37
  const raw = `object ${target}\ntype commit\ntag ${ref.slice(TAGS.length)}\ntagger ${who} <${who}@ringi> ${Math.floor(Date.now() / 1000)} +0000\n\n${body}`;
38
- const signature = signBytes(H, who, raw, home);
38
+ const signature = signBytes(H, who, raw);
39
39
  const sha = git(H, ['mktag'], { input: raw + signature }).trim();
40
40
  if (isRemote(H))
41
41
  checkHeartbeat(H, R, ref, sha);
@@ -1,10 +1,8 @@
1
1
  // keys: public keys as given and pinned, a signed hub's root and its signers, and which key signs for a principal.
2
2
  import crypto from 'node:crypto';
3
3
  import fs from 'node:fs';
4
- import path from 'node:path';
5
4
  import * as C from '../../../lib/index.js';
6
- import { config, id, isRemote, mkdir, rev } from './git.js';
7
- import { command } from './process.js';
5
+ import { id, rev } from './git.js';
8
6
  import { ROOT } from './types.js';
9
7
  export const parseKey = (value) => {
10
8
  const text = String(value || '').trim();
@@ -20,47 +18,24 @@ export const parseKey = (value) => {
20
18
  export const keyInput = (value) => parseKey(fs.existsSync(value) && fs.statSync(value).isFile() ? fs.readFileSync(value, 'utf8') : value);
21
19
  export const rootKey = (H) => parseKey(H.d.root || C.fail(`hub ${H.name} has no out-of-band root pin; use hub add --root <public-key|file>`));
22
20
  export const signerLine = (who, key) => `${who} namespaces="git" ${key.line}\n`;
23
- const keyPath = (H, who, home) => path.join(home, 'keys', id(H.name), id(who));
24
- const localKey = (H, who, home, create = false) => {
25
- const file = keyPath(H, who, home);
26
- if (!fs.existsSync(file)) {
27
- if (!create)
28
- C.fail(`no private key for ${who} on this machine; ${who === ROOT ? 'root keys are never obtained from a hub' : 'join here to generate one, then ask the root to admit its public key'}`);
29
- mkdir(path.dirname(file));
30
- command('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', `${who}@ringi`, '-f', file]);
31
- }
32
- // Derive from the private key instead of trusting a mutable .pub sidecar.
33
- const key = parseKey(command('ssh-keygen', ['-y', '-f', file]));
34
- return { ...key, file };
21
+ export const signingContext = (H) => H.signing ?? C.fail(`hub ${H.name}: signed writes require an injected Signer`);
22
+ export const identityKey = (H, who, create = false) => {
23
+ const context = signingContext(H);
24
+ return parseKey(context.signer.publicIdentity(context.resolve(id(H.name), id(who), create)).key);
35
25
  };
36
- export const identityKey = (H, who, home, create = false) => H.signing
37
- ? parseKey(H.signing.signer.publicIdentity(H.signing.resolve(id(H.name), id(who), create)).key)
38
- : localKey(H, who, home, create);
39
- export const setupSigned = (H, home, url) => {
26
+ export const setupSigned = (H, url) => {
40
27
  id(H.name);
41
28
  if (rev(H, 'refs/heads/main'))
42
29
  C.fail('setupSigned only initializes an empty hub; it cannot replace an existing trust anchor');
43
- const key = identityKey(H, ROOT, home, true);
30
+ const key = identityKey(H, ROOT, true);
44
31
  H.d.trust = 'signed';
45
32
  H.d.root = key.line;
46
- config(H, 'gpg.format', 'ssh');
47
- config(H, 'gpg.ssh.program', 'ssh-keygen');
48
33
  if (url) {
49
34
  checkUrl(url);
50
35
  H.d.store.remote = url;
51
36
  }
52
37
  return { root: key.line, allowedSigners: signerLine(ROOT, key) };
53
38
  };
54
- export const sign = (H, who, home) => {
55
- if (!isRemote(H))
56
- return [];
57
- const key = localKey(H, who, home);
58
- if (who === ROOT && key.line !== rootKey(H).line)
59
- C.fail('the local operator key does not match this hub\'s pinned root');
60
- config(H, 'gpg.format', 'ssh');
61
- config(H, 'gpg.ssh.program', 'ssh-keygen');
62
- return [`-S${key.file}`];
63
- };
64
39
  export const rootRoster = (H) => signerLine(ROOT, rootKey(H));
65
40
  const supportedUrl = (url) => {
66
41
  const malformed = /[\r\n\0]/.test(url) || url.startsWith('-');
@@ -1,7 +1,7 @@
1
1
  export { withVerification } from './remote/cache.js';
2
2
  export { gitEnv, gitTimeout, checkGitTimeout } from './remote/process.js';
3
3
  export { isRemote } from './remote/git.js';
4
- export { parseKey, setupSigned, sign, rootRoster } from './remote/keys.js';
4
+ export { parseKey, setupSigned, rootRoster } from './remote/keys.js';
5
5
  export { verifyHub } from './remote/main.js';
6
6
  export { readCommits } from './remote/commits.js';
7
7
  export { branch, verifyRoom, verify } from './remote/branch.js';
@@ -8,6 +8,7 @@ import { mkdirIn } from '../fs/index.js';
8
8
  import * as Remote from './remote.js';
9
9
  import { ZERO, ident, runGit, runGitOk } from './run.js';
10
10
  import { signCommit } from './signing.js';
11
+ import { signingContext } from './remote/keys.js';
11
12
  // The store is a repository whose work tree holds the contract (`main`) as it is now, so Git clients (SourceTree, IDEs)
12
13
  // open `store/` without errors, on the contract, with every room's branch beside it; `git status` stays clean.
13
14
  export const init = (dir, home) => runGit(['init', '-q', '-b', 'main', path.dirname(dir)], { where: home });
@@ -24,6 +25,8 @@ export class Repo {
24
25
  this.trust = d.trust;
25
26
  this.dir = path.join(hubDir, d.store.path, '.git');
26
27
  this.h = { name: d.name, dir: this.dir, d, ...(d.trust === 'signed' && signing ? { signing } : {}) };
28
+ if (this.trust === 'signed')
29
+ signingContext(this.h);
27
30
  if (!fs.existsSync(this.dir))
28
31
  fail(`hub ${d.name}: no git store at ${this.dir}`);
29
32
  this.openable();
@@ -82,8 +85,8 @@ export class Repo {
82
85
  }
83
86
  // Every commit is made here: on a signed hub, signed by its author's key on this machine.
84
87
  commit(who, text, tree, parents) {
85
- const sha = this.git(['commit-tree', ...(this.h.signing ? [] : Remote.sign(this.h, who, this.home)), tree, ...parents.flatMap(p => ['-p', p])], { input: text, env: ident(who) }).trim();
86
- return signCommit(this.h, who, sha, this.home);
88
+ const sha = this.git(['commit-tree', tree, ...parents.flatMap(p => ['-p', p])], { input: text, env: ident(who) }).trim();
89
+ return signCommit(this.h, who, sha);
87
90
  }
88
91
  advance(ref, sha, old) {
89
92
  if (this.gitOk(['update-ref', ref, sha, old ?? ZERO]).status !== 0)
@@ -1,18 +1,13 @@
1
1
  // GitSigningBridge: Signer signs the exact unsigned Git object. Tags append its SSH armor; commits fold it into
2
- // gpgsig. Custody is injected; only legacy direct entrypoints retain their previous file-key signing default.
2
+ // gpgsig. All signing and public identity come from the injected Signer; custody stays outside the transport.
3
3
  import { fail } from '../../lib/index.js';
4
4
  import { git, id, ident, isRemote } from './remote/git.js';
5
- import { identityKey, parseKey, rootKey, sign } from './remote/keys.js';
6
- import { command } from './remote/process.js';
5
+ import { identityKey, parseKey, rootKey, signingContext } from './remote/keys.js';
7
6
  import { checkSignature } from './remote/signature.js';
8
- export const signBytes = (H, who, payload, home) => {
7
+ export const signBytes = (H, who, payload) => {
9
8
  if (!isRemote(H))
10
9
  return '';
11
- if (!H.signing) {
12
- const file = sign(H, who, home)[0].slice(2);
13
- return command('ssh-keygen', ['-Y', 'sign', '-n', 'git', '-f', file], { input: payload });
14
- }
15
- const context = H.signing, key = context.resolve(id(H.name), id(who));
10
+ const context = signingContext(H), key = context.resolve(id(H.name), id(who));
16
11
  const identity = context.signer.publicIdentity(key), publicKey = parseKey(identity.key);
17
12
  if (who === 'operator' && publicKey.line !== rootKey(H).line)
18
13
  fail('the local operator key does not match this hub\'s pinned root');
@@ -21,24 +16,24 @@ export const signBytes = (H, who, payload, home) => {
21
16
  fail('the signer returned an incompatible signature identity or format');
22
17
  return Buffer.from(signature.bytes).toString('utf8');
23
18
  };
24
- // The legacy constructor path already asked Git to sign; only an injected provider uses the neutral byte bridge.
25
- export const signCommit = (H, who, sha, home) => {
26
- if (!H.signing || !isRemote(H))
19
+ export const signCommit = (H, who, sha) => {
20
+ if (!isRemote(H))
27
21
  return sha;
28
22
  const raw = git(H, ['cat-file', 'commit', sha], { raw: true }), end = raw.indexOf('\n\n');
29
23
  if (end < 0)
30
24
  fail('malformed unsigned commit');
31
- const signature = signBytes(H, who, raw, home).trimEnd().replace(/\n/g, '\n ');
25
+ const signature = signBytes(H, who, raw).trimEnd().replace(/\n/g, '\n ');
32
26
  const body = Buffer.concat([raw.subarray(0, end), Buffer.from(`\ngpgsig ${signature}\n\n`), raw.subarray(end + 2)]);
33
27
  const signed = git(H, ['hash-object', '-t', 'commit', '-w', '--stdin'], { input: body }).trim();
34
- checkSignature(H, signed, who, identityKey(H, who, home));
28
+ checkSignature(H, signed, who, identityKey(H, who));
35
29
  return signed;
36
30
  };
37
- export const signedTag = (H, who, tag, target, home) => {
31
+ export const signedTag = (H, who, tag, target) => {
38
32
  const tagger = git(H, ['var', 'GIT_COMMITTER_IDENT'], { env: ident(who) }).trim();
39
33
  const raw = `object ${target}\ntype commit\ntag ${tag}\ntagger ${tagger}\n\n`;
40
- const signed = git(H, ['mktag'], { input: raw + signBytes(H, who, raw, home) }).trim();
41
- checkSignature(H, signed, who, identityKey(H, who, home), 'tag');
34
+ const signed = git(H, ['mktag'], { input: raw + signBytes(H, who, raw) }).trim();
35
+ if (isRemote(H))
36
+ checkSignature(H, signed, who, identityKey(H, who), 'tag');
42
37
  return signed;
43
38
  };
44
39
  //# sourceMappingURL=signing.js.map
@@ -1,12 +1,12 @@
1
1
  // transport: the git store as a store type: a hub's repository is `store/.git` in the hub's directory; a signed hub's
2
- // root key is made here (remote/keys.ts) and a published hub is replicated by bootstrap (remote/bootstrap.ts).
3
- import path from 'node:path';
2
+ // root identity comes from Signer and a published hub is replicated by bootstrap (remote/bootstrap.ts).
3
+ import { fail } from '../../lib/index.js';
4
4
  import { GitStore } from './index.js';
5
5
  import * as Remote from './remote.js';
6
6
  const context = (d, opt) => d.trust === 'signed'
7
- ? typeof opt.signer === 'function' ? opt.signer() : opt.signer
7
+ ? (typeof opt.signer === 'function' ? opt.signer() : opt.signer) ?? fail('signed hubs require an injected Signer')
8
8
  : undefined;
9
- // `home`: where this machine keeps its state; the git store keeps its keys and scratch indexes there.
9
+ // `home`: where this machine keeps its state and scratch indexes; Signer owns key custody.
10
10
  export const gitTransport = (home, opt = {}) => ({
11
11
  // One branch per room with its messages and control records, signals as tags on them (hub-1, Layout).
12
12
  layout: 'git/2',
@@ -15,13 +15,13 @@ export const gitTransport = (home, opt = {}) => ({
15
15
  open: (at, d) => new GitStore(at, d, home, context(d, opt)),
16
16
  create: (at, d, genesis) => {
17
17
  const signing = context(d, opt);
18
- if (d.trust === 'signed')
18
+ if (signing)
19
19
  d.root = GitStore.rootFor(at, d, home, signing);
20
20
  const g = genesis(d);
21
21
  const store = GitStore.create(at, d, { home, signing }, g.files, g.text);
22
22
  if (d.trust === 'signed')
23
23
  d.genesis = store.contractHead();
24
- const custody = d.trust !== 'signed' ? null : signing ? signing.custody?.(d.name, 'operator') ?? null : path.join(home, 'keys', d.name, 'operator');
24
+ const custody = signing?.custody?.(d.name, 'operator') ?? null;
25
25
  return { store, custody };
26
26
  },
27
27
  attach: (name, url, pins) => { Remote.hubAdd({ home, name, url, ...pins }); },
@@ -14,7 +14,7 @@ export const createHub = (o) => {
14
14
  const r = Hubs.create(arg ?? '', { signed: flag(o, 'signed'), url: opt(o, 'url') });
15
15
  if (!r.add)
16
16
  return out(`created hub ${arg} (${r.H.dir})`);
17
- return out(`created signed hub ${arg}${r.H.d.store.remote ? `, published to ${r.H.d.store.remote}` : ''}. Its root key stays on this machine (${r.rootKey}).
17
+ return out(`created signed hub ${arg}${r.remote ? `, published to ${r.remote}` : ''}. Its root key stays on this machine (${r.rootKey}).
18
18
  Give every machine that joins it, out of band:
19
19
  ${r.add}`);
20
20
  };
@@ -2,8 +2,10 @@
2
2
  import { DEFAULT_PROFILE, PROFILES_CAPABILITY, parseProfile } from '../work/index.js';
3
3
  import { OPERATOR, config, createHub, hubNames, openHub, roomNames, setConfig, transportOf } from '../rooms/index.js';
4
4
  import { hubPorts } from '../ports/index.js';
5
- import { fail, strictJson, words } from '../../../lib/index.js';
5
+ import { fail, strictJson, withoutCredentials, words } from '../../../lib/index.js';
6
6
  import { ID_RE, compose } from '../protocol/index.js';
7
+ // A remote as shown and handed to other machines: never with a password or token in it; each machine has its own.
8
+ const shown = (url) => url ? withoutCredentials(url) : null;
7
9
  // The operator's terminal context picks the hub unless one is named.
8
10
  export const hubOf = (name) => openHub(name ?? config().hub);
9
11
  export const hubs = () => {
@@ -14,7 +16,7 @@ export const hubs = () => {
14
16
  return hubNames().map(n => {
15
17
  const H = openHub(n);
16
18
  return { name: H.name, current: H.name === cur, store: H.d.store.type, trust: H.contract.trust, contract: H.contract.version,
17
- rooms: roomNames(H).length, remote: H.d.store.remote ?? null, dir: H.dir };
19
+ rooms: roomNames(H).length, remote: shown(H.d.store.remote), dir: H.dir };
18
20
  });
19
21
  };
20
22
  // A new hub; a signed one makes its root key here and returns what other machines need to pin it.
@@ -22,9 +24,9 @@ export const create = (name, { signed = false, url } = {}) => {
22
24
  if (!ID_RE.test(name))
23
25
  fail('usage: ringi hub create <name> [--signed [--url <git url>]]');
24
26
  const { custody } = createHub(name, { signed, url });
25
- const H = openHub(name);
26
- return { H, rootKey: custody,
27
- add: H.signed ? `ringi hub add ${name} ${H.d.store.remote ?? '<url>'} --root '${H.d.root}' --genesis ${H.d.genesis}` : null };
27
+ const H = openHub(name), remote = shown(H.d.store.remote);
28
+ return { H, rootKey: custody, remote,
29
+ add: H.signed ? `ringi hub add ${name} ${remote ?? '<url>'} --root '${H.d.root}' --genesis ${H.d.genesis}` : null };
28
30
  };
29
31
  export const use = (name) => {
30
32
  openHub(name);
@@ -1,7 +1,12 @@
1
1
  // fail: a refusal ringi explains (Fail, shown as its message), apart from a bug (any other error, shown with its stack).
2
+ // A URL's credentials never shown, however git or a person wrote them: everything before its host goes, up to the last
3
+ // `@` (a token may be the whole user, or hold an `@`), except an SSH login name, which is no secret.
4
+ const USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/?#'"]*)@/gi;
5
+ export const withoutCredentials = (text) => text.replace(USERINFO, (_, scheme, user) => /^ssh:/i.test(scheme) && !user.includes(':') ? `${scheme}${user}@` : scheme);
2
6
  export class Fail extends Error {
3
7
  }
4
- export const fail = (msg) => { throw new Fail(msg); };
8
+ // Every refusal passes here, git's own diagnostics included, so none shows a URL's credentials.
9
+ export const fail = (msg) => { throw new Fail(withoutCredentials(msg)); };
5
10
  export const errno = (e) => e?.code;
6
11
  // A write the host refused: what to do is the same for every host; a tool's own way (its sandbox settings) is added
7
12
  // by the interface that knows the tools (cli.ts).
@@ -1,7 +1,8 @@
1
1
  // lib: helpers that know nothing of ringi and touch no host: failing (fail.ts), notes, formatting, strict JSON
2
2
  // (json.ts). Files and locks are the adapters' (adapters/fs).
3
- // Notes and alarms go where the program sends them (composition/: stderr). Those written before it says where wait
4
- // for it, so none is lost to the order modules load in.
3
+ import { withoutCredentials } from './fail.js';
4
+ // Notes and alarms go where the program sends them (composition/: stderr), never with a URL's credentials. Those
5
+ // written before it says where wait for it, so none is lost to the order modules load in.
5
6
  const notes = [];
6
7
  let sink = null;
7
8
  export const notesTo = (fn) => {
@@ -10,7 +11,7 @@ export const notesTo = (fn) => {
10
11
  fn(line);
11
12
  };
12
13
  export const note = (s) => {
13
- const line = `ringi: ${s}\n`;
14
+ const line = `ringi: ${withoutCredentials(s)}\n`;
14
15
  if (sink)
15
16
  sink(line);
16
17
  else