machine-bridge-mcp 0.17.1 → 0.18.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +12 -13
  3. package/SECURITY.md +13 -13
  4. package/browser-extension/manifest.json +2 -2
  5. package/docs/ARCHITECTURE.md +5 -5
  6. package/docs/AUDIT.md +3 -3
  7. package/docs/CLIENTS.md +2 -2
  8. package/docs/GETTING_STARTED.md +13 -9
  9. package/docs/LOGGING.md +2 -2
  10. package/docs/MANAGED_JOBS.md +1 -1
  11. package/docs/MULTI_ACCOUNT.md +75 -358
  12. package/docs/OPERATIONS.md +4 -4
  13. package/docs/POLICY_REFERENCE.md +1 -1
  14. package/docs/PRIVACY.md +1 -1
  15. package/docs/PROJECT_STANDARDS.md +1 -1
  16. package/docs/TESTING.md +4 -4
  17. package/package.json +4 -3
  18. package/src/local/account-access.mjs +37 -0
  19. package/src/local/account-admin.mjs +105 -0
  20. package/src/local/bounded-output.mjs +50 -0
  21. package/src/local/call-registry.mjs +10 -0
  22. package/src/local/cli-account-admin.mjs +79 -0
  23. package/src/local/cli-options.mjs +9 -4
  24. package/src/local/cli-policy.mjs +7 -28
  25. package/src/local/cli.mjs +48 -78
  26. package/src/local/daemon-process.mjs +3 -2
  27. package/src/local/errors.mjs +0 -14
  28. package/src/local/log.mjs +1 -1
  29. package/src/local/managed-jobs.mjs +1 -3
  30. package/src/local/process-execution.mjs +90 -60
  31. package/src/local/relay-connection.mjs +11 -3
  32. package/src/local/runtime.mjs +27 -2
  33. package/src/local/service-convergence.mjs +20 -0
  34. package/src/local/service.mjs +12 -29
  35. package/src/local/shell.mjs +17 -29
  36. package/src/local/state-inventory.mjs +2 -2
  37. package/src/local/state.mjs +30 -85
  38. package/src/local/tool-executor.mjs +8 -2
  39. package/src/shared/access-contract.json +23 -0
  40. package/src/shared/policy-contract.json +30 -7
  41. package/src/worker/access.ts +46 -0
  42. package/src/worker/account-admin.ts +103 -0
  43. package/src/worker/index.ts +75 -48
  44. package/src/worker/oauth-state.ts +184 -2
  45. package/src/worker/tool-catalog.ts +13 -0
@@ -1,3 +1,21 @@
1
+ import { normalizeAccountRole, type AccountRole } from "./access";
2
+
3
+ export const OAUTH_STORE_SCHEMA_VERSION = 1;
4
+ const PASSWORD_TOKEN_PATTERN = /^[a-z][a-z0-9_]{2,31}_[A-Za-z0-9_-]{43}$/;
5
+
6
+ export interface AccountRecord {
7
+ account_id: string;
8
+ name: string;
9
+ display_name: string;
10
+ role: AccountRole;
11
+ active: boolean;
12
+ version: number;
13
+ password_salt: string;
14
+ password_hash: string;
15
+ created_at: number;
16
+ updated_at: number;
17
+ }
18
+
1
19
  export interface OAuthClient {
2
20
  client_id: string;
3
21
  client_name: string;
@@ -9,6 +27,9 @@ export interface OAuthClient {
9
27
 
10
28
  export interface OAuthCode {
11
29
  client_id: string;
30
+ account_id: string;
31
+ account_version: number;
32
+ role: AccountRole;
12
33
  redirect_uri: string;
13
34
  code_challenge: string;
14
35
  scope: string;
@@ -18,6 +39,9 @@ export interface OAuthCode {
18
39
 
19
40
  export interface OAuthToken {
20
41
  client_id: string;
42
+ account_id: string;
43
+ account_version: number;
44
+ role: AccountRole;
21
45
  scope: string;
22
46
  resource: string;
23
47
  version: string;
@@ -32,6 +56,8 @@ export interface OAuthFailure {
32
56
  }
33
57
 
34
58
  export interface OAuthStore {
59
+ schema_version: number;
60
+ accounts: Record<string, AccountRecord>;
35
61
  clients: Record<string, OAuthClient>;
36
62
  codes: Record<string, OAuthCode>;
37
63
  tokens: Record<string, OAuthToken>;
@@ -52,6 +78,28 @@ const AUTH_FAILURE_WINDOW_SECONDS = 10 * 60;
52
78
  export const AUTH_BLOCK_SECONDS = 15 * 60;
53
79
  const AUTH_FAILURE_LIMIT = 10;
54
80
 
81
+ export function emptyOAuthStore(): OAuthStore {
82
+ return {
83
+ schema_version: OAUTH_STORE_SCHEMA_VERSION,
84
+ accounts: {},
85
+ clients: {},
86
+ codes: {},
87
+ tokens: {},
88
+ auth_failures: {},
89
+ };
90
+ }
91
+
92
+ export function isCurrentOAuthStore(value: unknown): value is OAuthStore {
93
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
94
+ const store = value as Partial<OAuthStore>;
95
+ return store.schema_version === OAUTH_STORE_SCHEMA_VERSION
96
+ && isRecord(store.accounts)
97
+ && isRecord(store.clients)
98
+ && isRecord(store.codes)
99
+ && isRecord(store.tokens)
100
+ && isRecord(store.auth_failures);
101
+ }
102
+
55
103
  export function validateAuthorizationRequest(
56
104
  body: Record<string, unknown>,
57
105
  base: string,
@@ -81,6 +129,106 @@ export function validateAuthorizationRequest(
81
129
  return { value: { client, clientId, redirectUri, codeChallenge, requestedResource, scope, state } };
82
130
  }
83
131
 
132
+ export function normalizeAccountName(value: unknown): string | null {
133
+ const name = String(value ?? "").trim().toLowerCase();
134
+ return /^[a-z0-9](?:[a-z0-9._-]{1,62}[a-z0-9])?$/.test(name) ? name : null;
135
+ }
136
+
137
+ export function accountByName(store: OAuthStore, name: unknown): AccountRecord | null {
138
+ const normalized = normalizeAccountName(name);
139
+ if (!normalized) return null;
140
+ return Object.values(store.accounts).find((account) => account.name === normalized) ?? null;
141
+ }
142
+
143
+ export async function createAccount(input: { name: unknown; displayName?: unknown; role: unknown; password: unknown; now: number }): Promise<AccountRecord> {
144
+ const name = normalizeAccountName(input.name);
145
+ const role = normalizeAccountRole(input.role);
146
+ const password = normalizePassword(input.password);
147
+ if (!name) throw new Error("account name must be 3-64 lowercase letters, digits, dots, underscores, or hyphens");
148
+ if (!role) throw new Error("account role is invalid");
149
+ const displayName = normalizeDisplayName(input.displayName, name);
150
+ const salt = new Uint8Array(16);
151
+ crypto.getRandomValues(salt);
152
+ return {
153
+ account_id: randomToken("acct"),
154
+ name,
155
+ display_name: displayName,
156
+ role,
157
+ active: true,
158
+ version: 1,
159
+ password_salt: base64Url(salt),
160
+ password_hash: await derivePasswordHash(password, salt),
161
+ created_at: input.now,
162
+ updated_at: input.now,
163
+ };
164
+ }
165
+
166
+ export async function replaceAccountPassword(account: AccountRecord, passwordValue: unknown, now: number): Promise<void> {
167
+ const password = normalizePassword(passwordValue);
168
+ const salt = new Uint8Array(16);
169
+ crypto.getRandomValues(salt);
170
+ account.password_salt = base64Url(salt);
171
+ account.password_hash = await derivePasswordHash(password, salt);
172
+ account.version += 1;
173
+ account.updated_at = now;
174
+ }
175
+
176
+ export async function verifyAccountPassword(account: AccountRecord, passwordValue: unknown): Promise<boolean> {
177
+ try {
178
+ const password = normalizePassword(passwordValue);
179
+ const salt = base64UrlDecode(account.password_salt);
180
+ const hash = await derivePasswordHash(password, salt);
181
+ return safeEqual(hash, account.password_hash);
182
+ } catch {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ export function publicAccount(account: AccountRecord): Record<string, unknown> {
188
+ return {
189
+ account_id: account.account_id,
190
+ name: account.name,
191
+ display_name: account.display_name,
192
+ role: account.role,
193
+ active: account.active,
194
+ version: account.version,
195
+ created_at: account.created_at,
196
+ updated_at: account.updated_at,
197
+ };
198
+ }
199
+
200
+ export function updateAccount(account: AccountRecord, input: { displayName?: unknown; role?: unknown; active?: unknown }, now: number): void {
201
+ let changed = false;
202
+ if (input.displayName !== undefined) {
203
+ account.display_name = normalizeDisplayName(input.displayName, account.name);
204
+ changed = true;
205
+ }
206
+ if (input.role !== undefined) {
207
+ const role = normalizeAccountRole(input.role);
208
+ if (!role) throw new Error("account role is invalid");
209
+ if (role !== account.role) {
210
+ account.role = role;
211
+ changed = true;
212
+ }
213
+ }
214
+ if (input.active !== undefined) {
215
+ if (typeof input.active !== "boolean") throw new Error("active must be a boolean");
216
+ if (account.active !== input.active) {
217
+ account.active = input.active;
218
+ changed = true;
219
+ }
220
+ }
221
+ if (changed) {
222
+ account.version += 1;
223
+ account.updated_at = now;
224
+ }
225
+ }
226
+
227
+ export function revokeAccountCredentials(store: OAuthStore, accountId: string): void {
228
+ for (const [key, value] of Object.entries(store.codes)) if (value.account_id === accountId) delete store.codes[key];
229
+ for (const [key, value] of Object.entries(store.tokens)) if (value.account_id === accountId) delete store.tokens[key];
230
+ }
231
+
84
232
  export function pruneClientRecordByExpiry<T extends { client_id: string; expires_at: number }>(record: Record<string, T>, clientId: string, keep: number): void {
85
233
  const allowed = new Set(Object.entries(record)
86
234
  .filter(([, value]) => value.client_id === clientId)
@@ -111,7 +259,7 @@ export async function authorizationIdentity(request: Request, keyMaterial: strin
111
259
  ["sign"],
112
260
  );
113
261
  const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(source));
114
- return `hmac-sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
262
+ return `hmac-sha256:${hex(new Uint8Array(digest))}`;
115
263
  }
116
264
 
117
265
  export function recordAuthorizationFailure(store: OAuthStore, identity: string, now: number): void {
@@ -142,7 +290,7 @@ export function randomToken(prefix: string): string {
142
290
 
143
291
  export async function sha256Hex(value: string): Promise<string> {
144
292
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
145
- return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
293
+ return hex(new Uint8Array(digest));
146
294
  }
147
295
 
148
296
  export async function safeEqual(left: string, right: string): Promise<boolean> {
@@ -165,6 +313,40 @@ export async function pkceS256(verifier: string): Promise<string> {
165
313
  return base64Url(new Uint8Array(digest));
166
314
  }
167
315
 
316
+ function normalizePassword(value: unknown): string {
317
+ if (typeof value !== "string" || !PASSWORD_TOKEN_PATTERN.test(value)) {
318
+ throw new Error("account password must be a generated 256-bit token");
319
+ }
320
+ return value;
321
+ }
322
+
323
+ function normalizeDisplayName(value: unknown, fallback: string): string {
324
+ const text = typeof value === "string" ? value.trim().replace(/[\r\n\t\u0000-\u001f\u007f]/g, " ").slice(0, 128) : "";
325
+ return text || fallback;
326
+ }
327
+
328
+ async function derivePasswordHash(password: string, salt: Uint8Array): Promise<string> {
329
+ if (salt.byteLength !== 16) throw new Error("account password salt is invalid");
330
+ const saltBuffer = salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength) as ArrayBuffer;
331
+ const key = await crypto.subtle.importKey("raw", saltBuffer, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
332
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(password));
333
+ return base64Url(new Uint8Array(signature));
334
+ }
335
+
168
336
  function base64Url(bytes: Uint8Array): string {
169
337
  return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
170
338
  }
339
+
340
+ function base64UrlDecode(value: string): Uint8Array {
341
+ const normalized = value.replaceAll("-", "+").replaceAll("_", "/");
342
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
343
+ return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
344
+ }
345
+
346
+ function hex(bytes: Uint8Array): string {
347
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
348
+ }
349
+
350
+ function isRecord(value: unknown): value is Record<string, unknown> {
351
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
352
+ }
@@ -0,0 +1,13 @@
1
+ import toolCatalog from "../shared/tool-catalog.json";
2
+
3
+ export type WorkerToolDefinition = Record<string, unknown> & { name: string; availability?: string };
4
+
5
+ const allTools = toolCatalog as WorkerToolDefinition[];
6
+
7
+ export const serverInfoTool = publicTool(allTools.find((tool) => tool.name === "server_info")!);
8
+ export const workspaceTools = Object.freeze(allTools.filter((tool) => tool.name !== "server_info").map(publicTool));
9
+
10
+ function publicTool(tool: WorkerToolDefinition): WorkerToolDefinition {
11
+ const { availability: _availability, ...definition } = tool;
12
+ return structuredClone(definition) as WorkerToolDefinition;
13
+ }