mouaif 0.3.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/LICENSE +21 -0
- package/README.md +140 -0
- package/bin/mouaif.js +281 -0
- package/frontend/dist/assets/AgentFilePicker-CcKLJorU.js +1 -0
- package/frontend/dist/assets/CliModal-Hs5phmNZ.js +7 -0
- package/frontend/dist/assets/DictationPage-BI23lp42.js +2 -0
- package/frontend/dist/assets/FileEditor-DDl31c6d.js +2 -0
- package/frontend/dist/assets/GitModal-3EC_gpJ5.js +2 -0
- package/frontend/dist/assets/Inspector-Ba3R1w04.js +73 -0
- package/frontend/dist/assets/SettingsAbout-bvZGDEDw.js +1 -0
- package/frontend/dist/assets/SettingsActions-Dk6WX9jv.js +1 -0
- package/frontend/dist/assets/SettingsAgents-BNV0MgDB.js +1 -0
- package/frontend/dist/assets/SettingsDefaults-DbMmQbzc.js +1 -0
- package/frontend/dist/assets/SettingsHiddenContent-BZ2sloH1.js +1 -0
- package/frontend/dist/assets/SettingsMcp-DOrfbQd1.js +1 -0
- package/frontend/dist/assets/SettingsMcpEdit-BGMQ2CWC.js +3 -0
- package/frontend/dist/assets/SettingsMcpRegistry-BywXee_A.js +1 -0
- package/frontend/dist/assets/SettingsNotifications-B0LEs11a.js +1 -0
- package/frontend/dist/assets/SettingsPricing-BAg33iVF.js +1 -0
- package/frontend/dist/assets/SettingsProject-DNrKhCcZ.js +14 -0
- package/frontend/dist/assets/SettingsProjects-IqkBfDcm.js +1 -0
- package/frontend/dist/assets/SettingsPrompts-BgeiASuk.js +1 -0
- package/frontend/dist/assets/SettingsProviders-k0xJN0IK.js +1 -0
- package/frontend/dist/assets/SettingsTags-B5kjFdQi.js +1 -0
- package/frontend/dist/assets/agentNavigation-BiiCpFz5.js +1 -0
- package/frontend/dist/assets/codemirror-Bp6CUUFk.js +30 -0
- package/frontend/dist/assets/index-BGvI4n0T.js +61 -0
- package/frontend/dist/assets/index-Bgg1gnDf.css +1 -0
- package/frontend/dist/assets/index-C1sQFIC-.css +1 -0
- package/frontend/dist/assets/index-CANPYzQg.css +1 -0
- package/frontend/dist/assets/index-Crn1LdzK.css +1 -0
- package/frontend/dist/assets/index-FbCWDPiB.css +1 -0
- package/frontend/dist/assets/projectQS-D1cSZ7Gr.js +1 -0
- package/frontend/dist/assets/virtual-list-6H9b4K51.js +1 -0
- package/frontend/dist/icons/favicon-32.png +0 -0
- package/frontend/dist/icons/icon-180-apple.png +0 -0
- package/frontend/dist/icons/icon-192.png +0 -0
- package/frontend/dist/icons/icon-512.png +0 -0
- package/frontend/dist/icons/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +83 -0
- package/frontend/dist/manifest.webmanifest +33 -0
- package/frontend/dist/sw.js +482 -0
- package/package.json +98 -0
- package/scripts/patch-zimmerframe.js +58 -0
- package/src/access-auth.js +515 -0
- package/src/agentFeatures.js +294 -0
- package/src/agentFiles.js +164 -0
- package/src/agentSkills.js +147 -0
- package/src/agents.js +230 -0
- package/src/ai-chat.js +21 -0
- package/src/ai-endpoints.js +1880 -0
- package/src/ai-stream.js +2048 -0
- package/src/ai.js +68 -0
- package/src/auth.js +391 -0
- package/src/chatdb.js +816 -0
- package/src/chats.js +275 -0
- package/src/custom-actions.js +65 -0
- package/src/files.js +431 -0
- package/src/hideFileContent.js +327 -0
- package/src/http-server.js +535 -0
- package/src/index.js +15 -0
- package/src/inspector.js +731 -0
- package/src/inspectorProfiles.js +503 -0
- package/src/live-chat.js +107 -0
- package/src/mcp.js +1517 -0
- package/src/messages.js +238 -0
- package/src/modelList.js +137 -0
- package/src/notifications.js +52 -0
- package/src/oauth-anthropic.js +280 -0
- package/src/oauth-github-copilot.js +417 -0
- package/src/oauth-mcp.js +216 -0
- package/src/oauth-openrouter.js +285 -0
- package/src/package-version.js +20 -0
- package/src/projects.js +285 -0
- package/src/promptProfiles.js +256 -0
- package/src/prompts.js +384 -0
- package/src/providerShapes.js +44 -0
- package/src/providers/base.js +41 -0
- package/src/providers/index.js +25 -0
- package/src/push.js +315 -0
- package/src/qr.js +192 -0
- package/src/restart.js +47 -0
- package/src/server-handlers-access.js +306 -0
- package/src/server-handlers-actions.js +100 -0
- package/src/server-handlers-ai.js +248 -0
- package/src/server-handlers-auth.js +273 -0
- package/src/server-handlers-chats.js +1436 -0
- package/src/server-handlers-git.js +467 -0
- package/src/server-handlers-mcp-oauth.js +56 -0
- package/src/server-handlers-misc.js +783 -0
- package/src/server-handlers-projects.js +289 -0
- package/src/server-handlers-prompts.js +259 -0
- package/src/server-handlers-push.js +102 -0
- package/src/server-handlers-settings.js +406 -0
- package/src/server-handlers-tools.js +654 -0
- package/src/server-handlers-transcribe.js +399 -0
- package/src/server-shared.js +780 -0
- package/src/server-web-static.js +191 -0
- package/src/settings.js +898 -0
- package/src/statusBar.js +541 -0
- package/src/tags.js +414 -0
- package/src/toolFeedback.js +225 -0
- package/src/tools/ask.js +154 -0
- package/src/tools/authorization.js +932 -0
- package/src/tools/files.js +1150 -0
- package/src/tools/progress.js +71 -0
- package/src/tools/restart.js +32 -0
- package/src/tools/searchEngine.js +957 -0
- package/src/tools/shell.js +341 -0
- package/src/tools/subagent.js +47 -0
- package/src/tools/task.js +234 -0
- package/src/tools/webpreview.js +448 -0
- package/src/trace.js +103 -0
- package/src/transcribe.js +683 -0
- package/src/usage.js +389 -0
- package/src/util.js +151 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auth + OAuth loopback callback REST handlers. Extracted from the
|
|
4
|
+
// original single-file http-server.js. Shared helpers live in
|
|
5
|
+
// src/server-shared.js.
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
sendJSON,
|
|
9
|
+
qs,
|
|
10
|
+
readJsonOr400,
|
|
11
|
+
htmlPage,
|
|
12
|
+
redirectMeta,
|
|
13
|
+
finishOAuth,
|
|
14
|
+
expectedOrigin,
|
|
15
|
+
DEFAULT_PORT,
|
|
16
|
+
auth,
|
|
17
|
+
oauthAnthropic,
|
|
18
|
+
oauthCopilot,
|
|
19
|
+
oauthOpenRouter,
|
|
20
|
+
safeDecode
|
|
21
|
+
} = require('./server-shared.js');
|
|
22
|
+
|
|
23
|
+
// The URL the OAuth provider must redirect the user's browser back to.
|
|
24
|
+
// The old hard-coded loopback (`http://127.0.0.1:<port>/oauth/callback`)
|
|
25
|
+
// only works when the app is served from the same machine the user is on.
|
|
26
|
+
// Behind a domain name (reverse proxy, --public-origin / MOUAIF_PUBLIC_ORIGIN)
|
|
27
|
+
// the provider would bounce the browser to the *user's* localhost, which is
|
|
28
|
+
// not the server. Derive the origin from the request instead: the public
|
|
29
|
+
// origin wins when configured, otherwise the Host header (which a proxy
|
|
30
|
+
// forwards by default). Falls back to 127.0.0.1 only when neither exists.
|
|
31
|
+
function defaultCallbackUrl(req, publicOrigin) {
|
|
32
|
+
const origin = expectedOrigin(req, publicOrigin);
|
|
33
|
+
if (origin) return origin + '/oauth/callback';
|
|
34
|
+
const port = req.socket.address() && req.socket.address().port;
|
|
35
|
+
return 'http://127.0.0.1:' + (port || DEFAULT_PORT) + '/oauth/callback';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function handleAuth(req, res, parsed, serverConfig) {
|
|
39
|
+
const urlPath = parsed.pathname;
|
|
40
|
+
const method = req.method;
|
|
41
|
+
const q = parsed.query || {};
|
|
42
|
+
const publicOrigin = serverConfig && serverConfig.publicOrigin;
|
|
43
|
+
|
|
44
|
+
if (urlPath === '/api/auth/accounts' && method === 'GET') {
|
|
45
|
+
return sendJSON(res, 200, { accounts: auth.listAccounts() });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (urlPath === '/api/auth/status' && method === 'GET') {
|
|
49
|
+
const provider = qs(q, 'provider');
|
|
50
|
+
if (!provider) return sendJSON(res, 400, { error: 'provider query param is required' });
|
|
51
|
+
if (!auth.SUPPORTED_PROVIDERS.includes(provider)) {
|
|
52
|
+
return sendJSON(res, 400, { error: 'Unknown provider', provider });
|
|
53
|
+
}
|
|
54
|
+
const accounts = auth.listAccounts();
|
|
55
|
+
return sendJSON(res, 200, {
|
|
56
|
+
provider,
|
|
57
|
+
accounts: accounts[provider] || [],
|
|
58
|
+
hasExchange: !!auth.getExchange(provider)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const delMatch = urlPath.match(/^\/api\/auth\/accounts\/([a-z0-9-]+)\/(.+)$/);
|
|
63
|
+
if (delMatch && method === 'DELETE') {
|
|
64
|
+
const provider = delMatch[1];
|
|
65
|
+
const account = safeDecode(delMatch[2]);
|
|
66
|
+
if (!auth.SUPPORTED_PROVIDERS.includes(provider)) {
|
|
67
|
+
return sendJSON(res, 400, { error: 'Unknown provider', provider });
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const r = auth.deleteToken(provider, account);
|
|
71
|
+
return sendJSON(res, 200, Object.assign({ ok: true }, r));
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return sendJSON(res, 500, { error: e.message, code: e.code || 'EKEYRING' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// POST /api/auth/sign-in/anthropic -> { authorizeUrl, state, expiresAt }
|
|
78
|
+
// The UI calls this, opens authorizeUrl in the user's browser, and
|
|
79
|
+
// polls GET /api/auth/status?provider=anthropic until hasExchange
|
|
80
|
+
// (already registered) and accounts include the signed-in email.
|
|
81
|
+
if (urlPath === '/api/auth/sign-in/anthropic' && method === 'POST') {
|
|
82
|
+
if (!auth.getExchange('anthropic')) {
|
|
83
|
+
return sendJSON(res, 501, { error: 'Anthropic OAuth is not registered in this build' });
|
|
84
|
+
}
|
|
85
|
+
const body = await readJsonOr400(req, res);
|
|
86
|
+
if (!body) return;
|
|
87
|
+
|
|
88
|
+
const state = oauthAnthropic.newState();
|
|
89
|
+
const verifier = oauthAnthropic.newVerifier();
|
|
90
|
+
const callbackUrl = new URL(body.redirectUri || defaultCallbackUrl(req, publicOrigin));
|
|
91
|
+
// The callback handler is shared by providers and therefore requires the
|
|
92
|
+
// provider name. OAuth providers return our redirect URI verbatim, so
|
|
93
|
+
// bind the provider into it before recording the pending exchange.
|
|
94
|
+
if (!callbackUrl.searchParams.has('provider')) callbackUrl.searchParams.set('provider', 'anthropic');
|
|
95
|
+
const redirectUri = callbackUrl.toString();
|
|
96
|
+
const scope = body.scope || oauthAnthropic.DEFAULT_SCOPE;
|
|
97
|
+
|
|
98
|
+
auth.recordPending('anthropic', {
|
|
99
|
+
state,
|
|
100
|
+
codeVerifier: verifier,
|
|
101
|
+
redirectUri,
|
|
102
|
+
scopes: scope,
|
|
103
|
+
accountHint: body.accountHint || ''
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const authorizeUrl = oauthAnthropic.buildAuthorizeUrl({
|
|
107
|
+
redirectUri,
|
|
108
|
+
state,
|
|
109
|
+
verifier,
|
|
110
|
+
scope
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return sendJSON(res, 200, {
|
|
114
|
+
authorizeUrl,
|
|
115
|
+
redirectUri,
|
|
116
|
+
state,
|
|
117
|
+
expiresAt: Date.now() + 5 * 60 * 1000,
|
|
118
|
+
// Echoed for debugging; the production base is https://api.anthropic.com
|
|
119
|
+
// unless MOUAIF_ANTHROPIC_API_BASE is set (test override).
|
|
120
|
+
apiBase: oauthAnthropic.DEFAULT_API_BASE
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// POST /api/auth/sign-in/github-copilot -> { authorizeUrl, state, expiresAt }
|
|
125
|
+
// GitHub Copilot uses the standard GitHub OAuth web flow with PKCE
|
|
126
|
+
// (decision §12: each provider picks its own auth shape). The user
|
|
127
|
+
// signs in at github.com/login/oauth/authorize, gets redirected
|
|
128
|
+
// back to /oauth/callback, the server exchanges the code for a
|
|
129
|
+
// long-lived GitHub OAuth access token, and the per-request
|
|
130
|
+
// Copilot API token is derived on demand by src/ai.js.
|
|
131
|
+
if (urlPath === '/api/auth/sign-in/github-copilot' && method === 'POST') {
|
|
132
|
+
if (!auth.getExchange('github-copilot')) {
|
|
133
|
+
return sendJSON(res, 501, { error: 'GitHub Copilot OAuth is not registered in this build' });
|
|
134
|
+
}
|
|
135
|
+
const body = await readJsonOr400(req, res);
|
|
136
|
+
if (!body) return;
|
|
137
|
+
|
|
138
|
+
const state = oauthCopilot.newState();
|
|
139
|
+
const verifier = oauthCopilot.newVerifier();
|
|
140
|
+
const callbackUrl = new URL(body.redirectUri || defaultCallbackUrl(req, publicOrigin));
|
|
141
|
+
if (!callbackUrl.searchParams.has('provider')) callbackUrl.searchParams.set('provider', 'github-copilot');
|
|
142
|
+
const redirectUri = callbackUrl.toString();
|
|
143
|
+
const scope = body.scope || oauthCopilot.DEFAULT_SCOPE;
|
|
144
|
+
|
|
145
|
+
auth.recordPending('github-copilot', {
|
|
146
|
+
state,
|
|
147
|
+
codeVerifier: verifier,
|
|
148
|
+
redirectUri,
|
|
149
|
+
scopes: scope,
|
|
150
|
+
accountHint: body.accountHint || ''
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const authorizeUrl = oauthCopilot.buildAuthorizeUrl({
|
|
154
|
+
redirectUri,
|
|
155
|
+
state,
|
|
156
|
+
verifier,
|
|
157
|
+
scope
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
return sendJSON(res, 200, {
|
|
161
|
+
authorizeUrl,
|
|
162
|
+
redirectUri,
|
|
163
|
+
state,
|
|
164
|
+
expiresAt: Date.now() + 10 * 60 * 1000,
|
|
165
|
+
// Echoed for debugging; the production base is the default.
|
|
166
|
+
apiBase: oauthCopilot.COPILOT_API_BASE
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// POST /api/auth/sign-in/openrouter -> { authorizeUrl, state, expiresAt }
|
|
171
|
+
// OpenRouter's PKCE flow (docs/decisions.md §12). Unlike
|
|
172
|
+
// Anthropic / GitHub Copilot, the loopback URL the user is
|
|
173
|
+
// redirected back to is the same URL we pass in — OpenRouter
|
|
174
|
+
// echoes it via the `callback_url` query param rather than
|
|
175
|
+
// expecting a pre-registered redirect. The user signs in at
|
|
176
|
+
// openrouter.ai/auth and OpenRouter redirects back to our
|
|
177
|
+
// /oauth/callback with `?code=...&state=...`. The server
|
|
178
|
+
// exchanges the code for a user-controlled OpenRouter API key
|
|
179
|
+
// (src/oauth-openrouter.js) and stores it in the keyring under
|
|
180
|
+
// the `openrouter` namespace. Subsequent chats use the same
|
|
181
|
+
// Bearer header path as a manually pasted OpenRouter key.
|
|
182
|
+
if (urlPath === '/api/auth/sign-in/openrouter' && method === 'POST') {
|
|
183
|
+
if (!auth.getExchange('openrouter')) {
|
|
184
|
+
return sendJSON(res, 501, { error: 'OpenRouter OAuth is not registered in this build' });
|
|
185
|
+
}
|
|
186
|
+
const body = await readJsonOr400(req, res);
|
|
187
|
+
if (!body) return;
|
|
188
|
+
|
|
189
|
+
const rawState = oauthOpenRouter.newState();
|
|
190
|
+
// Encode the provider in the state prefix so it survives the
|
|
191
|
+
// OAuth redirect. OpenRouter does not forward ?provider= from
|
|
192
|
+
// the callback_url, so we need the provider in a field the
|
|
193
|
+
// IdP echoes back faithfully. See finishOAuth()'s state parsing.
|
|
194
|
+
const state = 'openrouter:' + rawState;
|
|
195
|
+
const verifier = oauthOpenRouter.newVerifier();
|
|
196
|
+
const callbackUrl = new URL(body.redirectUri || defaultCallbackUrl(req, publicOrigin));
|
|
197
|
+
if (!callbackUrl.searchParams.has('provider')) callbackUrl.searchParams.set('provider', 'openrouter');
|
|
198
|
+
const redirectUri = callbackUrl.toString();
|
|
199
|
+
|
|
200
|
+
auth.recordPending('openrouter', {
|
|
201
|
+
state,
|
|
202
|
+
codeVerifier: verifier,
|
|
203
|
+
redirectUri,
|
|
204
|
+
scopes: 'openrouter',
|
|
205
|
+
accountHint: body.accountHint || ''
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const authorizeUrl = oauthOpenRouter.buildAuthorizeUrl({
|
|
209
|
+
callbackUrl: redirectUri,
|
|
210
|
+
state,
|
|
211
|
+
verifier
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return sendJSON(res, 200, {
|
|
215
|
+
authorizeUrl,
|
|
216
|
+
redirectUri,
|
|
217
|
+
state,
|
|
218
|
+
expiresAt: Date.now() + 10 * 60 * 1000,
|
|
219
|
+
apiBase: oauthOpenRouter.KEYS_URL
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return sendJSON(res, 404, { error: 'Not found', scope: 'auth' });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function handleOAuthCallback(req, res, parsed) {
|
|
227
|
+
const q = parsed.query || {};
|
|
228
|
+
const provider = qs(q, 'provider');
|
|
229
|
+
const state = qs(q, 'state');
|
|
230
|
+
const code = qs(q, 'code');
|
|
231
|
+
const errorParam = qs(q, 'error');
|
|
232
|
+
|
|
233
|
+
const result = await finishOAuth({ provider, state, code, errorParam, format: 'html' });
|
|
234
|
+
res.writeHead(result.status, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
235
|
+
// On iOS (especially PWA standalone mode) the popup that opened the OAuth
|
|
236
|
+
// provider may be the current page itself — there is no other tab to close.
|
|
237
|
+
// The redirect is relative (same origin) so the existing session cookie
|
|
238
|
+
// carries over.
|
|
239
|
+
const returnUrl = '/';
|
|
240
|
+
// Success pages return instantly: close the popup (the opener app polls
|
|
241
|
+
// the account list and reports success), or location.replace the full
|
|
242
|
+
// page back into the app. Error pages keep only the plain 2s meta refresh
|
|
243
|
+
// so the user can read what went wrong before the page returns to the app.
|
|
244
|
+
if (result.status === 200) {
|
|
245
|
+
res.end(htmlPage('Signed in', '<p class="ok">Signed in to <code>' + provider + '</code> as <code>' + result.account + '</code>.</p>'
|
|
246
|
+
+ redirectMeta(returnUrl, true)));
|
|
247
|
+
} else if (result.code === 'EPROVIDER_ERROR') {
|
|
248
|
+
res.end(htmlPage('Sign-in failed', '<p class="err">' + result.error + '</p>'
|
|
249
|
+
+ redirectMeta(returnUrl)));
|
|
250
|
+
} else if (result.code === 'ENOEXCHANGE') {
|
|
251
|
+
res.end(htmlPage('OAuth not configured', '<p class="err">Sign-in for <code>' + provider + '</code> is not configured in this build. A later commit will register the provider exchange.</p>'
|
|
252
|
+
+ redirectMeta(returnUrl)));
|
|
253
|
+
} else {
|
|
254
|
+
res.end(htmlPage('OAuth callback', '<p class="err">' + (result.error || 'unknown error') + '</p>'
|
|
255
|
+
+ redirectMeta(returnUrl)));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function handleOAuthCallbackPost(req, res, parsed) {
|
|
260
|
+
const body = await readJsonOr400(req, res);
|
|
261
|
+
if (!body) return;
|
|
262
|
+
const provider = typeof body.provider === 'string' ? body.provider : '';
|
|
263
|
+
const state = typeof body.state === 'string' ? body.state : '';
|
|
264
|
+
const code = typeof body.code === 'string' ? body.code : '';
|
|
265
|
+
const errorParam = typeof body.error === 'string' ? body.error : '';
|
|
266
|
+
const result = await finishOAuth({ provider, state, code, errorParam, format: 'json' });
|
|
267
|
+
if (result.status === 200) {
|
|
268
|
+
return sendJSON(res, 200, { ok: true, provider, account: result.account });
|
|
269
|
+
}
|
|
270
|
+
return sendJSON(res, result.status, { ok: false, error: result.error, code: result.code });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { handleAuth, handleOAuthCallback, handleOAuthCallbackPost };
|