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.
Files changed (44) hide show
  1. package/README.md +39 -2
  2. package/dist/src/api.js +517 -0
  3. package/dist/src/authenticated-digest.js +234 -0
  4. package/dist/src/browser.js +241 -0
  5. package/dist/src/ceremony.js +181 -0
  6. package/dist/src/commands/approve.js +392 -0
  7. package/dist/src/commands/connect.js +273 -0
  8. package/dist/src/commands/fetch.js +249 -0
  9. package/dist/src/commands/login.js +334 -0
  10. package/dist/src/commands/logout.js +74 -0
  11. package/dist/src/commands/pull.js +80 -0
  12. package/dist/src/commands/request.js +371 -0
  13. package/dist/src/commands/sync.js +138 -0
  14. package/dist/src/commands/vault.js +478 -0
  15. package/dist/src/commands/watch.js +47 -0
  16. package/dist/src/config.js +34 -0
  17. package/dist/src/credentials.js +374 -0
  18. package/dist/src/device-slot.js +148 -0
  19. package/dist/src/filelock.js +167 -0
  20. package/dist/src/index.js +242 -0
  21. package/dist/src/ledger.js +296 -0
  22. package/dist/src/machine-name.js +90 -0
  23. package/dist/src/oss-env.js +49 -0
  24. package/dist/src/oss.js +289 -0
  25. package/dist/src/paths.js +8 -0
  26. package/dist/src/pending.js +330 -0
  27. package/dist/src/pick-request.js +56 -0
  28. package/dist/src/preflight.js +257 -0
  29. package/dist/src/recovery-key.js +386 -0
  30. package/dist/src/sas.js +18 -0
  31. package/dist/src/secure-store.js +176 -0
  32. package/dist/src/shell-arg.js +18 -0
  33. package/dist/src/slot-advice.js +74 -0
  34. package/dist/src/vault-container.js +115 -0
  35. package/dist/src/vault-crypto.js +190 -0
  36. package/dist/src/vault-protocol.js +358 -0
  37. package/dist/src/version.js +57 -0
  38. package/node_modules/@agmsg-cloud/sas-core/dist/src/bech32.d.ts +17 -0
  39. package/node_modules/@agmsg-cloud/sas-core/dist/src/bech32.js +103 -0
  40. package/node_modules/@agmsg-cloud/sas-core/dist/src/index.d.ts +17 -0
  41. package/node_modules/@agmsg-cloud/sas-core/dist/src/index.js +147 -0
  42. package/node_modules/@agmsg-cloud/sas-core/package.json +30 -0
  43. package/package.json +50 -7
  44. 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.1",
4
- "description": "Reserved: companion CLI for the agmsg cloud service.",
5
- "bin": { "agmsg-cloud": "bin/agmsg-cloud.js" },
6
- "files": ["bin/", "README.md"],
7
- "repository": { "type": "git", "url": "git+https://github.com/JugemuAI/agmsg-cloud.git" },
8
- "author": "Jugemu Labs, Inc.",
9
- "license": "UNLICENSED"
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
  }
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- // Placeholder. This package name is reserved for the agmsg cloud
3
- // companion CLI. Nothing to run yet.
4
- console.log('agmsg-cloud: coming soon.');