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.
- package/CHANGELOG.md +27 -0
- package/README.md +12 -13
- package/SECURITY.md +13 -13
- package/browser-extension/manifest.json +2 -2
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/AUDIT.md +3 -3
- package/docs/CLIENTS.md +2 -2
- package/docs/GETTING_STARTED.md +13 -9
- package/docs/LOGGING.md +2 -2
- package/docs/MANAGED_JOBS.md +1 -1
- package/docs/MULTI_ACCOUNT.md +75 -358
- package/docs/OPERATIONS.md +4 -4
- package/docs/POLICY_REFERENCE.md +1 -1
- package/docs/PRIVACY.md +1 -1
- package/docs/PROJECT_STANDARDS.md +1 -1
- package/docs/TESTING.md +4 -4
- package/package.json +4 -3
- package/src/local/account-access.mjs +37 -0
- package/src/local/account-admin.mjs +105 -0
- package/src/local/bounded-output.mjs +50 -0
- package/src/local/call-registry.mjs +10 -0
- package/src/local/cli-account-admin.mjs +79 -0
- package/src/local/cli-options.mjs +9 -4
- package/src/local/cli-policy.mjs +7 -28
- package/src/local/cli.mjs +48 -78
- package/src/local/daemon-process.mjs +3 -2
- package/src/local/errors.mjs +0 -14
- package/src/local/log.mjs +1 -1
- package/src/local/managed-jobs.mjs +1 -3
- package/src/local/process-execution.mjs +90 -60
- package/src/local/relay-connection.mjs +11 -3
- package/src/local/runtime.mjs +27 -2
- package/src/local/service-convergence.mjs +20 -0
- package/src/local/service.mjs +12 -29
- package/src/local/shell.mjs +17 -29
- package/src/local/state-inventory.mjs +2 -2
- package/src/local/state.mjs +30 -85
- package/src/local/tool-executor.mjs +8 -2
- package/src/shared/access-contract.json +23 -0
- package/src/shared/policy-contract.json +30 -7
- package/src/worker/access.ts +46 -0
- package/src/worker/account-admin.ts +103 -0
- package/src/worker/index.ts +75 -48
- package/src/worker/oauth-state.ts +184 -2
- 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:${
|
|
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
|
|
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
|
+
}
|