openmates 0.15.0-alpha.99 → 0.16.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/dist/chunk-IWKP55ZB.js +205 -0
- package/dist/{chunk-3ZTRVFOG.js → chunk-SVL7BFYD.js} +3436 -784
- package/dist/cli-DJQVrG6j.d.ts +2735 -0
- package/dist/cli.d.ts +1 -12
- package/dist/cli.js +12 -1
- package/dist/index.d.ts +215 -2571
- package/dist/index.js +8 -1
- package/dist/remoteAccessCrypto.d.ts +41 -0
- package/dist/remoteAccessCrypto.js +14 -0
- package/package.json +7 -5
- package/templates/core/docker-compose.selfhost.yml +32 -2
package/README.md
CHANGED
|
@@ -144,9 +144,12 @@ review the spend confirmation before live runs.
|
|
|
144
144
|
### Local source bridge
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
openmates remote-access
|
|
148
|
-
openmates remote-access --path ./my-repo --path ../shared
|
|
149
|
-
openmates remote-access --path ./my-repo --json
|
|
147
|
+
openmates remote-access --personal
|
|
148
|
+
openmates remote-access --team <team> --path ./my-repo --path ../shared
|
|
149
|
+
openmates remote-access --personal --path ./my-repo --json
|
|
150
|
+
openmates projects files list <project> --personal --json
|
|
151
|
+
openmates projects files search <project> <query> --team <team> --source <source-id> --json
|
|
152
|
+
openmates projects files read <project> <relative-path> --team <team> --source <source-id> --json
|
|
150
153
|
```
|
|
151
154
|
|
|
152
155
|
The command discovers Git repositories below the current folder by default;
|
|
@@ -157,6 +160,14 @@ selected text previews. The bridge is read-only, excludes ignored, protected,
|
|
|
157
160
|
binary, and out-of-root files, and never uploads a preview unless you explicitly
|
|
158
161
|
choose to upload it.
|
|
159
162
|
|
|
163
|
+
Interactive hosting confirms Personal or Team context before discovery.
|
|
164
|
+
Non-interactive/JSON hosting and live file requests require `--personal` or
|
|
165
|
+
`--team <team>`. Team permissions are enforced by the API; live filesystem
|
|
166
|
+
methods remain CLI-only and are not exposed by the npm SDK.
|
|
167
|
+
Project deletion requires the exact Project ID, and source removal requires the
|
|
168
|
+
exact source ID, both in the CLI prompt and in the server request. Generic
|
|
169
|
+
boolean confirmation is not supported.
|
|
170
|
+
|
|
160
171
|
Project keys authenticate end-to-end encryption between trusted clients. The
|
|
161
172
|
backend receives routing metadata and opaque ciphertext, not filesystem paths,
|
|
162
173
|
queries, snippets, file contents, or encryption keys. Local source associations
|
|
@@ -304,12 +315,12 @@ intentionally want the chat saved to the OpenMates account.
|
|
|
304
315
|
|
|
305
316
|
## Versioning
|
|
306
317
|
|
|
307
|
-
OpenMates shows the short product line, for example `v0.
|
|
318
|
+
OpenMates shows the short product line, for example `v0.16`, in the web app.
|
|
308
319
|
The npm package uses exact artifact versions:
|
|
309
320
|
|
|
310
|
-
- `0.
|
|
321
|
+
- `0.16.0-alpha.N` is a prerelease from the `dev` branch published under the
|
|
311
322
|
`alpha` npm tag.
|
|
312
|
-
- `0.
|
|
323
|
+
- `0.16.0` is a stable release from `main` published under the `latest` npm tag.
|
|
313
324
|
|
|
314
325
|
Install stable releases with `npm install -g openmates`. Install prereleases with
|
|
315
326
|
`npm install -g openmates@alpha`.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/remoteAccessCrypto.ts
|
|
2
|
+
import { webcrypto } from "crypto";
|
|
3
|
+
import nacl from "tweetnacl";
|
|
4
|
+
var cryptoApi = globalThis.crypto ?? webcrypto;
|
|
5
|
+
var KEY_BYTES = 32;
|
|
6
|
+
var NONCE_BYTES = 12;
|
|
7
|
+
var MAX_PAYLOAD_BYTES = 200 * 1024;
|
|
8
|
+
var RemoteAccessReplayGuard = class {
|
|
9
|
+
seen = /* @__PURE__ */ new Set();
|
|
10
|
+
assertUnused(nonce) {
|
|
11
|
+
if (this.seen.has(nonce)) throw new Error("replayed remote-access envelope");
|
|
12
|
+
}
|
|
13
|
+
markUsed(nonce) {
|
|
14
|
+
this.seen.add(nonce);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
async function createRemoteAccessHandshake(projectKey, identity, role) {
|
|
18
|
+
assertProjectKey(projectKey);
|
|
19
|
+
validateIdentity(identity);
|
|
20
|
+
const protocolVersion = protocolVersionForIdentity(identity);
|
|
21
|
+
const privateKey = cryptoApi.getRandomValues(new Uint8Array(KEY_BYTES));
|
|
22
|
+
const publicKey = nacl.scalarMult.base(privateKey);
|
|
23
|
+
const publicKeyText = toBase64Url(publicKey);
|
|
24
|
+
return {
|
|
25
|
+
privateKey: toBase64Url(privateKey),
|
|
26
|
+
handshake: {
|
|
27
|
+
version: protocolVersion,
|
|
28
|
+
role,
|
|
29
|
+
publicKey: publicKeyText,
|
|
30
|
+
authenticationTag: await authenticateHandshake(projectKey, identity, role, publicKeyText)
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function deriveRemoteAccessSessionKey(projectKey, identity, localRole, localPrivateKey, localHandshake, remoteHandshake) {
|
|
35
|
+
assertProjectKey(projectKey);
|
|
36
|
+
validateIdentity(identity);
|
|
37
|
+
const protocolVersion = protocolVersionForIdentity(identity);
|
|
38
|
+
const remoteRole = localRole === "requester" ? "source" : "requester";
|
|
39
|
+
if (localHandshake.role !== localRole || remoteHandshake.role !== remoteRole) {
|
|
40
|
+
throw new Error("handshake authentication failed");
|
|
41
|
+
}
|
|
42
|
+
await verifyHandshake(projectKey, identity, localHandshake);
|
|
43
|
+
await verifyHandshake(projectKey, identity, remoteHandshake);
|
|
44
|
+
const privateKey = fromBase64Url(localPrivateKey, KEY_BYTES);
|
|
45
|
+
const remotePublicKey = fromBase64Url(remoteHandshake.publicKey, KEY_BYTES);
|
|
46
|
+
const sharedSecret = nacl.scalarMult(privateKey, remotePublicKey);
|
|
47
|
+
if (sharedSecret.every((value) => value === 0)) throw new Error("handshake shared secret invalid");
|
|
48
|
+
const requesterPublicKey = localRole === "requester" ? localHandshake.publicKey : remoteHandshake.publicKey;
|
|
49
|
+
const sourcePublicKey = localRole === "source" ? localHandshake.publicKey : remoteHandshake.publicKey;
|
|
50
|
+
return hkdf(
|
|
51
|
+
sharedSecret,
|
|
52
|
+
new TextEncoder().encode(`openmates:project-remote-access:v${protocolVersion}`),
|
|
53
|
+
await digest(transcript(identity, requesterPublicKey, sourcePublicKey))
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
async function sealRemoteAccessEnvelope(sessionKey, identity, requestId, direction, plaintext) {
|
|
57
|
+
assertSessionKey(sessionKey);
|
|
58
|
+
validateIdentity(identity);
|
|
59
|
+
const protocolVersion = protocolVersionForIdentity(identity);
|
|
60
|
+
if (!requestId || plaintext.length > MAX_PAYLOAD_BYTES) throw new Error("invalid remote-access envelope payload");
|
|
61
|
+
const nonce = cryptoApi.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
62
|
+
const key = await cryptoApi.subtle.importKey("raw", arrayBuffer(sessionKey), "AES-GCM", false, ["encrypt"]);
|
|
63
|
+
const ciphertext = await cryptoApi.subtle.encrypt(
|
|
64
|
+
{ name: "AES-GCM", iv: arrayBuffer(nonce), additionalData: arrayBuffer(envelopeAad(identity, requestId, direction)) },
|
|
65
|
+
key,
|
|
66
|
+
arrayBuffer(plaintext)
|
|
67
|
+
);
|
|
68
|
+
return { version: protocolVersion, nonce: toBase64Url(nonce), ciphertext: toBase64Url(new Uint8Array(ciphertext)) };
|
|
69
|
+
}
|
|
70
|
+
async function openRemoteAccessEnvelope(sessionKey, identity, requestId, direction, envelope, replayGuard) {
|
|
71
|
+
assertSessionKey(sessionKey);
|
|
72
|
+
validateIdentity(identity);
|
|
73
|
+
if (envelope.version !== protocolVersionForIdentity(identity) || !requestId) throw new Error("invalid remote-access envelope");
|
|
74
|
+
replayGuard.assertUnused(envelope.nonce);
|
|
75
|
+
const nonce = fromBase64Url(envelope.nonce, NONCE_BYTES);
|
|
76
|
+
const ciphertext = fromBase64Url(envelope.ciphertext);
|
|
77
|
+
if (ciphertext.length > MAX_PAYLOAD_BYTES + 16) throw new Error("invalid remote-access envelope");
|
|
78
|
+
try {
|
|
79
|
+
const key = await cryptoApi.subtle.importKey("raw", arrayBuffer(sessionKey), "AES-GCM", false, ["decrypt"]);
|
|
80
|
+
const plaintext = await cryptoApi.subtle.decrypt(
|
|
81
|
+
{ name: "AES-GCM", iv: arrayBuffer(nonce), additionalData: arrayBuffer(envelopeAad(identity, requestId, direction)) },
|
|
82
|
+
key,
|
|
83
|
+
arrayBuffer(ciphertext)
|
|
84
|
+
);
|
|
85
|
+
replayGuard.markUsed(envelope.nonce);
|
|
86
|
+
return new Uint8Array(plaintext);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error("envelope authentication failed");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function authenticateHandshake(projectKey, identity, role, publicKey) {
|
|
92
|
+
const key = await cryptoApi.subtle.importKey("raw", arrayBuffer(projectKey), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
93
|
+
const tag = await cryptoApi.subtle.sign("HMAC", key, arrayBuffer(handshakeAad(identity, role, publicKey)));
|
|
94
|
+
return toBase64Url(new Uint8Array(tag));
|
|
95
|
+
}
|
|
96
|
+
async function verifyHandshake(projectKey, identity, handshake) {
|
|
97
|
+
if (handshake.version !== protocolVersionForIdentity(identity)) throw new Error("handshake authentication failed");
|
|
98
|
+
fromBase64Url(handshake.publicKey, KEY_BYTES);
|
|
99
|
+
const expected = await authenticateHandshake(projectKey, identity, handshake.role, handshake.publicKey);
|
|
100
|
+
const actualBytes = fromBase64Url(handshake.authenticationTag, KEY_BYTES);
|
|
101
|
+
const expectedBytes = fromBase64Url(expected, KEY_BYTES);
|
|
102
|
+
if (!constantTimeEqual(actualBytes, expectedBytes)) throw new Error("handshake authentication failed");
|
|
103
|
+
}
|
|
104
|
+
function handshakeAad(identity, role, publicKey) {
|
|
105
|
+
const version = protocolVersionForIdentity(identity);
|
|
106
|
+
return encode([`OMRA-HANDSHAKE-${version}`, ...identityValues(identity), role, publicKey]);
|
|
107
|
+
}
|
|
108
|
+
function transcript(identity, requesterPublicKey, sourcePublicKey) {
|
|
109
|
+
const version = protocolVersionForIdentity(identity);
|
|
110
|
+
return encode([`OMRA-TRANSCRIPT-${version}`, ...identityValues(identity), requesterPublicKey, sourcePublicKey]);
|
|
111
|
+
}
|
|
112
|
+
function envelopeAad(identity, requestId, direction) {
|
|
113
|
+
const version = protocolVersionForIdentity(identity);
|
|
114
|
+
return encode([`OMRA-ENVELOPE-${version}`, ...identityValues(identity), requestId, direction]);
|
|
115
|
+
}
|
|
116
|
+
function identityValues(identity) {
|
|
117
|
+
if (protocolVersionForIdentity(identity) === 1) {
|
|
118
|
+
return [
|
|
119
|
+
identity.ownerId,
|
|
120
|
+
identity.projectId,
|
|
121
|
+
identity.sourceId,
|
|
122
|
+
identity.sourceSessionId,
|
|
123
|
+
identity.requestingClientId,
|
|
124
|
+
identity.keyEpoch
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
return [
|
|
128
|
+
identity.ownerId,
|
|
129
|
+
identity.contextType ?? "personal",
|
|
130
|
+
identity.contextId ?? identity.ownerId,
|
|
131
|
+
identity.projectId,
|
|
132
|
+
identity.sourceId,
|
|
133
|
+
identity.sourceSessionId,
|
|
134
|
+
identity.requestingClientId,
|
|
135
|
+
identity.hostMemberId ?? identity.ownerId,
|
|
136
|
+
identity.hostDeviceId ?? identity.sourceSessionId,
|
|
137
|
+
identity.requesterMemberId ?? identity.ownerId,
|
|
138
|
+
identity.requesterDeviceId ?? identity.requestingClientId,
|
|
139
|
+
identity.keyEpoch
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
function protocolVersionForIdentity(identity) {
|
|
143
|
+
return identity.contextType === "team" ? 2 : 1;
|
|
144
|
+
}
|
|
145
|
+
function encode(values) {
|
|
146
|
+
return new TextEncoder().encode(JSON.stringify(values));
|
|
147
|
+
}
|
|
148
|
+
async function hkdf(secret, salt, info) {
|
|
149
|
+
const key = await cryptoApi.subtle.importKey("raw", arrayBuffer(secret), "HKDF", false, ["deriveBits"]);
|
|
150
|
+
const bits = await cryptoApi.subtle.deriveBits(
|
|
151
|
+
{ name: "HKDF", hash: "SHA-256", salt: arrayBuffer(salt), info: arrayBuffer(info) },
|
|
152
|
+
key,
|
|
153
|
+
256
|
|
154
|
+
);
|
|
155
|
+
return new Uint8Array(bits);
|
|
156
|
+
}
|
|
157
|
+
async function digest(value) {
|
|
158
|
+
return new Uint8Array(await cryptoApi.subtle.digest("SHA-256", arrayBuffer(value)));
|
|
159
|
+
}
|
|
160
|
+
function validateIdentity(identity) {
|
|
161
|
+
const strings = identityValues(identity).filter((value) => typeof value === "string");
|
|
162
|
+
if (identity.contextType !== void 0 && !["personal", "team"].includes(identity.contextType)) {
|
|
163
|
+
throw new Error("invalid remote-access identity");
|
|
164
|
+
}
|
|
165
|
+
if (identity.contextType === "team" && (!identity.contextId || !identity.hostMemberId || !identity.hostDeviceId || !identity.requesterMemberId || !identity.requesterDeviceId)) throw new Error("invalid remote-access identity");
|
|
166
|
+
if (strings.some((value) => !value || value.length > 128) || !Number.isInteger(identity.keyEpoch) || identity.keyEpoch < 1) {
|
|
167
|
+
throw new Error("invalid remote-access identity");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function assertProjectKey(value) {
|
|
171
|
+
if (value.length !== KEY_BYTES) throw new Error("Project key must be 32 bytes");
|
|
172
|
+
}
|
|
173
|
+
function assertSessionKey(value) {
|
|
174
|
+
if (value.length !== KEY_BYTES) throw new Error("source-session key must be 32 bytes");
|
|
175
|
+
}
|
|
176
|
+
function toBase64Url(value) {
|
|
177
|
+
return Buffer.from(value).toString("base64url");
|
|
178
|
+
}
|
|
179
|
+
function fromBase64Url(value, expectedLength) {
|
|
180
|
+
if (!value || value.includes("=") || !/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
181
|
+
throw new Error("invalid remote-access key or envelope field");
|
|
182
|
+
}
|
|
183
|
+
const decoded = new Uint8Array(Buffer.from(value, "base64url"));
|
|
184
|
+
if (toBase64Url(decoded) !== value || expectedLength !== void 0 && decoded.length !== expectedLength) {
|
|
185
|
+
throw new Error("invalid remote-access key or envelope field");
|
|
186
|
+
}
|
|
187
|
+
return decoded;
|
|
188
|
+
}
|
|
189
|
+
function constantTimeEqual(left, right) {
|
|
190
|
+
if (left.length !== right.length) return false;
|
|
191
|
+
let difference = 0;
|
|
192
|
+
for (let index = 0; index < left.length; index += 1) difference |= left[index] ^ right[index];
|
|
193
|
+
return difference === 0;
|
|
194
|
+
}
|
|
195
|
+
function arrayBuffer(value) {
|
|
196
|
+
return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
RemoteAccessReplayGuard,
|
|
201
|
+
createRemoteAccessHandshake,
|
|
202
|
+
deriveRemoteAccessSessionKey,
|
|
203
|
+
sealRemoteAccessEnvelope,
|
|
204
|
+
openRemoteAccessEnvelope
|
|
205
|
+
};
|