@idleware/ringi 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -11
- package/package.json +1 -1
- package/src/main/resources/skill/ringi/SKILL.md +3 -4
- package/target/dts/adapters/{transport-git → store-git}/branches.d.ts +1 -3
- package/target/dts/adapters/{transport-git → store-git}/index.d.ts +0 -1
- package/target/dts/adapters/store-git/marks.d.ts +21 -0
- package/target/dts/adapters/{transport-git → store-git}/remote/push.d.ts +0 -1
- package/target/dts/adapters/store-git/remote/stream-check.d.ts +12 -0
- package/target/dts/adapters/store-git/remote/stream-control.d.ts +8 -0
- package/target/dts/adapters/store-git/remote/stream-facts.d.ts +4 -0
- package/target/dts/adapters/store-git/remote/streams.d.ts +31 -0
- package/target/dts/adapters/store-git/remote/sync-streams.d.ts +2 -0
- package/target/dts/adapters/{transport-git → store-git}/remote/types.d.ts +5 -4
- package/target/dts/adapters/{transport-git → store-git}/remote.d.ts +1 -4
- package/target/dts/adapters/{transport-git → store-git}/repo.d.ts +3 -2
- package/target/dts/adapters/{transport-git → store-git}/signing.d.ts +0 -1
- package/target/dts/clients/cli/operator/hub-commands.d.ts +0 -2
- package/target/dts/clients/cli/operator/index.d.ts +1 -1
- package/target/dts/clients/cli/operator/sessions.d.ts +0 -1
- package/target/dts/clients/cli/operator/where-format.d.ts +1 -1
- package/target/dts/clients/cli/shared/index.d.ts +2 -2
- package/target/dts/clients/cli/shared/messages.d.ts +0 -1
- package/target/dts/clients/cli/shared/profile.d.ts +0 -1
- package/target/dts/clients/cli/skill/index.d.ts +2 -1
- package/target/dts/core/agents/overview.d.ts +4 -7
- package/target/dts/core/agents/sessions/index.d.ts +0 -1
- package/target/dts/core/agents/sessions/join.d.ts +1 -2
- package/target/dts/core/hub/history/index.d.ts +1 -10
- package/target/dts/core/hub/ports/store.d.ts +35 -23
- package/target/dts/core/hub/rooms/address.d.ts +6 -0
- package/target/dts/core/hub/rooms/adoption.d.ts +7 -0
- package/target/dts/core/hub/rooms/compat.d.ts +3 -1
- package/target/dts/core/hub/rooms/contracts.d.ts +16 -0
- package/target/dts/core/hub/rooms/hubs.d.ts +3 -3
- package/target/dts/core/hub/rooms/index.d.ts +5 -6
- package/target/dts/core/hub/rooms/room-catalogue.d.ts +9 -0
- package/target/dts/core/hub/rooms/room-life.d.ts +10 -0
- package/target/dts/core/hub/rooms/room-view.d.ts +3 -0
- package/target/dts/core/hub/rooms/rooms.d.ts +4 -2
- package/target/dts/core/hub/signals/check.d.ts +2 -2
- package/target/dts/core/hub/signals/model.d.ts +2 -10
- package/target/dts/core/hub/signals/pins.d.ts +2 -2
- package/target/dts/core/hub/usecases/config.d.ts +0 -1
- package/target/dts/core/hub/usecases/context.d.ts +1 -2
- package/target/dts/core/hub/usecases/hubs.d.ts +1 -8
- package/target/dts/core/hub/usecases/invite.d.ts +12 -0
- package/target/dts/core/hub/usecases/room.d.ts +0 -1
- package/target/dts/core/hub/usecases/rooms.d.ts +9 -7
- package/target/dts/core/hub/usecases/roster.d.ts +1 -2
- package/target/dts/core/hub/work/index.d.ts +0 -1
- package/target/dts/index.d.ts +1 -1
- package/target/esm/adapters/{tool-claude → agent-claude}/index.js +2 -2
- package/target/esm/adapters/{tool-codex → agent-codex}/index.js +2 -2
- package/target/esm/adapters/{tool-cursor → agent-cursor}/index.js +1 -1
- package/target/esm/adapters/{tool-traces → agent-traces}/index.js +1 -1
- package/target/esm/adapters/state-fs/index.js +2 -2
- package/target/esm/adapters/{transport-git → store-git}/branches.js +21 -27
- package/target/esm/adapters/{transport-git → store-git}/index.js +10 -28
- package/target/esm/adapters/store-git/marks.js +86 -0
- package/target/esm/adapters/{transport-git → store-git}/remote/commits.js +3 -1
- package/target/esm/adapters/{transport-git → store-git}/remote/git.js +8 -1
- package/target/esm/adapters/{transport-git → store-git}/remote/keys.js +8 -3
- package/target/esm/adapters/{transport-git → store-git}/remote/push-retry.js +24 -6
- package/target/esm/adapters/store-git/remote/push-validate.js +19 -0
- package/target/esm/adapters/{transport-git → store-git}/remote/push.js +13 -14
- package/target/esm/adapters/store-git/remote/stream-check.js +44 -0
- package/target/esm/adapters/store-git/remote/stream-control.js +33 -0
- package/target/esm/adapters/store-git/remote/stream-facts.js +40 -0
- package/target/esm/adapters/store-git/remote/streams.js +76 -0
- package/target/esm/adapters/store-git/remote/sync-streams.js +28 -0
- package/target/esm/adapters/store-git/remote/sync.js +59 -0
- package/target/esm/adapters/{transport-git → store-git}/remote/types.js +0 -3
- package/target/esm/adapters/{transport-git → store-git}/remote.js +1 -4
- package/target/esm/adapters/{transport-git → store-git}/repo.js +42 -4
- package/target/esm/adapters/{transport-git → store-git}/signing.js +1 -9
- package/target/esm/adapters/store-git/transport.js +60 -0
- package/target/esm/clients/cli/agent/index.js +1 -1
- package/target/esm/clients/cli/agent/session.js +10 -10
- package/target/esm/clients/cli/commands.js +5 -5
- package/target/esm/clients/cli/context.js +1 -1
- package/target/esm/clients/cli/help.js +15 -21
- package/target/esm/clients/cli/operator/heartbeat-format.js +1 -1
- package/target/esm/clients/cli/operator/heartbeat.js +1 -2
- package/target/esm/clients/cli/operator/hub-commands.js +6 -19
- package/target/esm/clients/cli/operator/hub.js +2 -2
- package/target/esm/clients/cli/operator/index.js +2 -2
- package/target/esm/clients/cli/operator/room.js +25 -32
- package/target/esm/clients/cli/operator/sessions.js +4 -22
- package/target/esm/clients/cli/operator/where-format.js +2 -1
- package/target/esm/clients/cli/operator/where.js +5 -7
- package/target/esm/clients/cli/shared/index.js +2 -2
- package/target/esm/clients/cli/shared/messages.js +2 -12
- package/target/esm/clients/cli/shared/profile.js +1 -6
- package/target/esm/clients/cli/skill/index.js +14 -19
- package/target/esm/composition/index.js +4 -4
- package/target/esm/core/agents/heartbeat/pulse.js +4 -4
- package/target/esm/core/agents/overview.js +13 -25
- package/target/esm/core/agents/sessions/binding.js +1 -1
- package/target/esm/core/agents/sessions/index.js +3 -2
- package/target/esm/core/agents/sessions/join.js +17 -9
- package/target/esm/core/agents/sessions/resume.js +3 -6
- package/target/esm/core/hub/capabilities.js +1 -4
- package/target/esm/core/hub/history/index.js +23 -13
- package/target/esm/core/hub/ports/store.js +2 -2
- package/target/esm/core/hub/rooms/address.js +18 -0
- package/target/esm/core/hub/rooms/adoption.js +32 -0
- package/target/esm/core/hub/rooms/compat.js +39 -14
- package/target/esm/core/hub/rooms/contracts.js +62 -0
- package/target/esm/core/hub/rooms/hubs.js +33 -56
- package/target/esm/core/hub/rooms/index.js +2 -2
- package/target/esm/core/hub/rooms/messages.js +6 -6
- package/target/esm/core/hub/rooms/room-catalogue.js +31 -0
- package/target/esm/core/hub/rooms/room-life.js +59 -0
- package/target/esm/core/hub/rooms/room-view.js +39 -0
- package/target/esm/core/hub/rooms/rooms.js +26 -37
- package/target/esm/core/hub/signals/check.js +21 -26
- package/target/esm/core/hub/signals/index.js +10 -10
- package/target/esm/core/hub/signals/model.js +0 -2
- package/target/esm/core/hub/signals/pins.js +44 -16
- package/target/esm/core/hub/signals/write.js +8 -10
- package/target/esm/core/hub/usecases/actor.js +1 -1
- package/target/esm/core/hub/usecases/config.js +2 -2
- package/target/esm/core/hub/usecases/context.js +5 -8
- package/target/esm/core/hub/usecases/hubs.js +16 -33
- package/target/esm/core/hub/usecases/invite.js +26 -0
- package/target/esm/core/hub/usecases/room.js +4 -5
- package/target/esm/core/hub/usecases/rooms.js +40 -35
- package/target/esm/core/hub/usecases/roster.js +12 -21
- package/target/esm/core/hub/usecases/send.js +10 -6
- package/target/esm/core/hub/work/parse.js +1 -1
- package/target/esm/lib/fail.js +2 -2
- package/target/dts/adapters/transport-git/marks.d.ts +0 -20
- package/target/dts/adapters/transport-git/remote/bootstrap.d.ts +0 -7
- package/target/dts/adapters/transport-git/remote/heartbeat-name.d.ts +0 -6
- package/target/dts/adapters/transport-git/remote/heartbeat-object.d.ts +0 -10
- package/target/dts/adapters/transport-git/remote/heartbeat-push.d.ts +0 -2
- package/target/dts/adapters/transport-git/remote/heartbeat.d.ts +0 -7
- package/target/dts/adapters/transport-git/remote/signals.d.ts +0 -15
- package/target/dts/adapters/transport-git/remote/sync-heartbeat.d.ts +0 -2
- package/target/dts/adapters/transport-git/remote/sync-rooms.d.ts +0 -6
- package/target/dts/adapters/transport-git/remote/sync-tags.d.ts +0 -8
- package/target/dts/adapters/transport-git/tags.d.ts +0 -15
- package/target/dts/clients/cli/skill/targets.d.ts +0 -3
- package/target/dts/core/hub/usecases/staging.d.ts +0 -8
- package/target/esm/adapters/transport-git/marks.js +0 -82
- package/target/esm/adapters/transport-git/remote/bootstrap.js +0 -47
- package/target/esm/adapters/transport-git/remote/heartbeat-name.js +0 -19
- package/target/esm/adapters/transport-git/remote/heartbeat-object.js +0 -40
- package/target/esm/adapters/transport-git/remote/heartbeat-push.js +0 -44
- package/target/esm/adapters/transport-git/remote/heartbeat.js +0 -44
- package/target/esm/adapters/transport-git/remote/push-validate.js +0 -39
- package/target/esm/adapters/transport-git/remote/signals.js +0 -63
- package/target/esm/adapters/transport-git/remote/sync-heartbeat.js +0 -26
- package/target/esm/adapters/transport-git/remote/sync-rooms.js +0 -38
- package/target/esm/adapters/transport-git/remote/sync-tags.js +0 -48
- package/target/esm/adapters/transport-git/remote/sync.js +0 -38
- package/target/esm/adapters/transport-git/tags.js +0 -21
- package/target/esm/adapters/transport-git/transport.js +0 -29
- package/target/esm/clients/cli/skill/targets.js +0 -20
- package/target/esm/core/hub/usecases/staging.js +0 -60
- /package/target/dts/adapters/{tool-claude → agent-claude}/index.d.ts +0 -0
- /package/target/dts/adapters/{tool-codex → agent-codex}/index.d.ts +0 -0
- /package/target/dts/adapters/{tool-cursor → agent-cursor}/index.d.ts +0 -0
- /package/target/dts/adapters/{tool-traces → agent-traces}/index.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/files.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/admit.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/alarms.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/branch-commit.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/branch-control.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/branch-scan.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/branch.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/cache.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/commits.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/forget.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/git.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/heartbeat-facts.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/keys.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/main.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/process.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/push-retry.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/push-validate.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/roster.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/signature.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/remote/sync.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/run.d.ts +0 -0
- /package/target/dts/adapters/{transport-git → store-git}/transport.d.ts +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/files.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/admit.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/alarms.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/branch-commit.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/branch-control.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/branch-scan.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/branch.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/cache.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/forget.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/heartbeat-facts.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/main.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/process.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/roster.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/remote/signature.js +0 -0
- /package/target/esm/adapters/{transport-git → store-git}/run.js +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import * as C from '../../../lib/index.js';
|
|
5
|
+
import { storeOf } from '../../../core/hub/rooms/address.js';
|
|
5
6
|
import { id, rev } from './git.js';
|
|
6
7
|
import { ROOT } from './types.js';
|
|
7
8
|
export const parseKey = (value) => {
|
|
@@ -16,7 +17,7 @@ export const parseKey = (value) => {
|
|
|
16
17
|
return { line, fp: 'SHA256:' + crypto.createHash('sha256').update(data).digest('base64').replace(/=+$/, '') };
|
|
17
18
|
};
|
|
18
19
|
export const keyInput = (value) => parseKey(fs.existsSync(value) && fs.statSync(value).isFile() ? fs.readFileSync(value, 'utf8') : value);
|
|
19
|
-
export const rootKey = (H) => parseKey(H.d.root || C.fail(`hub ${H.name} has no out-of-band root pin; use
|
|
20
|
+
export const rootKey = (H) => parseKey(H.d.root || C.fail(`hub ${H.name} has no out-of-band root pin; use an invitation carrying the independently pinned root`));
|
|
20
21
|
export const signerLine = (who, key) => `${who} namespaces="git" ${key.line}\n`;
|
|
21
22
|
export const signingContext = (H) => H.signing ?? C.fail(`hub ${H.name}: signed writes require an injected Signer`);
|
|
22
23
|
export const identityKey = (H, who, create = false) => {
|
|
@@ -31,8 +32,12 @@ export const setupSigned = (H, url) => {
|
|
|
31
32
|
H.d.trust = 'signed';
|
|
32
33
|
H.d.root = key.line;
|
|
33
34
|
if (url) {
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const address = storeOf(url);
|
|
36
|
+
if (address.type !== 'git')
|
|
37
|
+
C.fail('git setup requires a git address');
|
|
38
|
+
checkUrl(address.locator);
|
|
39
|
+
const source = H.d.contract ?? C.fail('signed setup requires an explicit contract source');
|
|
40
|
+
source.remote = url;
|
|
36
41
|
}
|
|
37
42
|
return { root: key.line, allowedSigners: signerLine(ROOT, key) };
|
|
38
43
|
};
|
|
@@ -2,26 +2,44 @@
|
|
|
2
2
|
// verified remote tip to make it again.
|
|
3
3
|
import * as C from '../../../lib/index.js';
|
|
4
4
|
import { savePushAlarms } from './alarms.js';
|
|
5
|
+
import { validateRef } from './push-validate.js';
|
|
5
6
|
import { branchData } from './branch.js';
|
|
6
|
-
import {
|
|
7
|
+
import { readCommits } from './commits.js';
|
|
8
|
+
import { verifyMain } from './main.js';
|
|
9
|
+
import { ancestor, git, promote, rev, tracking, zero } from './git.js';
|
|
10
|
+
import { streamName } from './streams.js';
|
|
7
11
|
const fetchIncoming = ({ H, ref, track, url, sha, acc }) => {
|
|
8
12
|
git(H, ['-c', 'fetch.fsckObjects=true', 'fetch', '-q', '--no-tags', url, `+${ref}:${track}`]);
|
|
13
|
+
git(H, ['-c', 'fetch.fsckObjects=true', 'fetch', '-q', '--no-tags', url, `+refs/heads/main:${tracking('refs/heads/main')}`]);
|
|
14
|
+
const main = rev(H, tracking('refs/heads/main')) ?? C.fail('remote main is missing');
|
|
15
|
+
verifyMain(H, main);
|
|
16
|
+
if (H.endpoint && readCommits(H, [H.endpoint.genesis])[0].parents[0] !== main)
|
|
17
|
+
C.fail('the room main changed its frozen contract');
|
|
18
|
+
promote(H, 'refs/heads/main', main);
|
|
19
|
+
if (streamName(ref)) {
|
|
20
|
+
git(H, ['-c', 'fetch.fsckObjects=true', 'fetch', '-q', '--no-tags', url, `+refs/heads/chat:${tracking('refs/heads/chat')}`]);
|
|
21
|
+
const chat = rev(H, tracking('refs/heads/chat')) ?? C.fail('remote chat is missing');
|
|
22
|
+
const endpoint = H.endpoint ?? C.fail('stream retry requires its room endpoint');
|
|
23
|
+
branchData(H, endpoint.name, chat, endpoint.genesis);
|
|
24
|
+
promote(H, 'refs/heads/chat', chat);
|
|
25
|
+
}
|
|
9
26
|
const incoming = rev(H, track) ?? C.fail('remote room branch is missing');
|
|
10
|
-
|
|
11
|
-
|
|
27
|
+
validateRef(H, ref, sha);
|
|
28
|
+
validateRef(H, ref, incoming);
|
|
12
29
|
if (acc && !ancestor(H, acc, incoming))
|
|
13
30
|
C.fail('remote room was rewritten or rolled back; retry refused');
|
|
14
31
|
return incoming;
|
|
15
32
|
};
|
|
16
|
-
const retryable = ({ H, sha, acc }, stdout) => {
|
|
33
|
+
const retryable = ({ H, sha, acc, ref }, stdout) => {
|
|
17
34
|
const parents = git(H, ['show', '-s', '--format=%P', sha]).trim().split(' ');
|
|
18
35
|
const rejected = /\[rejected\].*\((?:fetch first|non-fast-forward)\)/.test(String(stdout));
|
|
19
|
-
|
|
36
|
+
const base = acc ?? (streamName(ref) ? H.endpoint?.genesis : undefined);
|
|
37
|
+
return rejected && !!base && parents.length === 1 && parents[0] === base;
|
|
20
38
|
};
|
|
21
39
|
const retryRefs = ({ H, sha, ref, pin, acc, alarms }, incoming) => {
|
|
22
40
|
git(H, ['update-ref', `refs/ringi/unpublished/${sha}`, sha]);
|
|
23
41
|
git(H, ['update-ref', ref, incoming, sha]);
|
|
24
|
-
git(H, ['update-ref', pin, incoming, acc]);
|
|
42
|
+
git(H, ['update-ref', pin, incoming, acc ?? zero(H)]);
|
|
25
43
|
delete alarms[ref];
|
|
26
44
|
savePushAlarms(H, alarms);
|
|
27
45
|
if (H.remoteAlarms)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// push-validate: a ref checked before it is published: main, a room's branch, or an actor's signal on a message of a
|
|
2
|
+
// verified room.
|
|
3
|
+
import * as C from '../../../lib/index.js';
|
|
4
|
+
import { branchData } from './branch.js';
|
|
5
|
+
import { verifyMain } from './main.js';
|
|
6
|
+
import { streamRoom, validateStream } from './streams.js';
|
|
7
|
+
export const validateRef = (H, ref, sha) => {
|
|
8
|
+
if (ref === 'refs/heads/main') {
|
|
9
|
+
verifyMain(H, sha);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const e = H.endpoint ?? C.fail('publication requires an explicit room endpoint');
|
|
13
|
+
if (ref !== 'refs/heads/chat')
|
|
14
|
+
return validateStream(H, streamRoom(H), ref, sha);
|
|
15
|
+
const why = branchData(H, e.name, sha, e.genesis).skipped.get(sha);
|
|
16
|
+
if (why)
|
|
17
|
+
C.fail(`${sha.slice(0, 12)} is no valid commit of room ${e.name}: ${why}`);
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=push-validate.js.map
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
// Publish one immutable ref. Room append retries preserve already returned message ids.
|
|
2
2
|
import * as C from '../../../lib/index.js';
|
|
3
3
|
import { pushAlarms, savePushAlarms } from './alarms.js';
|
|
4
|
-
import { accepted, ancestor, git, isRemote, locked,
|
|
5
|
-
import { heartbeatName } from './heartbeat.js';
|
|
4
|
+
import { accepted, ancestor, git, isRemote, locked, origin, rev, suffix, tracking, zero } from './git.js';
|
|
6
5
|
import { checkUrl } from './keys.js';
|
|
7
6
|
import { recoverPublication } from './push-retry.js';
|
|
8
7
|
import { validateRef } from './push-validate.js';
|
|
9
|
-
import { TAGS, TAG_ACCEPT, TAG_TRACK } from './types.js';
|
|
10
8
|
export { validateRef } from './push-validate.js';
|
|
11
9
|
const preparePublication = (H, ref) => {
|
|
12
|
-
const alarms = pushAlarms(H),
|
|
13
|
-
const
|
|
14
|
-
const track = tag ? TAG_TRACK + ref.slice(TAGS.length) : tracking(ref);
|
|
15
|
-
const sha = (tag ? object(H, ref) : rev(H, ref)) || C.fail(`no ${ref} to publish`);
|
|
10
|
+
const alarms = pushAlarms(H), pin = accepted(ref), track = tracking(ref);
|
|
11
|
+
const sha = rev(H, ref) || C.fail(`no ${ref} to publish`);
|
|
16
12
|
validateRef(H, ref, sha);
|
|
17
|
-
return { H, ref, alarms,
|
|
13
|
+
return { H, ref, alarms, pin, track, sha };
|
|
18
14
|
};
|
|
19
15
|
const rejectPublication = (p, behind, stdout) => {
|
|
20
16
|
let why = '';
|
|
21
|
-
if (behind
|
|
17
|
+
if ((behind || /\/(signals|telemetry)$/.test(p.ref)) && p.ref !== 'refs/heads/main') {
|
|
22
18
|
try {
|
|
23
19
|
const recovered = recoverPublication(p, stdout);
|
|
24
20
|
if (recovered !== null)
|
|
@@ -38,23 +34,26 @@ const pushUnlocked = (H, ref, behind) => {
|
|
|
38
34
|
if (!url)
|
|
39
35
|
return true;
|
|
40
36
|
checkUrl(url);
|
|
41
|
-
const {
|
|
42
|
-
const acc =
|
|
43
|
-
if (acc &&
|
|
37
|
+
const { pin, sha, track, alarms } = prepared;
|
|
38
|
+
const acc = rev(H, pin);
|
|
39
|
+
if (acc && !ancestor(H, acc, sha))
|
|
44
40
|
C.fail('ref to publish does not extend its accepted transport history');
|
|
41
|
+
// Replaying a known tuple can retry its retained unpublished head. A healthy acknowledged head needs no push.
|
|
42
|
+
if (acc === sha && !alarms[ref] && !H.remoteAlarms?.[ref])
|
|
43
|
+
return true;
|
|
45
44
|
const p = { ...prepared, acc, url };
|
|
46
45
|
const r = git(H, ['push', '-q', '--porcelain', url, `${sha}:${ref}`], { ok: true });
|
|
47
46
|
if (r.status !== 0)
|
|
48
47
|
return rejectPublication(p, behind, r.stdout);
|
|
49
48
|
delete alarms[ref];
|
|
50
49
|
savePushAlarms(H, alarms);
|
|
50
|
+
if (H.remoteAlarms)
|
|
51
|
+
delete H.remoteAlarms[ref];
|
|
51
52
|
git(H, ['update-ref', pin, sha, acc || zero(H)]);
|
|
52
53
|
git(H, ['update-ref', track, sha]);
|
|
53
54
|
return true;
|
|
54
55
|
};
|
|
55
56
|
export const push = (H, ref, { behind = false } = {}) => {
|
|
56
|
-
if (heartbeatName(ref))
|
|
57
|
-
C.fail('heartbeat publication requires its actor and expected object id (pushHeartbeat)');
|
|
58
57
|
if (!isRemote(H))
|
|
59
58
|
return true;
|
|
60
59
|
return locked(H, () => pushUnlocked(H, ref, behind));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// One stream record: structure, dependencies and admitted signature. Null means a dependency is not fetched yet.
|
|
2
|
+
import * as C from '../../../lib/index.js';
|
|
3
|
+
import { ancestor, git, rev } from './git.js';
|
|
4
|
+
import { streamFacts } from './stream-facts.js';
|
|
5
|
+
import { streamAdmission } from './stream-control.js';
|
|
6
|
+
const frame = ({ H, actor, parent }, c) => {
|
|
7
|
+
if (c.parents.length !== 1 || c.parents[0] !== parent)
|
|
8
|
+
C.fail('stream is not one linear continuation of its room genesis');
|
|
9
|
+
if (c.author !== actor || c.committer !== actor)
|
|
10
|
+
C.fail('stream author and committer must be its actor');
|
|
11
|
+
if (c.tree !== git(H, ['rev-parse', `${parent}^{tree}`]).trim())
|
|
12
|
+
C.fail('stream changes its parent tree');
|
|
13
|
+
};
|
|
14
|
+
const controlReady = ({ H, R, dep }, control) => {
|
|
15
|
+
if (dep.controls.has(control))
|
|
16
|
+
return true;
|
|
17
|
+
if (dep.messages.has(control) || dep.skipped.has(control))
|
|
18
|
+
C.fail('stream Control is not an accepted root control');
|
|
19
|
+
if (rev(H, control) && !ancestor(H, R.genesis, control))
|
|
20
|
+
C.fail('stream Control belongs to another history');
|
|
21
|
+
return false;
|
|
22
|
+
};
|
|
23
|
+
const targetReady = ({ H, R, dep, kind }, target) => {
|
|
24
|
+
if (dep.messages.has(target))
|
|
25
|
+
return true;
|
|
26
|
+
if (kind === 'telemetry' && dep.controls.has(target))
|
|
27
|
+
return true;
|
|
28
|
+
if (dep.controls.has(target) || dep.skipped.has(target))
|
|
29
|
+
C.fail('signal Target is not an accepted chat message');
|
|
30
|
+
if (rev(H, target) && !ancestor(H, R.genesis, target))
|
|
31
|
+
C.fail('stream Target belongs to another history');
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
export const checkStreamRecord = (ctx, c) => {
|
|
35
|
+
frame(ctx, c);
|
|
36
|
+
const f = streamFacts(c, ctx.kind), control = f.get('Control'), target = f.get('Target');
|
|
37
|
+
if (f.get('Room') !== ctx.R.genesis)
|
|
38
|
+
C.fail('stream names another room genesis');
|
|
39
|
+
if (!controlReady(ctx, control) || !targetReady(ctx, target))
|
|
40
|
+
return null;
|
|
41
|
+
streamAdmission(ctx.H, control, ctx.actor, c.sha);
|
|
42
|
+
return f;
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=stream-check.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Stream authority is the verified chat control it names, not current membership or a server listing.
|
|
2
|
+
import * as C from '../../../lib/index.js';
|
|
3
|
+
import * as P from '../../../core/hub/protocol/index.js';
|
|
4
|
+
import { branchData } from './branch.js';
|
|
5
|
+
import { readCommits } from './commits.js';
|
|
6
|
+
import { git, isRemote, rev } from './git.js';
|
|
7
|
+
import { rosterAt, blobText } from './roster.js';
|
|
8
|
+
import { checkSignature } from './signature.js';
|
|
9
|
+
import { SHA } from './types.js';
|
|
10
|
+
const control = (R, c) => c.sha === R.genesis || (c.author === 'operator' && P.parse(c.text, s => SHA.test(s)).kind === 'room');
|
|
11
|
+
export const streamDependencies = (H, R) => {
|
|
12
|
+
const tip = rev(H, R.ref) ?? C.fail('room chat is missing');
|
|
13
|
+
if (isRemote(H)) {
|
|
14
|
+
const b = branchData(H, R.name, tip, R.genesis);
|
|
15
|
+
return { controls: new Set(b.control), messages: new Set(b.authors.keys()), skipped: new Set(b.skipped.keys()) };
|
|
16
|
+
}
|
|
17
|
+
const shas = git(H, ['rev-list', '--first-parent', '--reverse', tip, `^${R.genesis}^`]).trim().split('\n').filter(Boolean);
|
|
18
|
+
const list = readCommits(H, shas);
|
|
19
|
+
return { controls: new Set(list.filter(c => control(R, c)).map(c => c.sha)),
|
|
20
|
+
messages: new Set(list.filter(c => !control(R, c)).map(c => c.sha)), skipped: new Set() };
|
|
21
|
+
};
|
|
22
|
+
export const streamAdmission = (H, control, actor, sha) => {
|
|
23
|
+
if (isRemote(H)) {
|
|
24
|
+
const key = rosterAt(H, control).entries.find(e => e.principal === actor)?.key ?? C.fail(`${actor} is unadmitted at stream Control`);
|
|
25
|
+
checkSignature(H, sha, actor, key);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const members = C.strictJson(blobText(H, control, 'members.json'), 'members.json');
|
|
29
|
+
if (!Object.hasOwn(members, actor))
|
|
30
|
+
C.fail(`${actor} is unadmitted at stream Control`);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=stream-control.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Exact facts of signal and telemetry records: bounded UTF-8, unique fields and full causal revisions.
|
|
2
|
+
import * as C from '../../../lib/index.js';
|
|
3
|
+
import * as P from '../../../core/hub/protocol/index.js';
|
|
4
|
+
import { heartbeatBody } from './heartbeat-facts.js';
|
|
5
|
+
import { SHA } from './types.js';
|
|
6
|
+
const fieldMap = (text) => {
|
|
7
|
+
const fields = new Map();
|
|
8
|
+
for (const line of text.trimEnd().split('\n')) {
|
|
9
|
+
const m = /^([A-Z][A-Za-z]*): (.+)$/.exec(line) ?? C.fail('stream record must contain only facts');
|
|
10
|
+
if (fields.has(m[1]))
|
|
11
|
+
C.fail(`duplicate stream fact ${m[1]}`);
|
|
12
|
+
fields.set(m[1], m[2]);
|
|
13
|
+
}
|
|
14
|
+
return fields;
|
|
15
|
+
};
|
|
16
|
+
const checkFields = (fields, kind) => {
|
|
17
|
+
const allowed = kind === 'signals' ? ['Room', 'Control', 'Target', 'Signal'] : ['Room', 'Control', 'Target', 'State', 'Waits', 'Alive', 'Quiet', 'Next'];
|
|
18
|
+
if ([...fields.keys()].some(k => !allowed.includes(k)))
|
|
19
|
+
C.fail('unknown stream fact');
|
|
20
|
+
for (const field of ['Room', 'Control', 'Target'])
|
|
21
|
+
if (!SHA.test(fields.get(field) ?? ''))
|
|
22
|
+
C.fail(`${field} must name a full revision`);
|
|
23
|
+
};
|
|
24
|
+
const checkSignal = (fields) => {
|
|
25
|
+
if (fields.size !== 4 || !/^[a-z][a-z0-9._-]*$/.test(fields.get('Signal') ?? ''))
|
|
26
|
+
C.fail('a signal has exactly Room, Control, Target and a logical Signal');
|
|
27
|
+
};
|
|
28
|
+
export const streamFacts = (c, kind) => {
|
|
29
|
+
const d = P.decodeText(c.body), limit = kind === 'signals' ? 4096 : 16384;
|
|
30
|
+
if (d.invalid || Buffer.byteLength(c.body) > limit)
|
|
31
|
+
C.fail('stream facts have invalid encoding or size');
|
|
32
|
+
const fields = fieldMap(d.text);
|
|
33
|
+
checkFields(fields, kind);
|
|
34
|
+
if (kind === 'signals')
|
|
35
|
+
checkSignal(fields);
|
|
36
|
+
else
|
|
37
|
+
heartbeatBody(d.text);
|
|
38
|
+
return fields;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=stream-facts.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Actor streams: immutable commit facts, room/control admission, and the verified prefix of a stream.
|
|
2
|
+
import * as C from '../../../lib/index.js';
|
|
3
|
+
import * as P from '../../../core/hub/protocol/index.js';
|
|
4
|
+
import { readCommits } from './commits.js';
|
|
5
|
+
import { ancestor, reason, rev } from './git.js';
|
|
6
|
+
import { chain } from './signature.js';
|
|
7
|
+
import { streamDependencies } from './stream-control.js';
|
|
8
|
+
import { checkStreamRecord } from './stream-check.js';
|
|
9
|
+
export const streamRoom = (H) => {
|
|
10
|
+
const e = H.endpoint ?? C.fail('actor stream requires a selected room endpoint');
|
|
11
|
+
const head = rev(H, 'refs/heads/chat') ?? C.fail('room chat is missing');
|
|
12
|
+
return { name: e.name, genesis: e.genesis, ref: 'refs/heads/chat', head };
|
|
13
|
+
};
|
|
14
|
+
export const streamName = (ref) => {
|
|
15
|
+
const m = /^refs\/heads\/([^/]+)\/(signals|telemetry)$/.exec(ref);
|
|
16
|
+
return m && P.ID_RE.test(m[1]) && !['main', 'chat'].includes(m[1]) ? { actor: m[1], kind: m[2] } : null;
|
|
17
|
+
};
|
|
18
|
+
export const streamRef = (actor, kind) => {
|
|
19
|
+
const ref = `refs/heads/${actor}/${kind}`;
|
|
20
|
+
if (!streamName(ref))
|
|
21
|
+
C.fail(`bad stream actor ${actor}`);
|
|
22
|
+
return ref;
|
|
23
|
+
};
|
|
24
|
+
const scanSuffix = ({ H, R, actor, kind, head, prefix }) => {
|
|
25
|
+
const result = { head: prefix?.head ?? R.genesis, records: [...prefix?.records ?? []] };
|
|
26
|
+
const dep = streamDependencies(H, R);
|
|
27
|
+
const list = chain(H, head, result.head);
|
|
28
|
+
for (const c of readCommits(H, list.map(c => c.sha))) {
|
|
29
|
+
try {
|
|
30
|
+
const f = checkStreamRecord({ H, R, actor, kind, parent: result.head, dep }, c);
|
|
31
|
+
if (!f) {
|
|
32
|
+
result.pending = c.sha;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
result.records.push({ raw: c, fields: f });
|
|
36
|
+
result.head = c.sha;
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
result.why = `${c.sha.slice(0, 12)}: ${reason(e)}`;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!list.length && head !== result.head)
|
|
44
|
+
result.why = 'stream does not descend from its room genesis';
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
// Valid facts are immutable. Reuse only a prefix whose chat and actor stream both still extend the last view.
|
|
48
|
+
const scanned = new WeakMap();
|
|
49
|
+
export const scanStream = ({ H, R, actor, kind, head, prefix }) => {
|
|
50
|
+
const key = JSON.stringify([R.genesis, actor, kind, H.d.genesis, H.d.root, H.d.trust]);
|
|
51
|
+
const chat = rev(H, R.ref) ?? C.fail('room chat is missing');
|
|
52
|
+
const cache = scanned.get(H) ?? new Map();
|
|
53
|
+
scanned.set(H, cache);
|
|
54
|
+
const old = cache.get(key);
|
|
55
|
+
if (!prefix && old?.seen === head && old.chat === chat)
|
|
56
|
+
return old.stream;
|
|
57
|
+
const base = prefix ?? (old && ancestor(H, old.chat, chat) && ancestor(H, old.stream.head, head) ? old.stream : undefined);
|
|
58
|
+
const result = scanSuffix({ H, R, actor, kind, head, prefix: base });
|
|
59
|
+
if (kind === 'telemetry')
|
|
60
|
+
result.records = result.records.slice(-1);
|
|
61
|
+
cache.set(key, { seen: head, chat, stream: result });
|
|
62
|
+
if (cache.size > 4096)
|
|
63
|
+
cache.delete(cache.keys().next().value);
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
export const signalEvents = (R, actor, s) => s.records.map(({ raw, fields }) => ({
|
|
67
|
+
id: raw.sha, parent: raw.parents[0], roomGenesis: R.genesis, control: fields.get('Control'), actor,
|
|
68
|
+
target: fields.get('Target'), signal: fields.get('Signal'),
|
|
69
|
+
}));
|
|
70
|
+
export const validateStream = (H, R, ref, head) => {
|
|
71
|
+
const name = streamName(ref) ?? C.fail('unknown actor stream');
|
|
72
|
+
const s = scanStream({ H, R, ...name, head });
|
|
73
|
+
if (s.why || s.head !== head)
|
|
74
|
+
C.fail(s.why ?? 'stream dependencies are not yet accepted');
|
|
75
|
+
};
|
|
76
|
+
//# sourceMappingURL=streams.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Fetch-stage actor heads verified against the pinned chat; partial suffixes never replace evidence.
|
|
2
|
+
import * as C from '../../../lib/index.js';
|
|
3
|
+
import { accepted, ancestor, promote, reason, refs, rev, suffix } from './git.js';
|
|
4
|
+
import { scanStream, streamName, streamRoom } from './streams.js';
|
|
5
|
+
import { TRACK } from './types.js';
|
|
6
|
+
const syncActor = (H, fetched, ref) => {
|
|
7
|
+
const name = streamName(ref), R = streamRoom(H);
|
|
8
|
+
try {
|
|
9
|
+
const head = fetched.get(TRACK + suffix(ref)) ?? C.fail('accepted actor stream was deleted');
|
|
10
|
+
const acc = rev(H, accepted(ref)), checked = scanStream({ H, R, ...name, head });
|
|
11
|
+
if (acc && !ancestor(H, acc, head))
|
|
12
|
+
C.fail('actor stream rewrites its accepted prefix');
|
|
13
|
+
if (checked.head !== R.genesis)
|
|
14
|
+
promote(H, ref, checked.head);
|
|
15
|
+
if (checked.why)
|
|
16
|
+
C.fail(checked.why);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
H.remoteAlarms[ref] = reason(err);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export const syncStreams = (H, fetched) => {
|
|
23
|
+
const names = [...fetched.keys(), ...refs(H, 'refs/ringi/remote-accepted/').keys()]
|
|
24
|
+
.map(r => r.replace(/^refs\/(?:remotes\/origin|ringi\/remote-accepted)\//, 'refs/heads/')).filter(r => streamName(r));
|
|
25
|
+
for (const ref of new Set(names))
|
|
26
|
+
syncActor(H, fetched, ref);
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=sync-streams.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Fetch into staging refs; verify main, the pinned chat, and actor stream prefixes before promotion.
|
|
2
|
+
import * as C from '../../../lib/index.js';
|
|
3
|
+
import { pushAlarms } from './alarms.js';
|
|
4
|
+
import { git, isRemote, locked, origin, promote, reason, refs, rev, suffix } from './git.js';
|
|
5
|
+
import { checkUrl } from './keys.js';
|
|
6
|
+
import { verifyHub, verifyMain } from './main.js';
|
|
7
|
+
import { readCommits } from './commits.js';
|
|
8
|
+
import { branchData } from './branch.js';
|
|
9
|
+
import { syncStreams } from './sync-streams.js';
|
|
10
|
+
import { TRACK } from './types.js';
|
|
11
|
+
const syncChat = (H, fetched) => {
|
|
12
|
+
const e = H.endpoint;
|
|
13
|
+
if (!e)
|
|
14
|
+
return;
|
|
15
|
+
const ref = 'refs/heads/chat';
|
|
16
|
+
try {
|
|
17
|
+
const head = fetched.get(TRACK + 'chat') ?? C.fail('remote chat was deleted or is missing');
|
|
18
|
+
branchData(H, e.name, head, e.genesis);
|
|
19
|
+
promote(H, ref, head);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
H.remoteAlarms[ref] = reason(err);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
syncStreams(H, fetched);
|
|
26
|
+
};
|
|
27
|
+
const syncUnlocked = (H) => {
|
|
28
|
+
H.remoteAlarms = { ...pushAlarms(H) };
|
|
29
|
+
H.heartbeatErrors = {};
|
|
30
|
+
const url = origin(H);
|
|
31
|
+
if (!url) {
|
|
32
|
+
if (rev(H, 'refs/heads/main'))
|
|
33
|
+
verifyHub(H);
|
|
34
|
+
return H;
|
|
35
|
+
}
|
|
36
|
+
checkUrl(url);
|
|
37
|
+
git(H, ['-c', 'fetch.fsckObjects=true', 'fetch', '-q', '--no-tags', '--prune', url, `+refs/heads/*:${TRACK}*`]);
|
|
38
|
+
const fetched = refs(H, TRACK);
|
|
39
|
+
const main = fetched.get(TRACK + 'main') || C.fail('ALARM remote main was deleted or is missing; nothing is delivered');
|
|
40
|
+
try {
|
|
41
|
+
verifyMain(H, main);
|
|
42
|
+
if (H.endpoint) {
|
|
43
|
+
const genesis = readCommits(H, [H.endpoint.genesis])[0];
|
|
44
|
+
if (genesis.parents.length !== 1 || genesis.parents[0] !== main)
|
|
45
|
+
C.fail('the room main changed its frozen contract');
|
|
46
|
+
}
|
|
47
|
+
promote(H, 'refs/heads/main', main);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
C.fail(`ALARM hub ${H.name}: ${reason(err)}; nothing is delivered`);
|
|
51
|
+
}
|
|
52
|
+
H.main = rev(H, 'refs/heads/main');
|
|
53
|
+
syncChat(H, fetched);
|
|
54
|
+
for (const [ref, why] of Object.entries(H.remoteAlarms))
|
|
55
|
+
C.note(`ALARM ${suffix(ref)}: ${why}; quarantined and withheld`);
|
|
56
|
+
return H;
|
|
57
|
+
};
|
|
58
|
+
export const sync = (H) => !isRemote(H) ? H : locked(H, () => syncUnlocked(H));
|
|
59
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -4,7 +4,4 @@ export const SHA = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
|
|
|
4
4
|
export const ROOT = 'operator';
|
|
5
5
|
export const TRACK = 'refs/remotes/origin/';
|
|
6
6
|
export const ACCEPT = 'refs/ringi/remote-accepted/';
|
|
7
|
-
export const TAGS = 'refs/tags/';
|
|
8
|
-
export const TAG_TRACK = 'refs/ringi/remote-tags/';
|
|
9
|
-
export const TAG_ACCEPT = 'refs/ringi/remote-accepted-tags/';
|
|
10
7
|
//# sourceMappingURL=types.js.map
|
|
@@ -5,13 +5,10 @@ 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
|
-
export { heartbeatName, heartbeatRef, verifyHeartbeat, makeHeartbeat } from './remote/heartbeat.js';
|
|
9
|
-
export { verifySignal } from './remote/signals.js';
|
|
10
8
|
export { sync } from './remote/sync.js';
|
|
11
|
-
export { hubAdd } from './remote/bootstrap.js';
|
|
12
9
|
export { join, roomAdmit } from './remote/admit.js';
|
|
13
10
|
export { push } from './remote/push.js';
|
|
14
|
-
export { pushHeartbeat } from './remote/heartbeat-push.js';
|
|
15
11
|
export { forget } from './remote/forget.js';
|
|
16
12
|
export { strictJson } from '../../lib/index.js';
|
|
13
|
+
export { scanStream, streamName, streamRef, streamRoom, validateStream } from './remote/streams.js';
|
|
17
14
|
//# sourceMappingURL=remote.js.map
|
|
@@ -8,7 +8,10 @@ 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
|
+
import { checkUrl, signingContext } from './remote/keys.js';
|
|
12
|
+
import { origin } from './remote/git.js';
|
|
13
|
+
import { verifyMain } from './remote/main.js';
|
|
14
|
+
import { chain, linear } from './remote/signature.js';
|
|
12
15
|
// The store is a repository whose work tree holds the contract (`main`) as it is now, so Git clients (SourceTree, IDEs)
|
|
13
16
|
// open `store/` without errors, on the contract, with every room's branch beside it; `git status` stays clean.
|
|
14
17
|
export const init = (dir, home) => runGit(['init', '-q', '-b', 'main', path.dirname(dir)], { where: home });
|
|
@@ -19,14 +22,19 @@ export class Repo {
|
|
|
19
22
|
home;
|
|
20
23
|
// The handle remote.ts works on: the repository and what it learned on the last sync.
|
|
21
24
|
h;
|
|
22
|
-
constructor(hubDir, d, home, signing) {
|
|
25
|
+
constructor(hubDir, d, home, signing, endpoint) {
|
|
23
26
|
this.d = d;
|
|
24
27
|
this.home = home;
|
|
25
28
|
this.trust = d.trust;
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
if (endpoint?.authRef)
|
|
30
|
+
fail('this git transport has no TransportAuth binding provider for authRef');
|
|
31
|
+
this.dir = path.join(hubDir, endpoint?.store.path ?? d.contract?.path ?? fail(`hub ${d.name}: no explicit contract source`), '.git');
|
|
32
|
+
this.h = { name: d.name, dir: this.dir, d, endpoint, ...(d.trust === 'signed' && signing ? { signing } : {}) };
|
|
28
33
|
if (this.trust === 'signed')
|
|
29
34
|
signingContext(this.h);
|
|
35
|
+
const locator = origin(this.h);
|
|
36
|
+
if (locator)
|
|
37
|
+
checkUrl(locator);
|
|
30
38
|
if (!fs.existsSync(this.dir))
|
|
31
39
|
fail(`hub ${d.name}: no git store at ${this.dir}`);
|
|
32
40
|
this.openable();
|
|
@@ -105,7 +113,37 @@ export class Repo {
|
|
|
105
113
|
const r = this.gitOk(['show', `${rev}:${name}`]);
|
|
106
114
|
return r.status === 0 ? r.stdout : null;
|
|
107
115
|
}
|
|
116
|
+
// Copy only contract objects. A room never acquires another room's refs, evidence or attachments this way.
|
|
117
|
+
seedContract(source) {
|
|
118
|
+
if (this.rev('refs/heads/main'))
|
|
119
|
+
fail('contract seeding is only for a fresh room repository');
|
|
120
|
+
if (source.d.genesis !== this.d.genesis || source.trust !== this.trust || source.d.root !== this.d.root)
|
|
121
|
+
fail('contract source has different hub pins');
|
|
122
|
+
const ref = `refs/ringi/contract-incoming/${crypto.randomBytes(8).toString('hex')}`;
|
|
123
|
+
try {
|
|
124
|
+
this.git(['fetch', '-q', '--no-tags', '--no-write-fetch-head', source.dir, `refs/heads/main:${ref}`]);
|
|
125
|
+
const head = this.rev(ref) ?? fail('contract source has no main');
|
|
126
|
+
const commits = chain(this.h, head);
|
|
127
|
+
if (commits[0]?.sha !== this.d.genesis)
|
|
128
|
+
fail('contract source changed hub genesis');
|
|
129
|
+
linear(commits, null, () => { });
|
|
130
|
+
if (this.trust === 'signed')
|
|
131
|
+
verifyMain(this.h, head);
|
|
132
|
+
const old = this.rev('refs/heads/main');
|
|
133
|
+
if (old && !this.extends(old, head))
|
|
134
|
+
fail('contract source does not extend the room main; nothing is replaced');
|
|
135
|
+
if (old !== head && !this.advance('refs/heads/main', head, old))
|
|
136
|
+
fail('main moved while copying; retry');
|
|
137
|
+
this.checkout();
|
|
138
|
+
return head;
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
this.gitOk(['update-ref', '-d', ref]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
108
144
|
amendContract(who, text, files) {
|
|
145
|
+
if (this.h.endpoint)
|
|
146
|
+
fail('a room contract is frozen; amend the operator contract source for new rooms');
|
|
109
147
|
const head = this.contractHead();
|
|
110
148
|
const sha = this.commit(who, text, this.tree(`${head}^{tree}`, Object.entries(files).map(([p, c]) => ({ path: p, content: c, mode: '100644' }))), [head]);
|
|
111
149
|
if (!this.advance('refs/heads/main', sha, head))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GitSigningBridge: Signer signs the exact unsigned Git object. Tags append its SSH armor; commits fold it into
|
|
2
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
|
-
import { git, id,
|
|
4
|
+
import { git, id, isRemote } from './remote/git.js';
|
|
5
5
|
import { identityKey, parseKey, rootKey, signingContext } from './remote/keys.js';
|
|
6
6
|
import { checkSignature } from './remote/signature.js';
|
|
7
7
|
export const signBytes = (H, who, payload) => {
|
|
@@ -28,12 +28,4 @@ export const signCommit = (H, who, sha) => {
|
|
|
28
28
|
checkSignature(H, signed, who, identityKey(H, who));
|
|
29
29
|
return signed;
|
|
30
30
|
};
|
|
31
|
-
export const signedTag = (H, who, tag, target) => {
|
|
32
|
-
const tagger = git(H, ['var', 'GIT_COMMITTER_IDENT'], { env: ident(who) }).trim();
|
|
33
|
-
const raw = `object ${target}\ntype commit\ntag ${tag}\ntagger ${tagger}\n\n`;
|
|
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');
|
|
37
|
-
return signed;
|
|
38
|
-
};
|
|
39
31
|
//# sourceMappingURL=signing.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// transport: an explicit contract source and one self-contained repository per room.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import { fail } from '../../lib/index.js';
|
|
6
|
+
import { GitStore } from './index.js';
|
|
7
|
+
import { init } from './repo.js';
|
|
8
|
+
const context = (d, opt) => d.trust === 'signed'
|
|
9
|
+
? (typeof opt.signer === 'function' ? opt.signer() : opt.signer) ?? fail('signed hubs require an injected Signer')
|
|
10
|
+
: undefined;
|
|
11
|
+
const gitStore = (s) => s instanceof GitStore ? s : fail('git contract copying requires a git source and target');
|
|
12
|
+
const fresh = (at, endpoint, home) => {
|
|
13
|
+
const dir = path.join(at, endpoint.store.path, '.git');
|
|
14
|
+
if (fs.existsSync(at))
|
|
15
|
+
fail('room directory already exists; retained for review');
|
|
16
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true, mode: 0o700 });
|
|
17
|
+
init(dir, home);
|
|
18
|
+
};
|
|
19
|
+
// All signing remains injected. Asserted construction never asks for a signer.
|
|
20
|
+
export const gitTransport = (home, opt = {}) => ({
|
|
21
|
+
layout: 'git/3',
|
|
22
|
+
open: (at, d, endpoint) => new GitStore(at, d, home, context(d, opt), endpoint),
|
|
23
|
+
create: (at, d, genesis) => {
|
|
24
|
+
const signing = context(d, opt);
|
|
25
|
+
if (signing)
|
|
26
|
+
d.root = GitStore.rootFor(at, d, home, signing);
|
|
27
|
+
const g = genesis(d);
|
|
28
|
+
const store = GitStore.create(at, d, { home, signing }, g.files, g.text);
|
|
29
|
+
d.genesis = store.contractHead();
|
|
30
|
+
return { store, custody: signing?.custody?.(d.name, 'operator') ?? null };
|
|
31
|
+
},
|
|
32
|
+
fork: (at, d, source, endpoint) => {
|
|
33
|
+
const signing = context(d, opt);
|
|
34
|
+
fresh(at, endpoint, home);
|
|
35
|
+
const s = new GitStore(at, d, home, signing, endpoint);
|
|
36
|
+
s.seedContract(gitStore(source));
|
|
37
|
+
return s;
|
|
38
|
+
},
|
|
39
|
+
attachRoom: (at, d, endpoint) => attachRoom(at, d, endpoint, home, context(d, opt)),
|
|
40
|
+
});
|
|
41
|
+
const attachRoom = (at, d, endpoint, home, signing) => {
|
|
42
|
+
if (d.trust !== 'signed' || !endpoint.store.remote)
|
|
43
|
+
fail('remote room attachment requires signed trust and an explicit remote');
|
|
44
|
+
if (fs.existsSync(at))
|
|
45
|
+
fail('room directory already exists; retained for review');
|
|
46
|
+
const stage = `${at}.attach.${process.pid}.${crypto.randomBytes(8).toString('hex')}`;
|
|
47
|
+
try {
|
|
48
|
+
fresh(stage, endpoint, home);
|
|
49
|
+
const s = new GitStore(stage, d, home, signing, endpoint);
|
|
50
|
+
s.sync();
|
|
51
|
+
if (!s.room(endpoint.name))
|
|
52
|
+
fail('remote has no pinned chat');
|
|
53
|
+
fs.renameSync(stage, at);
|
|
54
|
+
return new GitStore(at, d, home, signing, endpoint);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -29,7 +29,7 @@ export const take = (a, o) => {
|
|
|
29
29
|
};
|
|
30
30
|
export const whoami = (a) => {
|
|
31
31
|
const { session: s, profile, members } = Agent.whoami(a);
|
|
32
|
-
out(`${s.agent} in ${s.
|
|
32
|
+
out(`${s.agent} in ${s.room} (genesis ${short(s.genesis)}, profile ${profile}), session ${s.handle}, ${s.state}\nscope: ${scopeLine(s, a.R.quorum !== null)}\nmembers: ${members.join(', ')}`);
|
|
33
33
|
};
|
|
34
34
|
export const wait = async (a, o) => {
|
|
35
35
|
const r = await Agent.wait(a, { ...options(o), max: opt(o, 'max') === undefined ? undefined : Number(opt(o, 'max')) });
|