agmsg-cloud 0.0.1 → 0.1.0-rc.4
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 +39 -2
- package/dist/src/api.js +517 -0
- package/dist/src/authenticated-digest.js +234 -0
- package/dist/src/browser.js +241 -0
- package/dist/src/ceremony.js +181 -0
- package/dist/src/commands/approve.js +392 -0
- package/dist/src/commands/connect.js +273 -0
- package/dist/src/commands/fetch.js +249 -0
- package/dist/src/commands/login.js +334 -0
- package/dist/src/commands/logout.js +74 -0
- package/dist/src/commands/pull.js +80 -0
- package/dist/src/commands/request.js +371 -0
- package/dist/src/commands/sync.js +138 -0
- package/dist/src/commands/vault.js +478 -0
- package/dist/src/commands/watch.js +47 -0
- package/dist/src/config.js +34 -0
- package/dist/src/credentials.js +374 -0
- package/dist/src/device-slot.js +148 -0
- package/dist/src/filelock.js +167 -0
- package/dist/src/index.js +242 -0
- package/dist/src/ledger.js +296 -0
- package/dist/src/machine-name.js +90 -0
- package/dist/src/oss-env.js +49 -0
- package/dist/src/oss.js +289 -0
- package/dist/src/paths.js +8 -0
- package/dist/src/pending.js +330 -0
- package/dist/src/pick-request.js +56 -0
- package/dist/src/preflight.js +257 -0
- package/dist/src/recovery-key.js +386 -0
- package/dist/src/sas.js +18 -0
- package/dist/src/secure-store.js +176 -0
- package/dist/src/shell-arg.js +18 -0
- package/dist/src/slot-advice.js +74 -0
- package/dist/src/vault-container.js +115 -0
- package/dist/src/vault-crypto.js +190 -0
- package/dist/src/vault-protocol.js +358 -0
- package/dist/src/version.js +57 -0
- package/node_modules/@agmsg-cloud/sas-core/dist/src/bech32.d.ts +17 -0
- package/node_modules/@agmsg-cloud/sas-core/dist/src/bech32.js +103 -0
- package/node_modules/@agmsg-cloud/sas-core/dist/src/index.d.ts +17 -0
- package/node_modules/@agmsg-cloud/sas-core/dist/src/index.js +147 -0
- package/node_modules/@agmsg-cloud/sas-core/package.json +30 -0
- package/package.json +50 -7
- package/bin/agmsg-cloud.js +0 -4
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, parse } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
// This package's own name, as its manifest declares it. Not a duplicate of the
|
|
5
|
+
// version — an identity, and the only thing that tells one package.json from
|
|
6
|
+
// another while walking up a tree that may contain several.
|
|
7
|
+
const PACKAGE_NAME = 'agmsg-cloud';
|
|
8
|
+
// The version this build was published as.
|
|
9
|
+
//
|
|
10
|
+
// Read from the package's own `package.json` at runtime rather than baked into
|
|
11
|
+
// a constant, because a constant is a second place to update and the one that
|
|
12
|
+
// gets forgotten: a build that says the wrong version is worse than one that
|
|
13
|
+
// says none, since the answer looks authoritative either way. npm always
|
|
14
|
+
// includes package.json in a tarball, so it is there for a published install.
|
|
15
|
+
//
|
|
16
|
+
// Found by walking UP from this module rather than by a fixed relative path,
|
|
17
|
+
// because the depth differs between the two places this file runs from:
|
|
18
|
+
// `src/version.ts` sits one level under the package root, `dist/src/version.js`
|
|
19
|
+
// sits two. A path correct in one is wrong in the other, and the one that would
|
|
20
|
+
// be wrong is the published one.
|
|
21
|
+
//
|
|
22
|
+
// The walk answers only from a manifest that NAMES this package. Without that
|
|
23
|
+
// check the first non-empty version above the module wins, and the installs
|
|
24
|
+
// most likely to hit it are the broken ones — a package.json missing or
|
|
25
|
+
// stripped of its version is exactly when a monorepo root, an npx cache entry,
|
|
26
|
+
// or a parent workspace supplies its own. Reporting a stranger's version is the
|
|
27
|
+
// failure this command exists to prevent, wearing the shape of a success.
|
|
28
|
+
export function packageVersion(moduleUrl = import.meta.url) {
|
|
29
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
30
|
+
const { root } = parse(dir);
|
|
31
|
+
for (;;) {
|
|
32
|
+
let manifest;
|
|
33
|
+
try {
|
|
34
|
+
manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// No manifest here, or one that is not JSON. Either way it does not
|
|
38
|
+
// identify itself, so it cannot be this package's answer.
|
|
39
|
+
manifest = undefined;
|
|
40
|
+
}
|
|
41
|
+
if (manifest?.name === PACKAGE_NAME) {
|
|
42
|
+
const version = manifest.version;
|
|
43
|
+
if (typeof version === 'string' && version !== '')
|
|
44
|
+
return version;
|
|
45
|
+
// Fail closed. This IS the package and it cannot say what it is; climbing
|
|
46
|
+
// past would hand the question to whatever sits above, which is how a
|
|
47
|
+
// broken install comes to announce a stranger's version.
|
|
48
|
+
throw new Error(`the installed ${PACKAGE_NAME} package.json carries no version — this install cannot say which build it is`);
|
|
49
|
+
}
|
|
50
|
+
if (dir === root)
|
|
51
|
+
break;
|
|
52
|
+
dir = dirname(dir);
|
|
53
|
+
}
|
|
54
|
+
// Reached when the package was installed without its manifest. Said, not
|
|
55
|
+
// guessed — see the note above.
|
|
56
|
+
throw new Error(`cannot determine the installed version: no ${PACKAGE_NAME} package.json above this module`);
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode a canonical lowercase bech32 string, or return null.
|
|
3
|
+
*
|
|
4
|
+
* Mixed case is rejected rather than folded: BIP-173 allows an all-uppercase
|
|
5
|
+
* form, but folding it here would mean two spellings of one recipient, and the
|
|
6
|
+
* commitment binds the STRING. Anything a caller has to normalize first is
|
|
7
|
+
* something the two sides could normalize differently.
|
|
8
|
+
*/
|
|
9
|
+
export declare function decodeBech32(input: string): {
|
|
10
|
+
hrp: string;
|
|
11
|
+
data: number[];
|
|
12
|
+
} | null;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a string is a canonical age recipient: bech32 with the `age` human-
|
|
15
|
+
* readable part, a valid checksum, and exactly a 32-byte X25519 payload.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isCanonicalAgeRecipient(input: string): boolean;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Bech32 (BIP-173) decoding, enough to decide whether a string is a canonical
|
|
2
|
+
// age recipient.
|
|
3
|
+
//
|
|
4
|
+
// This exists because a shape check is not a canonicality check. `age1` followed
|
|
5
|
+
// by 58 lowercase alphanumerics matches strings that are not recipients at all:
|
|
6
|
+
// `b`, `i`, `o` and `1` are excluded from the bech32 data alphabet precisely
|
|
7
|
+
// because they are easy to confuse with `6`, `1`, `0` and `l`, and the six
|
|
8
|
+
// trailing characters are a checksum that a length check never looks at.
|
|
9
|
+
//
|
|
10
|
+
// Spec §2 says reject rather than normalize. A validator that accepts a
|
|
11
|
+
// checksum-broken recipient does not merely admit junk: it lets two
|
|
12
|
+
// implementations disagree about whether a transcript is valid, and the whole
|
|
13
|
+
// point of the ceremony is that both sides commit to the SAME device key.
|
|
14
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
15
|
+
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
16
|
+
// An X25519 public key. age recipients carry exactly this much payload, so a
|
|
17
|
+
// well-formed bech32 string of the wrong length is still not a recipient.
|
|
18
|
+
const AGE_PAYLOAD_BYTES = 32;
|
|
19
|
+
const AGE_HRP = 'age';
|
|
20
|
+
function polymod(values) {
|
|
21
|
+
let chk = 1;
|
|
22
|
+
for (const value of values) {
|
|
23
|
+
const top = chk >> 25;
|
|
24
|
+
chk = ((chk & 0x1ffffff) << 5) ^ value;
|
|
25
|
+
for (let i = 0; i < 5; i++) {
|
|
26
|
+
if ((top >> i) & 1)
|
|
27
|
+
chk ^= GENERATOR[i];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return chk;
|
|
31
|
+
}
|
|
32
|
+
function hrpExpand(hrp) {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (let i = 0; i < hrp.length; i++)
|
|
35
|
+
out.push(hrp.charCodeAt(i) >> 5);
|
|
36
|
+
out.push(0);
|
|
37
|
+
for (let i = 0; i < hrp.length; i++)
|
|
38
|
+
out.push(hrp.charCodeAt(i) & 31);
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
// 5-bit groups back to 8-bit bytes. The two trailing checks are the part a
|
|
42
|
+
// naive implementation drops: leftover bits must be fewer than a full byte and
|
|
43
|
+
// must be zero, or several distinct encodings would decode to the same key.
|
|
44
|
+
function convert5to8(data) {
|
|
45
|
+
let acc = 0;
|
|
46
|
+
let bits = 0;
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const value of data) {
|
|
49
|
+
acc = (acc << 5) | value;
|
|
50
|
+
bits += 5;
|
|
51
|
+
while (bits >= 8) {
|
|
52
|
+
bits -= 8;
|
|
53
|
+
out.push((acc >> bits) & 0xff);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (bits >= 5)
|
|
57
|
+
return null; // more padding than a partial group can justify
|
|
58
|
+
if ((acc << (8 - bits)) & 0xff)
|
|
59
|
+
return null; // padding bits must be zero
|
|
60
|
+
return Uint8Array.from(out);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Decode a canonical lowercase bech32 string, or return null.
|
|
64
|
+
*
|
|
65
|
+
* Mixed case is rejected rather than folded: BIP-173 allows an all-uppercase
|
|
66
|
+
* form, but folding it here would mean two spellings of one recipient, and the
|
|
67
|
+
* commitment binds the STRING. Anything a caller has to normalize first is
|
|
68
|
+
* something the two sides could normalize differently.
|
|
69
|
+
*/
|
|
70
|
+
export function decodeBech32(input) {
|
|
71
|
+
if (input !== input.toLowerCase())
|
|
72
|
+
return null;
|
|
73
|
+
const separator = input.lastIndexOf('1');
|
|
74
|
+
if (separator < 1 || separator + 7 > input.length)
|
|
75
|
+
return null;
|
|
76
|
+
const hrp = input.slice(0, separator);
|
|
77
|
+
for (let i = 0; i < hrp.length; i++) {
|
|
78
|
+
const code = hrp.charCodeAt(i);
|
|
79
|
+
if (code < 33 || code > 126)
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const data = [];
|
|
83
|
+
for (const char of input.slice(separator + 1)) {
|
|
84
|
+
const index = CHARSET.indexOf(char);
|
|
85
|
+
if (index === -1)
|
|
86
|
+
return null;
|
|
87
|
+
data.push(index);
|
|
88
|
+
}
|
|
89
|
+
if (polymod([...hrpExpand(hrp), ...data]) !== 1)
|
|
90
|
+
return null;
|
|
91
|
+
return { hrp, data: data.slice(0, data.length - 6) };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Whether a string is a canonical age recipient: bech32 with the `age` human-
|
|
95
|
+
* readable part, a valid checksum, and exactly a 32-byte X25519 payload.
|
|
96
|
+
*/
|
|
97
|
+
export function isCanonicalAgeRecipient(input) {
|
|
98
|
+
const decoded = decodeBech32(input);
|
|
99
|
+
if (!decoded || decoded.hrp !== AGE_HRP)
|
|
100
|
+
return false;
|
|
101
|
+
const payload = convert5to8(decoded.data);
|
|
102
|
+
return payload !== null && payload.length === AGE_PAYLOAD_BYTES;
|
|
103
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { decodeBech32, isCanonicalAgeRecipient } from './bech32.js';
|
|
2
|
+
export type Bytes32 = Uint8Array;
|
|
3
|
+
export interface SasCommitment {
|
|
4
|
+
commitmentHex: string;
|
|
5
|
+
openingNonce: Bytes32;
|
|
6
|
+
}
|
|
7
|
+
export declare function deriveSasCommitment(devicePubkey: string, openingNonce: Bytes32): string;
|
|
8
|
+
export declare function deriveApproverCommitment(approverNonce: Bytes32, handoffDigestHex: string): string;
|
|
9
|
+
export declare function deriveRequestNonce(approverNonce: Bytes32, openingNonce: Bytes32): Bytes32;
|
|
10
|
+
export declare function deriveSas(devicePubkey: string, requestNonce: Bytes32, handoffDigestHex: string): string;
|
|
11
|
+
export declare function verifySasCommitment(commitmentHex: string, devicePubkey: string, openingNonce: Bytes32): boolean;
|
|
12
|
+
export declare function verifyApproverCommitment(commitmentHex: string, approverNonce: Bytes32, handoffDigestHex: string): boolean;
|
|
13
|
+
export declare function createSasCommitment(devicePubkey: string, randomBytes?: (length: 32) => Bytes32): SasCommitment;
|
|
14
|
+
export declare function createApproverCommitment(handoffDigestHex: string, randomBytes?: (length: 32) => Bytes32): {
|
|
15
|
+
commitmentHex: string;
|
|
16
|
+
approverNonce: Bytes32;
|
|
17
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { isCanonicalAgeRecipient } from './bech32.js';
|
|
3
|
+
// Re-exported so the routes validate with the SAME function the derivations
|
|
4
|
+
// use. A route that kept its own check is how the two drift apart.
|
|
5
|
+
export { decodeBech32, isCanonicalAgeRecipient } from './bech32.js';
|
|
6
|
+
const NONCE_BYTES = 32;
|
|
7
|
+
const DIGEST_HEX = /^[0-9a-f]{64}$/;
|
|
8
|
+
// §3.5: the largest multiple of 100,000,000 below 2^32. Candidates at or above
|
|
9
|
+
// it are rejected so every eight-digit value stays equally likely.
|
|
10
|
+
const REJECT_AT = 4_200_000_000;
|
|
11
|
+
const SAS_MODULUS = 100_000_000;
|
|
12
|
+
function ascii(s) {
|
|
13
|
+
return Buffer.from(s, 'ascii');
|
|
14
|
+
}
|
|
15
|
+
function u32be(n) {
|
|
16
|
+
const b = Buffer.alloc(4);
|
|
17
|
+
b.writeUInt32BE(n >>> 0);
|
|
18
|
+
return b;
|
|
19
|
+
}
|
|
20
|
+
// LP(x) = U32BE(byte_length(x)) || x (§2). The length prefix is what stops a
|
|
21
|
+
// longer pubkey from being confused with a shorter one plus the bytes after it.
|
|
22
|
+
function lengthPrefixed(x) {
|
|
23
|
+
return Buffer.concat([u32be(x.length), x]);
|
|
24
|
+
}
|
|
25
|
+
function sha256(...parts) {
|
|
26
|
+
return createHash('sha256').update(Buffer.concat(parts)).digest();
|
|
27
|
+
}
|
|
28
|
+
// §2: reject rather than normalize. Trimming or lowercasing here would make two
|
|
29
|
+
// callers that disagree about the input agree about the digest, which is the
|
|
30
|
+
// substitution this protocol is built to detect.
|
|
31
|
+
function requireDevicePubkey(devicePubkey) {
|
|
32
|
+
// Checked as an ENCODING, not as a shape. `age1` plus 58 lowercase
|
|
33
|
+
// alphanumerics matches strings that are not recipients: the bech32 alphabet
|
|
34
|
+
// excludes b, i, o and 1, and the last six characters are a checksum a length
|
|
35
|
+
// test never reads. A validator that accepts those lets two implementations
|
|
36
|
+
// disagree about whether a transcript is valid, which is exactly the
|
|
37
|
+
// disagreement the ceremony exists to make impossible.
|
|
38
|
+
if (!isCanonicalAgeRecipient(devicePubkey)) {
|
|
39
|
+
throw new Error('device_pubkey is not a canonical age recipient');
|
|
40
|
+
}
|
|
41
|
+
return ascii(devicePubkey);
|
|
42
|
+
}
|
|
43
|
+
function requireNonce(nonce, label) {
|
|
44
|
+
if (!(nonce instanceof Uint8Array) || nonce.length !== NONCE_BYTES) {
|
|
45
|
+
throw new Error(`${label} must be exactly ${NONCE_BYTES} bytes`);
|
|
46
|
+
}
|
|
47
|
+
return Buffer.from(nonce);
|
|
48
|
+
}
|
|
49
|
+
function requireDigestHex(hex, label) {
|
|
50
|
+
if (!DIGEST_HEX.test(hex)) {
|
|
51
|
+
throw new Error(`${label} must be 64 lowercase hexadecimal characters`);
|
|
52
|
+
}
|
|
53
|
+
return Buffer.from(hex, 'hex');
|
|
54
|
+
}
|
|
55
|
+
function csprng(length) {
|
|
56
|
+
return new Uint8Array(randomBytes(length));
|
|
57
|
+
}
|
|
58
|
+
// §3.1 — B's commitment to (device_pubkey, b).
|
|
59
|
+
export function deriveSasCommitment(devicePubkey, openingNonce) {
|
|
60
|
+
return sha256(ascii('agmsg-sas-commit-v1\0'), lengthPrefixed(requireDevicePubkey(devicePubkey)), requireNonce(openingNonce, 'opening nonce')).toString('hex');
|
|
61
|
+
}
|
|
62
|
+
// §3.2 — A's commitment to `a` AND to the SNAPSHOT it will hand over.
|
|
63
|
+
//
|
|
64
|
+
// The digest is bound HERE, before either side opens, and that ordering is the
|
|
65
|
+
// whole point. The previous version fixed the eight digits first and produced
|
|
66
|
+
// the handed-over state afterwards, so the side that produced it last was free
|
|
67
|
+
// to vary it: an attacker could search offline for a state whose digest still
|
|
68
|
+
// produced the same eight digits, and the brute force that two-party
|
|
69
|
+
// commitment removes walked back in through the handover.
|
|
70
|
+
//
|
|
71
|
+
// What this closes, stated as knowledge rather than as ignorance: A commits
|
|
72
|
+
// before B opens its committed `device_pubkey` and `b`. A is NOT blind here —
|
|
73
|
+
// it has fetched B's request and holds the commitment and the label — but
|
|
74
|
+
// neither of those lets it aim at the resulting digits, because B's commitment
|
|
75
|
+
// hides those two values behind a fresh random nonce and both are SAS inputs.
|
|
76
|
+
// Within the attempt, A can no longer change what it will hand over; starting
|
|
77
|
+
// a fresh attempt is a different act, bounded by §4.1 (raised in review).
|
|
78
|
+
//
|
|
79
|
+
// It is still A's own nonce that hides the digest until the opening: the
|
|
80
|
+
// commitment is over `a || handoff_digest`, and `a` is 32 random bytes, so the
|
|
81
|
+
// commitment discloses nothing about which snapshot is being handed over.
|
|
82
|
+
export function deriveApproverCommitment(approverNonce, handoffDigestHex) {
|
|
83
|
+
return sha256(
|
|
84
|
+
// The tag stays v1, and the INPUT is what changed. Nothing has ever been
|
|
85
|
+
// shipped under the old input, so there is no deployed derivation for this
|
|
86
|
+
// one to collide with — and cutting a v2 would mean a second protocol
|
|
87
|
+
// identity, wire negotiation, and a rejection boundary for values that do
|
|
88
|
+
// not exist. The tag names the spec; the spec is being corrected in place
|
|
89
|
+
// (raised in review).
|
|
90
|
+
ascii('agmsg-sas-approver-commit-v1\0'), requireNonce(approverNonce, 'approver nonce'), requireDigestHex(handoffDigestHex, 'handoff digest')).toString('hex');
|
|
91
|
+
}
|
|
92
|
+
// §3.4 — the nonce the SAS is derived from. Neither side alone chooses it.
|
|
93
|
+
export function deriveRequestNonce(approverNonce, openingNonce) {
|
|
94
|
+
return new Uint8Array(sha256(ascii('agmsg-sas-request-nonce-v1\0'), requireNonce(approverNonce, 'approver nonce'), requireNonce(openingNonce, 'opening nonce')));
|
|
95
|
+
}
|
|
96
|
+
// §3.5 — eight decimal digits as dddd-dddd, rejection-sampled so no value is
|
|
97
|
+
// more likely than another. Leading zeroes are significant.
|
|
98
|
+
export function deriveSas(devicePubkey, requestNonce, handoffDigestHex) {
|
|
99
|
+
const seed = sha256(ascii('agmsg-sas-v1\0'), lengthPrefixed(requireDevicePubkey(devicePubkey)), requireNonce(requestNonce, 'request nonce'),
|
|
100
|
+
// The digits attest the BUNDLE as well as the device. Without this the
|
|
101
|
+
// human comparison says "the machine I am talking to holds this key" and
|
|
102
|
+
// says nothing about what is being handed to it, which is the half the
|
|
103
|
+
// committed digest exists to cover — and a code that covers one of two
|
|
104
|
+
// things reads to the person comparing it as covering the ceremony.
|
|
105
|
+
requireDigestHex(handoffDigestHex, 'handoff digest'));
|
|
106
|
+
for (let counter = 0;; counter += 1) {
|
|
107
|
+
const block = sha256(ascii('agmsg-sas-code-v1\0'), seed, u32be(counter));
|
|
108
|
+
const candidate = block.readUInt32BE(0);
|
|
109
|
+
if (candidate < REJECT_AT) {
|
|
110
|
+
const digits = String(candidate % SAS_MODULUS).padStart(8, '0');
|
|
111
|
+
return `${digits.slice(0, 4)}-${digits.slice(4)}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Both verifiers compare decoded digest BYTES in constant time, after strict
|
|
116
|
+
// encoding checks (§7). A string compare would leak how much of a guessed
|
|
117
|
+
// commitment was right through timing, and would also accept an uppercase or
|
|
118
|
+
// padded form that the derivation never produces.
|
|
119
|
+
export function verifySasCommitment(commitmentHex, devicePubkey, openingNonce) {
|
|
120
|
+
const offered = requireDigestHex(commitmentHex, 'commitment');
|
|
121
|
+
const computed = Buffer.from(deriveSasCommitment(devicePubkey, openingNonce), 'hex');
|
|
122
|
+
return timingSafeEqual(offered, computed);
|
|
123
|
+
}
|
|
124
|
+
export function verifyApproverCommitment(commitmentHex, approverNonce, handoffDigestHex) {
|
|
125
|
+
const offered = requireDigestHex(commitmentHex, 'approver commitment');
|
|
126
|
+
const computed = Buffer.from(deriveApproverCommitment(approverNonce, handoffDigestHex), 'hex');
|
|
127
|
+
return timingSafeEqual(offered, computed);
|
|
128
|
+
}
|
|
129
|
+
// Randomness is injectable for tests only (§7); production callers take the
|
|
130
|
+
// default, which is the platform CSPRNG.
|
|
131
|
+
export function createSasCommitment(devicePubkey, randomBytes = csprng) {
|
|
132
|
+
const openingNonce = randomBytes(NONCE_BYTES);
|
|
133
|
+
return { commitmentHex: deriveSasCommitment(devicePubkey, openingNonce), openingNonce };
|
|
134
|
+
}
|
|
135
|
+
// The digest is a parameter, not something this can produce: A must have
|
|
136
|
+
// exported the snapshot locally before it can commit. That order is the change
|
|
137
|
+
// — a caller that has no digest yet cannot get a commitment out of this, which
|
|
138
|
+
// is the compile-time shape of "nothing is committed to before the state being
|
|
139
|
+
// handed over is fixed". The bundle carrying identity material is generated
|
|
140
|
+
// after the user answers, and §4 checks it against this digest.
|
|
141
|
+
export function createApproverCommitment(handoffDigestHex, randomBytes = csprng) {
|
|
142
|
+
const approverNonce = randomBytes(NONCE_BYTES);
|
|
143
|
+
return {
|
|
144
|
+
commitmentHex: deriveApproverCommitment(approverNonce, handoffDigestHex),
|
|
145
|
+
approverNonce,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agmsg-cloud/sas-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/src/index.js",
|
|
10
|
+
"types": "dist/src/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/src/index.d.ts",
|
|
14
|
+
"default": "./dist/src/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.build.json",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.20.1",
|
|
24
|
+
"typescript": "7.0.2",
|
|
25
|
+
"vitest": "4.1.10"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/src"
|
|
29
|
+
]
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agmsg-cloud",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
"version": "0.1.0-rc.4",
|
|
4
|
+
"description": "Companion CLI for the agmsg cloud service: connect a team, join from another machine, and back up its keys.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"agmsg-cloud": "dist/src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/src",
|
|
15
|
+
"!dist/src/**/*.map"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/JugemuAI/agmsg-cloud.git",
|
|
20
|
+
"directory": "packages/cli"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://agmsg.ai",
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build:deps": "pnpm --filter @agmsg-cloud/sas-core build",
|
|
28
|
+
"prebuild": "pnpm run build:deps",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"start": "node dist/src/index.js",
|
|
31
|
+
"pretest": "pnpm run build:deps",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"pretypecheck": "pnpm run build:deps",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"lint": "tsx scripts/check-printed-commands.ts && tsx scripts/check-dist-is-current.ts && tsx scripts/check-readme-commands.ts && tsx scripts/check-handles.ts",
|
|
36
|
+
"prepack": "pnpm run build && tsx scripts/check-dist-is-current.ts && tsx scripts/check-readme-commands.ts && tsx scripts/check-handles.ts"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@agmsg-cloud/sas-core": "workspace:*",
|
|
40
|
+
"hash-wasm": "4.12.0",
|
|
41
|
+
"uuid": "14.0.1"
|
|
42
|
+
},
|
|
43
|
+
"bundleDependencies": [
|
|
44
|
+
"@agmsg-cloud/sas-core"
|
|
45
|
+
],
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^22.20.1",
|
|
48
|
+
"tsx": "4.23.1",
|
|
49
|
+
"typescript": "7.0.2",
|
|
50
|
+
"typescript5": "npm:typescript@5.9.3",
|
|
51
|
+
"vitest": "4.1.10"
|
|
52
|
+
}
|
|
10
53
|
}
|
package/bin/agmsg-cloud.js
DELETED