@idleware/ringi 0.7.1 → 0.8.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.
Files changed (202) hide show
  1. package/README.md +18 -11
  2. package/package.json +1 -1
  3. package/src/main/resources/skill/ringi/SKILL.md +3 -4
  4. package/target/dts/adapters/{transport-git → store-git}/branches.d.ts +1 -3
  5. package/target/dts/adapters/{transport-git → store-git}/index.d.ts +0 -1
  6. package/target/dts/adapters/store-git/marks.d.ts +21 -0
  7. package/target/dts/adapters/{transport-git → store-git}/remote/push.d.ts +0 -1
  8. package/target/dts/adapters/store-git/remote/stream-check.d.ts +12 -0
  9. package/target/dts/adapters/store-git/remote/stream-control.d.ts +8 -0
  10. package/target/dts/adapters/store-git/remote/stream-facts.d.ts +4 -0
  11. package/target/dts/adapters/store-git/remote/streams.d.ts +31 -0
  12. package/target/dts/adapters/store-git/remote/sync-streams.d.ts +2 -0
  13. package/target/dts/adapters/{transport-git → store-git}/remote/types.d.ts +5 -4
  14. package/target/dts/adapters/{transport-git → store-git}/remote.d.ts +1 -4
  15. package/target/dts/adapters/{transport-git → store-git}/repo.d.ts +3 -2
  16. package/target/dts/adapters/{transport-git → store-git}/signing.d.ts +0 -1
  17. package/target/dts/clients/cli/operator/hub-commands.d.ts +0 -2
  18. package/target/dts/clients/cli/operator/index.d.ts +1 -1
  19. package/target/dts/clients/cli/operator/sessions.d.ts +0 -1
  20. package/target/dts/clients/cli/operator/where-format.d.ts +1 -1
  21. package/target/dts/clients/cli/shared/index.d.ts +2 -2
  22. package/target/dts/clients/cli/shared/messages.d.ts +0 -1
  23. package/target/dts/clients/cli/shared/profile.d.ts +0 -1
  24. package/target/dts/clients/cli/skill/index.d.ts +2 -1
  25. package/target/dts/core/agents/overview.d.ts +4 -7
  26. package/target/dts/core/agents/sessions/index.d.ts +0 -1
  27. package/target/dts/core/agents/sessions/join.d.ts +1 -2
  28. package/target/dts/core/hub/history/index.d.ts +1 -10
  29. package/target/dts/core/hub/ports/store.d.ts +35 -23
  30. package/target/dts/core/hub/rooms/address.d.ts +6 -0
  31. package/target/dts/core/hub/rooms/adoption.d.ts +7 -0
  32. package/target/dts/core/hub/rooms/compat.d.ts +3 -1
  33. package/target/dts/core/hub/rooms/contracts.d.ts +16 -0
  34. package/target/dts/core/hub/rooms/hubs.d.ts +3 -3
  35. package/target/dts/core/hub/rooms/index.d.ts +5 -6
  36. package/target/dts/core/hub/rooms/room-catalogue.d.ts +9 -0
  37. package/target/dts/core/hub/rooms/room-life.d.ts +10 -0
  38. package/target/dts/core/hub/rooms/room-view.d.ts +3 -0
  39. package/target/dts/core/hub/rooms/rooms.d.ts +4 -2
  40. package/target/dts/core/hub/signals/check.d.ts +2 -2
  41. package/target/dts/core/hub/signals/model.d.ts +2 -10
  42. package/target/dts/core/hub/signals/pins.d.ts +2 -2
  43. package/target/dts/core/hub/usecases/config.d.ts +0 -1
  44. package/target/dts/core/hub/usecases/context.d.ts +1 -2
  45. package/target/dts/core/hub/usecases/hubs.d.ts +2 -8
  46. package/target/dts/core/hub/usecases/invite.d.ts +12 -0
  47. package/target/dts/core/hub/usecases/room.d.ts +0 -1
  48. package/target/dts/core/hub/usecases/rooms.d.ts +9 -7
  49. package/target/dts/core/hub/usecases/roster.d.ts +1 -2
  50. package/target/dts/core/hub/work/index.d.ts +0 -1
  51. package/target/dts/index.d.ts +1 -1
  52. package/target/dts/lib/fail.d.ts +1 -0
  53. package/target/esm/adapters/{tool-claude → agent-claude}/index.js +2 -2
  54. package/target/esm/adapters/{tool-codex → agent-codex}/index.js +2 -2
  55. package/target/esm/adapters/{tool-cursor → agent-cursor}/index.js +1 -1
  56. package/target/esm/adapters/{tool-traces → agent-traces}/index.js +1 -1
  57. package/target/esm/adapters/state-fs/index.js +2 -2
  58. package/target/esm/adapters/{transport-git → store-git}/branches.js +21 -27
  59. package/target/esm/adapters/{transport-git → store-git}/index.js +10 -28
  60. package/target/esm/adapters/store-git/marks.js +86 -0
  61. package/target/esm/adapters/{transport-git → store-git}/remote/commits.js +3 -1
  62. package/target/esm/adapters/{transport-git → store-git}/remote/git.js +8 -1
  63. package/target/esm/adapters/{transport-git → store-git}/remote/keys.js +8 -3
  64. package/target/esm/adapters/{transport-git → store-git}/remote/push-retry.js +24 -6
  65. package/target/esm/adapters/store-git/remote/push-validate.js +19 -0
  66. package/target/esm/adapters/{transport-git → store-git}/remote/push.js +13 -14
  67. package/target/esm/adapters/store-git/remote/stream-check.js +44 -0
  68. package/target/esm/adapters/store-git/remote/stream-control.js +33 -0
  69. package/target/esm/adapters/store-git/remote/stream-facts.js +40 -0
  70. package/target/esm/adapters/store-git/remote/streams.js +76 -0
  71. package/target/esm/adapters/store-git/remote/sync-streams.js +28 -0
  72. package/target/esm/adapters/store-git/remote/sync.js +59 -0
  73. package/target/esm/adapters/{transport-git → store-git}/remote/types.js +0 -3
  74. package/target/esm/adapters/{transport-git → store-git}/remote.js +1 -4
  75. package/target/esm/adapters/{transport-git → store-git}/repo.js +42 -4
  76. package/target/esm/adapters/{transport-git → store-git}/signing.js +1 -9
  77. package/target/esm/adapters/store-git/transport.js +60 -0
  78. package/target/esm/clients/cli/agent/index.js +1 -1
  79. package/target/esm/clients/cli/agent/session.js +9 -9
  80. package/target/esm/clients/cli/commands.js +5 -5
  81. package/target/esm/clients/cli/context.js +1 -1
  82. package/target/esm/clients/cli/help.js +14 -20
  83. package/target/esm/clients/cli/operator/heartbeat-format.js +1 -1
  84. package/target/esm/clients/cli/operator/heartbeat.js +1 -2
  85. package/target/esm/clients/cli/operator/hub-commands.js +6 -19
  86. package/target/esm/clients/cli/operator/hub.js +2 -2
  87. package/target/esm/clients/cli/operator/index.js +2 -2
  88. package/target/esm/clients/cli/operator/room.js +25 -32
  89. package/target/esm/clients/cli/operator/sessions.js +4 -22
  90. package/target/esm/clients/cli/operator/where-format.js +2 -1
  91. package/target/esm/clients/cli/operator/where.js +5 -7
  92. package/target/esm/clients/cli/shared/index.js +2 -2
  93. package/target/esm/clients/cli/shared/messages.js +2 -12
  94. package/target/esm/clients/cli/shared/profile.js +1 -6
  95. package/target/esm/clients/cli/skill/index.js +14 -19
  96. package/target/esm/composition/index.js +4 -4
  97. package/target/esm/core/agents/heartbeat/pulse.js +4 -4
  98. package/target/esm/core/agents/overview.js +13 -25
  99. package/target/esm/core/agents/sessions/binding.js +1 -1
  100. package/target/esm/core/agents/sessions/index.js +3 -2
  101. package/target/esm/core/agents/sessions/join.js +17 -9
  102. package/target/esm/core/agents/sessions/resume.js +3 -6
  103. package/target/esm/core/hub/capabilities.js +1 -4
  104. package/target/esm/core/hub/history/index.js +23 -13
  105. package/target/esm/core/hub/ports/store.js +2 -2
  106. package/target/esm/core/hub/rooms/address.js +18 -0
  107. package/target/esm/core/hub/rooms/adoption.js +32 -0
  108. package/target/esm/core/hub/rooms/compat.js +39 -14
  109. package/target/esm/core/hub/rooms/contracts.js +62 -0
  110. package/target/esm/core/hub/rooms/hubs.js +33 -56
  111. package/target/esm/core/hub/rooms/index.js +2 -2
  112. package/target/esm/core/hub/rooms/messages.js +6 -6
  113. package/target/esm/core/hub/rooms/room-catalogue.js +31 -0
  114. package/target/esm/core/hub/rooms/room-life.js +59 -0
  115. package/target/esm/core/hub/rooms/room-view.js +39 -0
  116. package/target/esm/core/hub/rooms/rooms.js +26 -37
  117. package/target/esm/core/hub/signals/check.js +21 -26
  118. package/target/esm/core/hub/signals/model.js +0 -2
  119. package/target/esm/core/hub/signals/pins.js +44 -16
  120. package/target/esm/core/hub/signals/write.js +8 -10
  121. package/target/esm/core/hub/usecases/actor.js +1 -1
  122. package/target/esm/core/hub/usecases/config.js +2 -2
  123. package/target/esm/core/hub/usecases/context.js +5 -8
  124. package/target/esm/core/hub/usecases/hubs.js +18 -33
  125. package/target/esm/core/hub/usecases/invite.js +26 -0
  126. package/target/esm/core/hub/usecases/room.js +4 -5
  127. package/target/esm/core/hub/usecases/rooms.js +40 -35
  128. package/target/esm/core/hub/usecases/roster.js +12 -21
  129. package/target/esm/core/hub/usecases/send.js +10 -6
  130. package/target/esm/core/hub/work/parse.js +1 -1
  131. package/target/esm/lib/fail.js +6 -1
  132. package/target/esm/lib/index.js +4 -3
  133. package/target/dts/adapters/transport-git/marks.d.ts +0 -20
  134. package/target/dts/adapters/transport-git/remote/bootstrap.d.ts +0 -7
  135. package/target/dts/adapters/transport-git/remote/heartbeat-name.d.ts +0 -6
  136. package/target/dts/adapters/transport-git/remote/heartbeat-object.d.ts +0 -10
  137. package/target/dts/adapters/transport-git/remote/heartbeat-push.d.ts +0 -2
  138. package/target/dts/adapters/transport-git/remote/heartbeat.d.ts +0 -7
  139. package/target/dts/adapters/transport-git/remote/signals.d.ts +0 -15
  140. package/target/dts/adapters/transport-git/remote/sync-heartbeat.d.ts +0 -2
  141. package/target/dts/adapters/transport-git/remote/sync-rooms.d.ts +0 -6
  142. package/target/dts/adapters/transport-git/remote/sync-tags.d.ts +0 -8
  143. package/target/dts/adapters/transport-git/tags.d.ts +0 -15
  144. package/target/dts/clients/cli/skill/targets.d.ts +0 -3
  145. package/target/dts/core/hub/usecases/staging.d.ts +0 -8
  146. package/target/esm/adapters/transport-git/marks.js +0 -82
  147. package/target/esm/adapters/transport-git/remote/bootstrap.js +0 -47
  148. package/target/esm/adapters/transport-git/remote/heartbeat-name.js +0 -19
  149. package/target/esm/adapters/transport-git/remote/heartbeat-object.js +0 -40
  150. package/target/esm/adapters/transport-git/remote/heartbeat-push.js +0 -44
  151. package/target/esm/adapters/transport-git/remote/heartbeat.js +0 -44
  152. package/target/esm/adapters/transport-git/remote/push-validate.js +0 -39
  153. package/target/esm/adapters/transport-git/remote/signals.js +0 -63
  154. package/target/esm/adapters/transport-git/remote/sync-heartbeat.js +0 -26
  155. package/target/esm/adapters/transport-git/remote/sync-rooms.js +0 -38
  156. package/target/esm/adapters/transport-git/remote/sync-tags.js +0 -48
  157. package/target/esm/adapters/transport-git/remote/sync.js +0 -38
  158. package/target/esm/adapters/transport-git/tags.js +0 -21
  159. package/target/esm/adapters/transport-git/transport.js +0 -29
  160. package/target/esm/clients/cli/skill/targets.js +0 -20
  161. package/target/esm/core/hub/usecases/staging.js +0 -60
  162. /package/target/dts/adapters/{tool-claude → agent-claude}/index.d.ts +0 -0
  163. /package/target/dts/adapters/{tool-codex → agent-codex}/index.d.ts +0 -0
  164. /package/target/dts/adapters/{tool-cursor → agent-cursor}/index.d.ts +0 -0
  165. /package/target/dts/adapters/{tool-traces → agent-traces}/index.d.ts +0 -0
  166. /package/target/dts/adapters/{transport-git → store-git}/files.d.ts +0 -0
  167. /package/target/dts/adapters/{transport-git → store-git}/remote/admit.d.ts +0 -0
  168. /package/target/dts/adapters/{transport-git → store-git}/remote/alarms.d.ts +0 -0
  169. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-commit.d.ts +0 -0
  170. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-control.d.ts +0 -0
  171. /package/target/dts/adapters/{transport-git → store-git}/remote/branch-scan.d.ts +0 -0
  172. /package/target/dts/adapters/{transport-git → store-git}/remote/branch.d.ts +0 -0
  173. /package/target/dts/adapters/{transport-git → store-git}/remote/cache.d.ts +0 -0
  174. /package/target/dts/adapters/{transport-git → store-git}/remote/commits.d.ts +0 -0
  175. /package/target/dts/adapters/{transport-git → store-git}/remote/forget.d.ts +0 -0
  176. /package/target/dts/adapters/{transport-git → store-git}/remote/git.d.ts +0 -0
  177. /package/target/dts/adapters/{transport-git → store-git}/remote/heartbeat-facts.d.ts +0 -0
  178. /package/target/dts/adapters/{transport-git → store-git}/remote/keys.d.ts +0 -0
  179. /package/target/dts/adapters/{transport-git → store-git}/remote/main.d.ts +0 -0
  180. /package/target/dts/adapters/{transport-git → store-git}/remote/process.d.ts +0 -0
  181. /package/target/dts/adapters/{transport-git → store-git}/remote/push-retry.d.ts +0 -0
  182. /package/target/dts/adapters/{transport-git → store-git}/remote/push-validate.d.ts +0 -0
  183. /package/target/dts/adapters/{transport-git → store-git}/remote/roster.d.ts +0 -0
  184. /package/target/dts/adapters/{transport-git → store-git}/remote/signature.d.ts +0 -0
  185. /package/target/dts/adapters/{transport-git → store-git}/remote/sync.d.ts +0 -0
  186. /package/target/dts/adapters/{transport-git → store-git}/run.d.ts +0 -0
  187. /package/target/dts/adapters/{transport-git → store-git}/transport.d.ts +0 -0
  188. /package/target/esm/adapters/{transport-git → store-git}/files.js +0 -0
  189. /package/target/esm/adapters/{transport-git → store-git}/remote/admit.js +0 -0
  190. /package/target/esm/adapters/{transport-git → store-git}/remote/alarms.js +0 -0
  191. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-commit.js +0 -0
  192. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-control.js +0 -0
  193. /package/target/esm/adapters/{transport-git → store-git}/remote/branch-scan.js +0 -0
  194. /package/target/esm/adapters/{transport-git → store-git}/remote/branch.js +0 -0
  195. /package/target/esm/adapters/{transport-git → store-git}/remote/cache.js +0 -0
  196. /package/target/esm/adapters/{transport-git → store-git}/remote/forget.js +0 -0
  197. /package/target/esm/adapters/{transport-git → store-git}/remote/heartbeat-facts.js +0 -0
  198. /package/target/esm/adapters/{transport-git → store-git}/remote/main.js +0 -0
  199. /package/target/esm/adapters/{transport-git → store-git}/remote/process.js +0 -0
  200. /package/target/esm/adapters/{transport-git → store-git}/remote/roster.js +0 -0
  201. /package/target/esm/adapters/{transport-git → store-git}/remote/signature.js +0 -0
  202. /package/target/esm/adapters/{transport-git → store-git}/run.js +0 -0
@@ -1,15 +1,20 @@
1
- // rooms: the operator's rooms in a hub: list, create, switch, admit and remove members, delete, move to a
2
- // newer contract, and accept a new baseline for a log after reviewing an alarm.
1
+ // rooms: the rooms on this machine, each named uniquely whatever hub keeps it: list, create, switch, admit a member,
2
+ // delete, and accept a new baseline for a log after reviewing an alarm.
3
3
  import crypto from 'node:crypto';
4
4
  import { DEFAULT_PROFILE, parseProfile } from '../work/index.js';
5
- import { hubPorts, locked } from '../ports/index.js';
6
- import { OPERATOR, RESERVED_ROOMS, ROOM_MD, config, controlChange, profileText, room as roomOf, roomNames, setConfig } from '../rooms/index.js';
5
+ import { hubPorts } from '../ports/index.js';
6
+ import { OPERATOR, RESERVED_ROOMS, ROOM_MD, config, createRoom, deleteRoom, hubNames, openContract, openHub, profileText, room as roomOf, roomNames, setConfig } from '../rooms/index.js';
7
7
  import { Fail, fail, note } from '../../../lib/index.js';
8
+ import { invite } from './invite.js';
8
9
  import { ID_RE, compose } from '../protocol/index.js';
9
- import { notSigned } from './roster.js';
10
- export { admit, forgetLog, remove } from './roster.js';
11
- // The operator's terminal context picks the room unless one is named.
12
- export const roomIn = (H, name) => roomOf(H, name ?? config().room);
10
+ export { admit, forgetLog } from './roster.js';
11
+ // A room by its name, unique on this machine whatever hub keeps it; without one, the terminal's current room.
12
+ export const roomNamed = (name) => {
13
+ hubPorts().state.ensure();
14
+ const n = name ?? config().room ?? fail('no current room: ringi room create <id> | ringi room switch <id>');
15
+ const hub = hubNames().find(h => roomNames(openHub(h)).includes(n)) ?? fail(`no room ${n} on this machine`);
16
+ return roomOf(openHub(hub), n);
17
+ };
13
18
  // A room the store offers but that does not open (a history under another name) is an alarm, not a row.
14
19
  const opens = (H, n) => {
15
20
  try {
@@ -22,47 +27,47 @@ const opens = (H, n) => {
22
27
  return null;
23
28
  }
24
29
  };
25
- export const rooms = (H) => {
30
+ export const rooms = () => {
31
+ hubPorts().state.ensure();
26
32
  const cur = config().room;
27
- const view = (n, R) => ({ name: n, current: n === cur, members: Object.keys(R.members).length, profile: R.profile.name, genesis: R.genesis });
28
- return roomNames(H).flatMap(n => { const R = opens(H, n); return R ? [view(n, R)] : []; });
33
+ const view = (H, n, R) => ({ name: n, hub: H.name, current: n === cur, members: Object.keys(R.members).length, profile: R.profile.name, genesis: R.genesis });
34
+ return hubNames().map(h => openHub(h)).flatMap(H => roomNames(H).flatMap(n => { const R = opens(H, n); return R ? [view(H, n, R)] : []; }));
29
35
  };
30
36
  // A room carries its profile from the start: later changes to the hub's profiles do not reach it.
31
37
  // `quorum`: the room's work starts by itself once it has that many agents; until then a join waits.
32
- export const create = (H, name, { profile, quorum } = {}) => {
38
+ // `url`: a signed hub publishes the room's repository there; the result says how an agent elsewhere joins it.
39
+ export const create = (H, name, { profile, quorum, url } = {}) => {
33
40
  if (!ID_RE.test(name) || RESERVED_ROOMS.has(name))
34
41
  fail(`usage: ringi room create <id> [--profile <name>] [--quorum <agents>] (an id like fix-badge; not ${[...RESERVED_ROOMS].join(', ')})`);
35
42
  if (quorum !== undefined && !(Number.isSafeInteger(quorum) && quorum >= 1 && quorum <= 99))
36
43
  fail('--quorum takes a number of agents, 1 to 99');
37
- const p = profile ?? H.contract.profile ?? DEFAULT_PROFILE;
44
+ const contract = openContract(H);
45
+ const p = profile ?? contract.contract.profile ?? DEFAULT_PROFILE;
38
46
  const text = profileText(H, p) ?? fail(`hub ${H.name} carries no profile ${p}; add it with ringi hub amend -m "<why>" --attach <file>:profiles/${p}.json`);
39
47
  parseProfile(text, `profile ${p}`);
40
- locked(`hub-${H.name}`, () => {
41
- const files = { 'ROOM.md': ROOM_MD(name), 'members.json': JSON.stringify({ [OPERATOR]: { role: 'root' } }, null, 2) + '\n', 'profile.json': text };
42
- // A signed room's roster: who may sign its logs, starting with the root.
43
- if (H.signed)
44
- files.allowed_signers = H.store.rootRoster?.() ?? fail(`hub ${H.name}: its store cannot start a signed roster`);
45
- // The genesis names the room; a random id makes it unique, so a room deleted and created again is a different room.
46
- H.store.createRoom(name, OPERATOR, compose(`room ${name}`, [['To', '*'], ['Kind', 'room'], ['Room-Id', crypto.randomBytes(16).toString('hex')],
47
- ...(quorum ? [['Quorum', String(quorum)]] : [])]), files);
48
- });
49
- setConfig({ hub: H.name, room: name });
50
- return { profile: p };
48
+ const files = { 'ROOM.md': ROOM_MD(name), 'members.json': JSON.stringify({ [OPERATOR]: { role: 'root' } }, null, 2) + '\n', 'profile.json': text };
49
+ // A signed room's roster: who may sign its logs, starting with the root.
50
+ if (H.signed)
51
+ files.allowed_signers = contract.store.rootRoster?.() ?? fail(`hub ${H.name}: its store cannot start a signed roster`);
52
+ // The genesis names the room; a random id makes it unique, so a room deleted and created again is a different room.
53
+ const genesis = compose(`room ${name}`, [['To', '*'], ['Kind', 'room'], ['Room-Id', crypto.randomBytes(16).toString('hex')],
54
+ ...(quorum ? [['Quorum', String(quorum)]] : [])]);
55
+ const R = createRoom(H, name, { who: OPERATOR, text: genesis, files, remote: url });
56
+ setConfig({ room: name });
57
+ const line = invite(R);
58
+ return { profile: p, join: line ? `ringi join ${line} --as <agent>` : null };
51
59
  };
52
- export const use = (H, name) => {
53
- const R = roomOf(H, name);
54
- setConfig({ hub: H.name, room: R.name });
60
+ export const use = (name) => {
61
+ const R = roomNamed(name);
62
+ setConfig({ room: R.name });
55
63
  return R;
56
64
  };
57
- // The room and everything kept for it: in the store (deleteRoom) and its readers' state. The sessions bound to it,
58
- // on this machine or another, leave it on their own when they find its branch gone (core/agents/sessions).
59
- export const erase = (H, R) => {
60
- notSigned(H, 'delete');
61
- locked(`hub-${H.name}`, () => H.store.deleteRoom(R.s));
62
- hubPorts().state.remove(['readers', H.name, R.genesis]);
65
+ // The room's store on this machine and everything kept for it: its endpoint and its readers' state. Sessions bound to
66
+ // it, here or elsewhere, leave it on their own when they find it gone (core/agents/sessions); a forge's copy is the
67
+ // access adapter's to delete.
68
+ export const erase = (R) => {
69
+ deleteRoom(R);
63
70
  if (config().room === R.name)
64
71
  setConfig({ room: null });
65
72
  };
66
- // The room follows the hub's current contract from now on; nothing written before is reinterpreted.
67
- export const rebase = (H, R) => controlChange(R, OPERATOR, `contract ${H.contract.version}`, () => { }, [['Contract', H.main]]);
68
73
  //# sourceMappingURL=rooms.js.map
@@ -1,27 +1,18 @@
1
- // roster: who belongs to a room and whose history its readers accept: admit a signed member, remove one, and accept a
2
- // new baseline for a log after an alarm.
1
+ // roster: who belongs to a room and whose history its readers accept: admit a signed member, and accept a new
2
+ // baseline for a log after an alarm.
3
3
  import { hubPorts, locked } from '../ports/index.js';
4
- import { OPERATOR, controlChange } from '../rooms/index.js';
4
+ import { OPERATOR } from '../rooms/index.js';
5
5
  import { fail } from '../../../lib/index.js';
6
6
  import { AGENT_RE, ID_RE } from '../protocol/index.js';
7
7
  import { loadState, readerLock, saveState } from '../history/index.js';
8
- // Removing or deleting in a signed hub needs revocation published to every replica: not in this version.
9
- export const notSigned = (H, what) => { if (H.signed)
10
- fail(`room ${what} is not supported on signed hubs yet: a local change would come back with the next fetch`); };
8
+ // An agent's id in a room: `operator` is the root, `main` and `chat` name a room's own branches (hub-2).
9
+ const RESERVED = new Set([OPERATOR, 'main', 'chat']);
10
+ export const agentId = (who) => AGENT_RE.test(who) && !RESERVED.has(who) ? who : fail(`bad agent id ${who}: lowercase letters, digits, - and _, no dot; not ${[...RESERVED].join(', ')}`);
11
11
  export const admit = (H, R, who, key) => {
12
- if (!H.signed || !H.store.admit)
12
+ if (!H.signed || !R.store.admit)
13
13
  fail('room admit is for signed hubs; on a local hub the operator\'s join adds members');
14
- if (!AGENT_RE.test(who))
15
- fail(`bad agent id ${who}: lowercase letters, digits, - and _, no dot`);
16
- locked(`hub-${H.name}`, () => H.store.admit(R.s, who, key));
17
- };
18
- export const remove = (H, R, who) => {
19
- notSigned(H, 'remove');
20
- if (!R.members[who])
21
- fail(`${who} is not a member of ${R.name}`);
22
- if (who === OPERATOR)
23
- fail('the operator stays in every room');
24
- controlChange(R, OPERATOR, `remove ${who}`, m => { delete m[who]; });
14
+ agentId(who);
15
+ locked(`hub-${H.name}`, () => R.store.admit(R.s, who, key));
25
16
  };
26
17
  // One reader forgets the log's accepted head and the alarms about it; whether it had accepted any.
27
18
  const forgetIn = (R, reader, id) => readerLock(R, reader, () => {
@@ -32,7 +23,7 @@ const forgetIn = (R, reader, id) => readerLock(R, reader, () => {
32
23
  st.alarms = (st.alarms ?? []).filter(a => !a.startsWith(`log ${id} `) && !a.startsWith(`log ${id}:`));
33
24
  saveState(R, reader, st);
34
25
  }
35
- R.hub.store.unpin(R.s, reader, id);
26
+ R.store.unpin(R.s, reader, id);
36
27
  return had;
37
28
  });
38
29
  // The operator accepts a new baseline for one log, for every reader of the room, after reviewing an alarm. A signed
@@ -41,7 +32,7 @@ export const forgetLog = (H, R, id) => {
41
32
  if (!ID_RE.test(id))
42
33
  fail('usage: ringi forget <room>/<log>');
43
34
  if (H.signed)
44
- H.store.forgetLog?.(R.s, id);
45
- return hubPorts().state.list(['readers', H.name, R.genesis]).filter(reader => forgetIn(R, reader, id)).length;
35
+ R.store.forgetLog?.(R.s, id);
36
+ return hubPorts().state.list(['readers', H.d.genesis, R.genesis]).filter(reader => forgetIn(R, reader, id)).length;
46
37
  };
47
38
  //# sourceMappingURL=roster.js.map
@@ -1,10 +1,11 @@
1
1
  // send: a message from the actor, its recipients, trailers and files checked before it is written.
2
2
  import { OPERATOR, append } from '../rooms/index.js';
3
+ import fs from 'node:fs';
3
4
  import { fail, note } from '../../../lib/index.js';
5
+ import { checkCollisions } from '../protocol/files.js';
4
6
  import { ID_RE, TOKEN_RE, checkOutgoing, compose, lookup } from '../protocol/index.js';
5
- import { linked, readerLock } from '../history/index.js';
7
+ import { linked } from '../history/index.js';
6
8
  import { isRef } from './actor.js';
7
- import { outgoing, shrink, staged } from './staging.js';
8
9
  // A reply without `to` goes back to the sender (with `all`, to everyone the message named); one's own, to its recipients.
9
10
  const replyTo = (me, x, re) => {
10
11
  if (!x.reply || !re)
@@ -43,6 +44,8 @@ const checkTrailers = (P, x, to, assign, re) => {
43
44
  if (x.roles && !x.roles.split(',').every(role))
44
45
  fail('--roles takes "<id>=<label>, …"; labels mean what the room profile says');
45
46
  };
47
+ const FILE_MAX = 64 << 20;
48
+ const tooBig = (path, size) => size > FILE_MAX && fail(`${path} is ${Math.ceil(size / (1 << 20))} MiB; an attachment is at most ${FILE_MAX >> 20} MiB`);
46
49
  // A message from the actor. The operator may omit `to`: the room, or the assignees when there are any.
47
50
  export const send = ({ R, me }, x) => {
48
51
  const P = R.profile;
@@ -51,16 +54,17 @@ export const send = ({ R, me }, x) => {
51
54
  const assign = x.assign ? [...new Set(x.assign)] : null;
52
55
  const to = recipients(me, x, re, assign);
53
56
  checkTrailers(P, x, to, assign, re);
54
- const stagedNow = staged({ R, me });
55
- const entries = outgoing(stagedNow, x.files ?? []);
57
+ const entries = x.files ?? [];
58
+ for (const e of entries)
59
+ if (e.file)
60
+ tooBig(e.path, fs.statSync(e.file).size);
61
+ checkCollisions(entries.map(e => e.path));
56
62
  const text = compose(x.text, [
57
63
  ['To', to.join(', ')], ['Re', re?.id], ['Kind', x.task && P.opens], ['State', x.state], ['Assign', assign?.join(', ')],
58
64
  ['Roles', x.roles], ['Hop', re && String(re.hopEff + 1)],
59
65
  ]);
60
66
  checkOutgoing(text, isRef(R));
61
67
  const id = append(R, me, text, entries);
62
- if (stagedNow.length)
63
- readerLock(R, me, () => shrink({ R, me }, s => !stagedNow.some(t => t.path === s.path && t.blob === s.blob)));
64
68
  if (to.length === 1 && to[0] === '*')
65
69
  note(`addressed to the whole room: this ${x.task ? P.opens : 'message'} wakes nobody; name recipients (--to or --assign) to start work`);
66
70
  return { id, to, files: entries.length };
@@ -3,7 +3,7 @@ import { ID_RE, TOKEN_RE } from '../protocol/index.js';
3
3
  import { fail, strictJson } from '../../../lib/index.js';
4
4
  import { PROFILE_SCHEMA } from './index.js';
5
5
  const PRINCIPAL_RE = /^(?:requester|assignee|member|role:[a-z][a-z0-9-]*|agent:[a-z0-9]+(?:[._-][a-z0-9]+)*)$/;
6
- const RULE_RE = /^(?:assign|roles|grant|state:(?:\*|[a-z][a-z0-9-]*))$/;
6
+ const RULE_RE = /^(?:assign|roles|state:(?:\*|[a-z][a-z0-9-]*))$/;
7
7
  const distinctTokens = (xs) => Array.isArray(xs) && xs.length > 0 && xs.every(s => TOKEN_RE.test(s)) && new Set(xs).size === xs.length;
8
8
  const principals = (v) => Array.isArray(v) && v.length <= 64 && v.every(x => PRINCIPAL_RE.test(x));
9
9
  const waitingOk = (p) => p.waiting === undefined || (p.states.includes(p.waiting) && !p.terminal.includes(p.waiting));
@@ -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 (ssh://, git+ssh://), 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
@@ -1,20 +0,0 @@
1
- import { Branches } from './branches.ts';
2
- import type { SignalRecord, StoredRoom } from '../../core/hub/ports/store.ts';
3
- export declare class Marks extends Branches {
4
- heartbeat(room: StoredRoom, who: string, target: string, text: string): string;
5
- heartbeats(room: StoredRoom): Array<{
6
- agent: string;
7
- target: string;
8
- time: number;
9
- text: string;
10
- }>;
11
- signal(room: StoredRoom, who: string, target: string, name: string): void;
12
- protected tags(): import("./tags.ts").TagRef[];
13
- protected tagObjects(shas: string[]): Map<string, {
14
- target: string;
15
- tagger: string;
16
- time: number;
17
- text: string;
18
- }>;
19
- signals(room: StoredRoom): SignalRecord[];
20
- }
@@ -1,7 +0,0 @@
1
- export declare const hubAdd: ({ home, name, url, root, genesis }: {
2
- home: string;
3
- name: string;
4
- url: string;
5
- root?: string;
6
- genesis?: string;
7
- }) => string;
@@ -1,6 +0,0 @@
1
- export declare const HEARTBEAT_NAME: RegExp;
2
- export declare const heartbeatName: (ref: string) => {
3
- actor: string;
4
- genesis12: string;
5
- } | null;
6
- export declare const heartbeatRef: (who: string, genesis: string) => string;
@@ -1,10 +0,0 @@
1
- import type { Handle, Room } from './types.ts';
2
- export declare const heartbeatObject: (H: Handle, ref: string, sha: string) => {
3
- actor: string;
4
- genesis12: string;
5
- genesis: string;
6
- target: string;
7
- time: number;
8
- text: string;
9
- };
10
- export declare const checkHeartbeat: (H: Handle, R: Room, ref: string, sha: string) => void;
@@ -1,2 +0,0 @@
1
- import type { Handle, Room } from './types.ts';
2
- export declare const pushHeartbeat: (H: Handle, R: Room, who: string, sha: string, expected: string | null) => boolean;
@@ -1,7 +0,0 @@
1
- import type { Handle, Room } from './types.ts';
2
- export { checkHeartbeat, heartbeatObject } from './heartbeat-object.ts';
3
- export { heartbeatBody } from './heartbeat-facts.ts';
4
- export { HEARTBEAT_NAME, heartbeatName, heartbeatRef } from './heartbeat-name.ts';
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];
7
- export declare const makeHeartbeat: (...[H, R, who, target, text]: HeartbeatArgs) => string;
@@ -1,15 +0,0 @@
1
- import type { Handle, Room } from './types.ts';
2
- export declare const SIGNAL_NAME: RegExp;
3
- export declare const signalName: (ref: string) => {
4
- actor: string;
5
- id: string;
6
- signal: string;
7
- };
8
- export declare const signalObject: (H: Handle, ref: string, sha: string) => {
9
- actor: string;
10
- id: string;
11
- signal: string;
12
- message: string;
13
- };
14
- export declare const checkSignal: (H: Handle, R: Room, ref: string, sha: string) => void;
15
- export declare const verifySignal: (H: Handle, R: Room, ref: string, sha: string) => string | null;
@@ -1,2 +0,0 @@
1
- import type { TagSync } from './sync-tags.ts';
2
- export declare const syncHeartbeat: ({ H, tags, pinned, byGenesis }: TagSync, name: string) => void;
@@ -1,6 +0,0 @@
1
- import type { Handle, Room } from './types.ts';
2
- export type RoomsIndex = {
3
- byGenesis: Map<string, Room>;
4
- byTarget: Map<string, Room>;
5
- };
6
- export declare const syncRooms: (H: Handle, fetched: Map<string, string>) => RoomsIndex;
@@ -1,8 +0,0 @@
1
- import type { Handle } from './types.ts';
2
- import type { RoomsIndex } from './sync-rooms.ts';
3
- export type TagSync = RoomsIndex & {
4
- H: Handle;
5
- tags: Map<string, string>;
6
- pinned: Map<string, string>;
7
- };
8
- export declare const syncTags: (H: Handle, rooms: RoomsIndex) => void;
@@ -1,15 +0,0 @@
1
- export type TagRef = {
2
- ref: string;
3
- actor: string;
4
- name: string;
5
- object: string;
6
- type: string;
7
- };
8
- export type TagObject = {
9
- target: string;
10
- tagger: string;
11
- time: number;
12
- text: string;
13
- };
14
- export declare const signalTags: (listing: string, heartbeat: (ref: string) => boolean) => TagRef[];
15
- export declare const tagObject: (body: Buffer) => TagObject;
@@ -1,3 +0,0 @@
1
- export declare const SKILL: string;
2
- export declare const skillTargets: (project: boolean) => string[];
3
- export declare const sameSkill: (dir: string) => boolean;
@@ -1,8 +0,0 @@
1
- import type { Entry } from '../protocol/files.ts';
2
- import type { Staged } from '../history/index.ts';
3
- import type { Actor } from './actor.ts';
4
- export declare const staged: ({ R, me }: Actor) => Staged[];
5
- export declare const stage: ({ R, me }: Actor, files: Entry[]) => Staged[];
6
- export declare const shrink: ({ R, me }: Actor, keep: (s: Staged) => boolean) => Staged[];
7
- export declare const unstage: (a: Actor, paths: string[] | null) => Staged[];
8
- export declare const outgoing: (stagedNow: Staged[], explicit: Entry[]) => Entry[];
@@ -1,82 +0,0 @@
1
- // marks: tags on a room's commits. Signals are immutable annotated tags `<agent>.<signal>.<message12>`; a heartbeat is
2
- // one mutable tag `<agent>.heartbeat.<genesis12>` per agent and room, replaced on every beat.
3
- import * as Remote from './remote.js';
4
- import { signedTag } from './signing.js';
5
- import { ZERO, frames, runGitRaw } from './run.js';
6
- import { signalTags, tagObject } from './tags.js';
7
- import { Branches } from './branches.js';
8
- import { fail, note } from '../../lib/index.js';
9
- export class Marks extends Branches {
10
- // The heartbeat of `who` in the room: a tag `<who>.heartbeat.<genesis12>` on the room's last commit here, its text the
11
- // facts of the beat (core/agents/heartbeat). Each beat replaces the object under the same name, compare-and-swap on the one it
12
- // read, so two writers do not overwrite each other blindly. No reader pins it; its replacement is no alarm.
13
- // On a signed hub the object is signed by `who` and published with the OID it replaced; a newer beat there wins, and
14
- // this one is taken back here (remote.ts).
15
- heartbeat(room, who, target, text) {
16
- const ref = Remote.heartbeatRef(who, room.genesis), name = ref.slice('refs/tags/'.length);
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)
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
- if (this.gitOk(['update-ref', ref, obj, old ?? ZERO]).status !== 0)
21
- fail(`${name} moved meanwhile; the next beat writes it`);
22
- if (this.trust === 'signed' && !Remote.pushHeartbeat(this.h, this.remote(room), who, obj, old))
23
- this.gitOk(['update-ref', ...(old ? [ref, old, obj] : ['-d', ref, obj])]);
24
- return obj;
25
- }
26
- // The room's heartbeats as they stand: whose, on which commit, when, and what they say.
27
- heartbeats(room) {
28
- const g12 = room.genesis.slice(0, 12);
29
- const tags = this.git(['for-each-ref', '--format=%(refname) %(objectname) %(objecttype)', 'refs/tags/']).split('\n')
30
- .map(l => l.split(' ')).filter(([ref, , type]) => type === 'tag' && Remote.heartbeatName(ref ?? '')?.genesis12 === g12);
31
- const objects = this.tagObjects(tags.map(([, object]) => object));
32
- // On a signed hub only a verified beat shows.
33
- const ok = (ref, object) => this.trust !== 'signed' || !Remote.verifyHeartbeat(this.h, this.remote(room), ref, object);
34
- return tags.flatMap(([ref, object]) => {
35
- const o = objects.get(object);
36
- return o && ok(ref, object) ? [{ agent: Remote.heartbeatName(ref).actor, target: o.target, time: o.time, text: o.text }] : [];
37
- });
38
- }
39
- // An annotated tag without text in its author's namespace, on the message's commit, signed on a signed hub. The
40
- // message's id in the name keeps it unique, so writing the same signal twice is a no-op.
41
- signal(room, who, target, name) {
42
- const tag = `${who}.${name}`;
43
- const ref = `refs/tags/${tag}`;
44
- if (this.gitOk(['rev-parse', '-q', '--verify', ref]).status === 0) {
45
- Remote.push(this.h, ref);
46
- return;
47
- }
48
- this.git(['update-ref', ref, signedTag(this.h, who, tag, target), ZERO]);
49
- Remote.push(this.h, ref);
50
- }
51
- // The tags named `<actor>.<signal>.<message12>`, with the objects they name; a heartbeat is no signal (heartbeats()).
52
- tags() {
53
- return signalTags(this.git(['for-each-ref', '--format=%(refname) %(objectname) %(objecttype)', 'refs/tags/']), ref => !!Remote.heartbeatName(ref));
54
- }
55
- // Annotated tag objects read whole (run.ts frames): their target, tagger and text are never split on bytes they hold.
56
- tagObjects(shas) {
57
- if (!shas.length)
58
- return new Map();
59
- const raw = runGitRaw(this.args(['cat-file', '--batch']), this.home, Buffer.from(shas.join('\n') + '\n'));
60
- return new Map([...frames(raw, shas)].filter(([, f]) => f.type === 'tag').map(([sha, f]) => [sha, tagObject(f.body)]));
61
- }
62
- signals(room) {
63
- const tags = this.tags(), objects = this.tagObjects(tags.filter(t => t.type === 'tag').map(t => t.object));
64
- const mine = this.commitsOf(room);
65
- const records = tags.flatMap(t => {
66
- const o = objects.get(t.object);
67
- // Only an annotated tag on a commit is a signal; anything else points nowhere.
68
- if (!mine.has(o?.target || t.object))
69
- return [];
70
- return [{ ref: t.ref, object: t.object, name: t.name, actor: t.actor, target: o?.target ?? '', tagger: o?.tagger ?? '', text: o?.text ?? '' }];
71
- });
72
- if (this.trust !== 'signed')
73
- return records;
74
- return Remote.withVerification(this.h, () => records.filter(s => {
75
- const why = Remote.verifySignal(this.h, this.remote(room), s.ref, s.object);
76
- if (why)
77
- note(`ALARM signal ${s.ref}: ${why}; quarantined and withheld`);
78
- return !why;
79
- }));
80
- }
81
- }
82
- //# sourceMappingURL=marks.js.map
@@ -1,47 +0,0 @@
1
- // bootstrap: a replica of a published signed hub, verified against the root and genesis the operator gives out of
2
- // band before its descriptor is written.
3
- import crypto from 'node:crypto';
4
- import fs from 'node:fs';
5
- import path from 'node:path';
6
- import * as C from '../../../lib/index.js';
7
- import { id, mkdir } from './git.js';
8
- import { checkUrl, keyInput } from './keys.js';
9
- import { command } from './process.js';
10
- import { sync } from './sync.js';
11
- import { SHA } from './types.js';
12
- export const hubAdd = ({ home, name, url, root, genesis }) => {
13
- id(name);
14
- checkUrl(url);
15
- if (!root)
16
- C.fail('hub add requires --root <public-key|file> from the operator; a key advertised by the hub is not a trust anchor');
17
- const key = keyInput(root ?? C.fail('hub add requires --root <public-key|file>'));
18
- if (genesis && !SHA.test(genesis))
19
- C.fail('--genesis must be a full commit id');
20
- // A hub is a descriptor (hub.json) beside its store; for a git store the repository is store/.git.
21
- const hubDir = path.join(home, 'hub', name);
22
- const dir = path.join(hubDir, 'store', '.git');
23
- if (fs.existsSync(hubDir))
24
- C.fail(`hub ${name} exists`);
25
- mkdir(path.dirname(dir));
26
- const stage = path.join(home, 'hub', `.${name}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.git`);
27
- try {
28
- command('git', ['init', '--bare', '-q', '-b', 'main', stage]);
29
- const descriptor = {
30
- schema: 'ringi-hub/2', name, trust: 'signed', root: key.line, genesis,
31
- store: { type: 'git', path: 'store', remote: url }
32
- };
33
- const H = { name, dir: stage, d: descriptor, bootstrap: !genesis };
34
- sync(H);
35
- fs.renameSync(stage, dir);
36
- fs.writeFileSync(path.join(hubDir, 'hub.json'), JSON.stringify(descriptor, null, 2) + '\n', { mode: 0o600 });
37
- return dir;
38
- }
39
- finally {
40
- if (fs.existsSync(stage))
41
- fs.rmSync(stage, { recursive: true, force: true });
42
- // A failed add leaves no half-made hub behind (the directory did not exist before).
43
- if (!fs.existsSync(dir))
44
- fs.rmSync(hubDir, { recursive: true, force: true });
45
- }
46
- };
47
- //# sourceMappingURL=bootstrap.js.map
@@ -1,19 +0,0 @@
1
- // heartbeat-name: the one mutable tag name, `<actor>.heartbeat.<genesis12>`; every other tag is a signal.
2
- import * as C from '../../../lib/index.js';
3
- import { SHA, TAGS } from './types.js';
4
- // Heartbeats deliberately share the tag transport, but never the immutable signal acceptance.
5
- // Only this exact namespace is mutable; e.g. `actor.opened.heartbeat.<id>` is still a signal.
6
- export const HEARTBEAT_NAME = /^([a-z0-9]+(?:[_-][a-z0-9]+)*)\.heartbeat\.([0-9a-f]{12})$/;
7
- export const heartbeatName = (ref) => {
8
- const m = ref.startsWith(TAGS) && HEARTBEAT_NAME.exec(ref.slice(TAGS.length));
9
- return m ? { actor: m[1], genesis12: m[2] } : null;
10
- };
11
- export const heartbeatRef = (who, genesis) => {
12
- if (!SHA.test(genesis))
13
- C.fail('heartbeat requires a full room genesis');
14
- const ref = `${TAGS}${who}.heartbeat.${genesis.slice(0, 12)}`;
15
- if (!heartbeatName(ref))
16
- C.fail('malformed heartbeat actor');
17
- return ref;
18
- };
19
- //# sourceMappingURL=heartbeat-name.js.map
@@ -1,40 +0,0 @@
1
- // heartbeat-object: a heartbeat tag object read and checked: its target in the room, its tagger, its signature.
2
- import * as C from '../../../lib/index.js';
3
- import * as P from '../../../core/hub/protocol/index.js';
4
- import { branch } from './branch.js';
5
- import { git, rev } from './git.js';
6
- import { heartbeatBody } from './heartbeat-facts.js';
7
- import { heartbeatName } from './heartbeat-name.js';
8
- import { checkSignature } from './signature.js';
9
- import { SHA, TAGS } from './types.js';
10
- export const heartbeatObject = (H, ref, sha) => {
11
- const s = heartbeatName(ref) ?? C.fail('malformed heartbeat namespace');
12
- if (!SHA.test(sha) || git(H, ['cat-file', '-t', sha]).trim() !== 'tag')
13
- C.fail('heartbeat must be an annotated tag');
14
- if (Number(git(H, ['cat-file', '-s', sha])) > 32768)
15
- C.fail('heartbeat object exceeds 32 KiB');
16
- const decoded = P.decodeText(git(H, ['cat-file', 'tag', sha], { raw: true }));
17
- if (decoded.invalid)
18
- C.fail(`heartbeat tag is ${decoded.invalid}`);
19
- const [header, ...rest] = decoded.text.split('\n\n');
20
- const target = /^object ([0-9a-f]+)\ntype commit\ntag ([^\n]+)\ntagger ([^<\n]+) <[^\n]*> (\d+) [+-]\d{4}\n?$/.exec(header) ?? C.fail('malformed heartbeat tag headers');
21
- if (!SHA.test(target[1]) || target[2] !== ref.slice(TAGS.length) || target[3].trim() !== s.actor)
22
- C.fail('heartbeat tag name or actor disagrees');
23
- const text = rest.join('\n\n').split('-----BEGIN SSH SIGNATURE-----')[0];
24
- const genesis = heartbeatBody(text);
25
- if (!genesis.startsWith(s.genesis12))
26
- C.fail('heartbeat tag name and room genesis disagree');
27
- return { ...s, genesis, target: target[1], time: Number(target[4]), text };
28
- };
29
- export const checkHeartbeat = (H, R, ref, sha) => {
30
- const s = heartbeatObject(H, ref, sha);
31
- if (s.genesis !== R.genesis)
32
- C.fail('heartbeat belongs to another room genesis');
33
- const b = branch(H, R, rev(H, R.ref) ?? C.fail('heartbeat room has no branch here'));
34
- // It is a replica's sync position, not a claim to author or execute the target message.
35
- if (!b.control.includes(s.target) && !b.authors.has(s.target) && !b.skipped.has(s.target))
36
- C.fail('heartbeat target is not on this room branch');
37
- const signer = b.entries.find(e => e.principal === s.actor) ?? C.fail(`unadmitted heartbeat principal ${s.actor}`);
38
- checkSignature(H, sha, s.actor, signer.key, 'tag');
39
- };
40
- //# sourceMappingURL=heartbeat-object.js.map