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 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.15`, in the web app.
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.15.0-alpha.N` is a prerelease from the `dev` branch published under the
321
+ - `0.16.0-alpha.N` is a prerelease from the `dev` branch published under the
311
322
  `alpha` npm tag.
312
- - `0.15.0` is a stable release from `main` published under the `latest` npm tag.
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
+ };