aimeat 2.5.0 → 2.6.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/dist/.env.example +33 -0
- package/dist/build-stamp.json +13 -0
- package/dist/locales/en.json +166 -3
- package/dist/locales/fi.json +166 -3
- package/dist/public/components/VoiceRecorder.js +116 -0
- package/dist/public/connection-done.html +53 -0
- package/dist/public/css/components/voice-recorder.css +66 -0
- package/dist/public/css/views/inbox.css +41 -0
- package/dist/public/css/views/profile.css +171 -95
- package/dist/public/js/services/audio-recorder.js +171 -0
- package/dist/public/js/services/messages.js +21 -2
- package/dist/public/spa.html +3 -0
- package/dist/public/views/profile/access-tab/connections.js +264 -0
- package/dist/public/views/profile/access-tab.js +2 -0
- package/dist/public/views/profile/inbox-tab/components.js +60 -10
- package/dist/public/views/profile/inbox-tab/helpers.js +33 -0
- package/dist/public/views/profile/inbox-tab/panels.js +6 -1
- package/dist/public/views/profile/inbox-tab/use-voice.js +50 -0
- package/dist/public/views/profile/inbox-tab/voice-parts.js +111 -0
- package/dist/public/views/profile/inbox-tab.js +18 -24
- package/dist/public/views/profile/openrouter/budget-panel.js +224 -0
- package/dist/public/views/profile/openrouter/model-picker.js +150 -0
- package/dist/public/views/profile/openrouter/pricing.js +139 -0
- package/dist/public/views/profile/openrouter-settings.js +425 -488
- package/dist/scripts/build-sdk-libs.d.ts.map +1 -1
- package/dist/scripts/build-sdk-libs.js +1 -0
- package/dist/scripts/build-sdk-libs.js.map +1 -1
- package/dist/scripts/check-ai-disclosure.d.ts.map +1 -1
- package/dist/scripts/check-ai-disclosure.js +65 -0
- package/dist/scripts/check-ai-disclosure.js.map +1 -1
- package/dist/src/cli/connect/ai-provenance-carry.d.ts +54 -3
- package/dist/src/cli/connect/ai-provenance-carry.d.ts.map +1 -1
- package/dist/src/cli/connect/ai-provenance-carry.js +84 -12
- package/dist/src/cli/connect/ai-provenance-carry.js.map +1 -1
- package/dist/src/cli/connect/api-client.d.ts +11 -0
- package/dist/src/cli/connect/api-client.d.ts.map +1 -1
- package/dist/src/cli/connect/api-client.js +2 -0
- package/dist/src/cli/connect/api-client.js.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.js +11 -0
- package/dist/src/cli/connect/mcp/local-server.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.d.ts +1 -0
- package/dist/src/cli/connect/mcp/tools/apps.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.js +3 -2
- package/dist/src/cli/connect/mcp/tools/apps.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.js +5 -2
- package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts +1 -0
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.js +3 -2
- package/dist/src/cli/connect/mcp/tools/knowledge.js.map +1 -1
- package/dist/src/config-types.d.ts +14 -0
- package/dist/src/config-types.d.ts.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +10 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +969 -4
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.d.ts +33 -0
- package/dist/src/mcp/ai-provenance-input.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.js +36 -0
- package/dist/src/mcp/ai-provenance-input.js.map +1 -1
- package/dist/src/mcp/apps.d.ts +7 -0
- package/dist/src/mcp/apps.d.ts.map +1 -1
- package/dist/src/mcp/apps.js +20 -2
- package/dist/src/mcp/apps.js.map +1 -1
- package/dist/src/models/ai-provenance-schemas.d.ts +7 -1
- package/dist/src/models/ai-provenance-schemas.d.ts.map +1 -1
- package/dist/src/models/ai-provenance-schemas.js +11 -1
- package/dist/src/models/ai-provenance-schemas.js.map +1 -1
- package/dist/src/models/connection-schemas.d.ts +203 -0
- package/dist/src/models/connection-schemas.d.ts.map +1 -0
- package/dist/src/models/connection-schemas.js +33 -0
- package/dist/src/models/connection-schemas.js.map +1 -0
- package/dist/src/models/message-schemas.d.ts +40 -0
- package/dist/src/models/message-schemas.d.ts.map +1 -1
- package/dist/src/models/message-schemas.js +23 -0
- package/dist/src/models/message-schemas.js.map +1 -1
- package/dist/src/routes/ai-provenance.d.ts +6 -0
- package/dist/src/routes/ai-provenance.d.ts.map +1 -1
- package/dist/src/routes/ai-provenance.js +34 -1
- package/dist/src/routes/ai-provenance.js.map +1 -1
- package/dist/src/routes/ai.d.ts +5 -0
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +83 -0
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +4 -0
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/apps/drafts.d.ts +3 -0
- package/dist/src/routes/apps/drafts.d.ts.map +1 -1
- package/dist/src/routes/apps/drafts.js +13 -0
- package/dist/src/routes/apps/drafts.js.map +1 -1
- package/dist/src/routes/apps/publish.d.ts +5 -0
- package/dist/src/routes/apps/publish.d.ts.map +1 -1
- package/dist/src/routes/apps/publish.js +21 -5
- package/dist/src/routes/apps/publish.js.map +1 -1
- package/dist/src/routes/connections.d.ts +35 -0
- package/dist/src/routes/connections.d.ts.map +1 -0
- package/dist/src/routes/connections.js +406 -0
- package/dist/src/routes/connections.js.map +1 -0
- package/dist/src/routes/libs.js +1 -1
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/messages.d.ts +5 -0
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +92 -0
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +6 -1
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +37 -7
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/upload.d.ts +5 -0
- package/dist/src/routes/upload.d.ts.map +1 -1
- package/dist/src/routes/upload.js +32 -0
- package/dist/src/routes/upload.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +2 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/ai-completion.d.ts +48 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -1
- package/dist/src/services/ai-completion.js +112 -72
- package/dist/src/services/ai-completion.js.map +1 -1
- package/dist/src/services/ai-disclosure.d.ts +6 -0
- package/dist/src/services/ai-disclosure.d.ts.map +1 -1
- package/dist/src/services/ai-disclosure.js +20 -2
- package/dist/src/services/ai-disclosure.js.map +1 -1
- package/dist/src/services/ai-provenance-page.d.ts +59 -0
- package/dist/src/services/ai-provenance-page.d.ts.map +1 -0
- package/dist/src/services/ai-provenance-page.js +185 -0
- package/dist/src/services/ai-provenance-page.js.map +1 -0
- package/dist/src/services/ai-provenance.d.ts +6 -0
- package/dist/src/services/ai-provenance.d.ts.map +1 -1
- package/dist/src/services/ai-provenance.js +5 -0
- package/dist/src/services/ai-provenance.js.map +1 -1
- package/dist/src/services/ai-transcription.d.ts +66 -0
- package/dist/src/services/ai-transcription.d.ts.map +1 -0
- package/dist/src/services/ai-transcription.js +90 -0
- package/dist/src/services/ai-transcription.js.map +1 -0
- package/dist/src/services/app-publish.d.ts.map +1 -1
- package/dist/src/services/app-publish.js +16 -1
- package/dist/src/services/app-publish.js.map +1 -1
- package/dist/src/services/build-app-prompt.d.ts.map +1 -1
- package/dist/src/services/build-app-prompt.js +2 -1
- package/dist/src/services/build-app-prompt.js.map +1 -1
- package/dist/src/services/connections/attach.d.ts +62 -0
- package/dist/src/services/connections/attach.d.ts.map +1 -0
- package/dist/src/services/connections/attach.js +168 -0
- package/dist/src/services/connections/attach.js.map +1 -0
- package/dist/src/services/connections/credential.d.ts +39 -0
- package/dist/src/services/connections/credential.d.ts.map +1 -0
- package/dist/src/services/connections/credential.js +66 -0
- package/dist/src/services/connections/credential.js.map +1 -0
- package/dist/src/services/connections/instance.d.ts +65 -0
- package/dist/src/services/connections/instance.d.ts.map +1 -0
- package/dist/src/services/connections/instance.js +144 -0
- package/dist/src/services/connections/instance.js.map +1 -0
- package/dist/src/services/connections/oauth.d.ts +77 -0
- package/dist/src/services/connections/oauth.d.ts.map +1 -0
- package/dist/src/services/connections/oauth.js +290 -0
- package/dist/src/services/connections/oauth.js.map +1 -0
- package/dist/src/services/connections/providers.d.ts +118 -0
- package/dist/src/services/connections/providers.d.ts.map +1 -0
- package/dist/src/services/connections/providers.js +260 -0
- package/dist/src/services/connections/providers.js.map +1 -0
- package/dist/src/services/connections/publish-gate.d.ts +103 -0
- package/dist/src/services/connections/publish-gate.d.ts.map +1 -0
- package/dist/src/services/connections/publish-gate.js +0 -0
- package/dist/src/services/connections/publish-gate.js.map +1 -0
- package/dist/src/services/connections/publish.d.ts +60 -0
- package/dist/src/services/connections/publish.d.ts.map +1 -0
- package/dist/src/services/connections/publish.js +340 -0
- package/dist/src/services/connections/publish.js.map +1 -0
- package/dist/src/services/connections/refresh.d.ts +55 -0
- package/dist/src/services/connections/refresh.d.ts.map +1 -0
- package/dist/src/services/connections/refresh.js +307 -0
- package/dist/src/services/connections/refresh.js.map +1 -0
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +16 -2
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/openrouter.d.ts +67 -1
- package/dist/src/services/openrouter.d.ts.map +1 -1
- package/dist/src/services/openrouter.js +102 -11
- package/dist/src/services/openrouter.js.map +1 -1
- package/dist/src/services/upload-token.d.ts +5 -1
- package/dist/src/services/upload-token.d.ts.map +1 -1
- package/dist/src/services/upload-token.js +13 -1
- package/dist/src/services/upload-token.js.map +1 -1
- package/dist/src/services/workflow/engine-util.d.ts +18 -0
- package/dist/src/services/workflow/engine-util.d.ts.map +1 -1
- package/dist/src/services/workflow/engine-util.js +27 -0
- package/dist/src/services/workflow/engine-util.js.map +1 -1
- package/dist/src/services/workflow/engine.d.ts.map +1 -1
- package/dist/src/services/workflow/engine.js +4 -2
- package/dist/src/services/workflow/engine.js.map +1 -1
- package/dist/src/storage/interface.d.ts +2 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/db-types.d.ts +65 -0
- package/dist/src/storage/providers/postgres-kysely/db-types.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/index.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/index.js +2 -1
- package/dist/src/storage/providers/postgres-kysely/index.js.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts +44 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts.map +1 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.js +308 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.js.map +1 -0
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js +4 -2
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/migrations/0021_connections.sql +116 -0
- package/dist/src/storage/providers/postgres-kysely/migrations/0022_provider_clients.sql +36 -0
- package/dist/src/storage/providers/postgres-kysely/migrations/0023_nonce_payload.sql +13 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +2 -1
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/community.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/community.js +2 -1
- package/dist/src/storage/providers/sqlite/methods/community.js.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/connections.d.ts +60 -0
- package/dist/src/storage/providers/sqlite/methods/connections.d.ts.map +1 -0
- package/dist/src/storage/providers/sqlite/methods/connections.js +287 -0
- package/dist/src/storage/providers/sqlite/methods/connections.js.map +1 -0
- package/dist/src/storage/providers/sqlite/schema-tables-1.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-1.js +4 -0
- package/dist/src/storage/providers/sqlite/schema-tables-1.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-3.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-3.js +106 -0
- package/dist/src/storage/providers/sqlite/schema-tables-3.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +2 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/connection.repository.d.ts +124 -0
- package/dist/src/storage/repositories/connection.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/connection.repository.js +2 -0
- package/dist/src/storage/repositories/connection.repository.js.map +1 -0
- package/dist/src/storage/types/agents-messaging.d.ts +19 -0
- package/dist/src/storage/types/agents-messaging.d.ts.map +1 -1
- package/dist/src/storage/types/agents-messaging.js.map +1 -1
- package/dist/src/storage/types/auth.d.ts +8 -1
- package/dist/src/storage/types/auth.d.ts.map +1 -1
- package/dist/src/utils/build-stamp.d.ts +56 -0
- package/dist/src/utils/build-stamp.d.ts.map +1 -0
- package/dist/src/utils/build-stamp.js +142 -0
- package/dist/src/utils/build-stamp.js.map +1 -0
- package/dist/src/utils/messaging.d.ts +14 -0
- package/dist/src/utils/messaging.d.ts.map +1 -1
- package/dist/src/utils/messaging.js +0 -0
- package/dist/src/utils/messaging.js.map +1 -1
- package/dist/static/sdk-libs/connect/index.js +199 -0
- package/dist/static/sdk-libs/connect/notes.js +47 -0
- package/dist/static/sdk-libs/connect/panel.js +238 -0
- package/dist/static/sdk-libs/data/index.js +63 -11
- package/dist/static/sdk-libs/dist/aimeat-connect.js +357 -0
- package/dist/static/sdk-libs/dist/aimeat-data.js +40 -9
- package/package.json +2 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file instance.ts
|
|
3
|
+
* @description Instance-scoped provider support (TARGET-057): normalising a user-supplied instance
|
|
4
|
+
* address, and registering this node as an application at that instance on first contact.
|
|
5
|
+
*
|
|
6
|
+
* THE ADDRESS COMES FROM A USER. That single fact drives this whole file. `mastodon.social` is
|
|
7
|
+
* typed into a box by whoever is connecting, and the node then makes an HTTP request to it. That
|
|
8
|
+
* is an SSRF vector, and the answer is two layers rather than one: normalise and reject
|
|
9
|
+
* syntactically here, then let safeFetch apply the network-level rules (private ranges, redirect
|
|
10
|
+
* re-validation) on every request that follows.
|
|
11
|
+
*
|
|
12
|
+
* LAZY REGISTRATION. There is no way to know which instance the next user arrives from, and there
|
|
13
|
+
* are thousands, so the client credentials cannot be configuration. The node registers itself the
|
|
14
|
+
* first time someone connects from an instance and remembers the result, so the second user
|
|
15
|
+
* reuses the first user's registration rather than minting a parallel one.
|
|
16
|
+
* @structure normalizeInstance · registerAtInstance
|
|
17
|
+
* @usage import { normalizeInstance, registerAtInstance } from './instance.js';
|
|
18
|
+
* @version-history
|
|
19
|
+
* v1.0.0 — 2026-08-02 — TARGET-057 Phase 1c.
|
|
20
|
+
*/
|
|
21
|
+
import { randomUUID } from 'node:crypto';
|
|
22
|
+
import { safeFetch } from '../../utils/url-validator.js';
|
|
23
|
+
import { logger } from '../../utils/logger.js';
|
|
24
|
+
import { sealCredential, openCredential } from './credential.js';
|
|
25
|
+
/**
|
|
26
|
+
* Normalise `mastodon.social`, `https://mastodon.social/`, `HTTPS://Mastodon.Social` and friends to
|
|
27
|
+
* one canonical origin, or refuse.
|
|
28
|
+
*
|
|
29
|
+
* Canonicalising matters beyond tidiness: the origin is the storage key for the node's registration
|
|
30
|
+
* at that instance, so two spellings of one instance would register twice and hold two client
|
|
31
|
+
* secrets for the same place.
|
|
32
|
+
*
|
|
33
|
+
* HTTPS is forced rather than accepted-if-offered. An instance reached over http would carry the
|
|
34
|
+
* user's authorization code in the clear, and accepting `http://` from a text box is also the
|
|
35
|
+
* easiest way to reach something that is not on the internet at all.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeInstance(raw) {
|
|
38
|
+
const trimmed = (raw ?? '').trim();
|
|
39
|
+
if (!trimmed)
|
|
40
|
+
return { ok: false, reason: 'instance address is empty' };
|
|
41
|
+
if (trimmed.length > 253)
|
|
42
|
+
return { ok: false, reason: 'instance address is too long' };
|
|
43
|
+
let url;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { ok: false, reason: 'instance address is not a valid host' };
|
|
49
|
+
}
|
|
50
|
+
if (url.protocol !== 'https:')
|
|
51
|
+
return { ok: false, reason: 'instance must be reachable over https' };
|
|
52
|
+
// Credentials in the URL would be silently carried into every later request to that instance.
|
|
53
|
+
if (url.username || url.password)
|
|
54
|
+
return { ok: false, reason: 'instance address must not carry credentials' };
|
|
55
|
+
// A path would make two spellings of one instance look like two instances.
|
|
56
|
+
if (url.pathname !== '/' || url.search || url.hash) {
|
|
57
|
+
return { ok: false, reason: 'instance address must be a bare host, with no path or query' };
|
|
58
|
+
}
|
|
59
|
+
// Hostnames only. An IP literal here is either a mistake or an attempt to reach something
|
|
60
|
+
// internal; safeFetch would refuse the private ranges anyway, and refusing earlier is clearer.
|
|
61
|
+
if (/^\[|^\d+\.\d+\.\d+\.\d+$/.test(url.hostname)) {
|
|
62
|
+
return { ok: false, reason: 'instance address must be a hostname, not an IP address' };
|
|
63
|
+
}
|
|
64
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i.test(url.hostname)) {
|
|
65
|
+
return { ok: false, reason: 'instance address is not a valid hostname' };
|
|
66
|
+
}
|
|
67
|
+
return { ok: true, origin: `https://${url.hostname.toLowerCase()}${url.port ? `:${url.port}` : ''}` };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The node's application registration at `origin`, registering it if this is the first contact.
|
|
71
|
+
*
|
|
72
|
+
* Concurrency is handled by the store rather than here: `upsertProviderClient` is insert-if-new and
|
|
73
|
+
* returns whatever is now there, so two users arriving from one instance at the same moment
|
|
74
|
+
* converge on a single registration. The loser of that race has registered an application at the
|
|
75
|
+
* instance that nothing will ever point at, which is untidy at the instance and harmless to us —
|
|
76
|
+
* the alternative, a lock held across a network call to a stranger's server, is worse.
|
|
77
|
+
*/
|
|
78
|
+
export async function registerAtInstance(storage, key, provider, origin, app) {
|
|
79
|
+
const existing = await storage.getProviderClient(provider, origin);
|
|
80
|
+
if (existing) {
|
|
81
|
+
const opened = openCredential(existing.clientSecret, key);
|
|
82
|
+
// A registration whose secret cannot be read is worse than none: every authorization against it
|
|
83
|
+
// would fail at the token exchange with a message from the instance, far from the cause.
|
|
84
|
+
if (!opened)
|
|
85
|
+
return { error: 'stored registration for this instance cannot be read' };
|
|
86
|
+
return { clientId: existing.clientId, clientSecret: opened.accessToken };
|
|
87
|
+
}
|
|
88
|
+
const body = new URLSearchParams({
|
|
89
|
+
client_name: app.name,
|
|
90
|
+
redirect_uris: app.redirectUri,
|
|
91
|
+
scopes: app.scopes.join(' '),
|
|
92
|
+
website: app.website,
|
|
93
|
+
});
|
|
94
|
+
let res;
|
|
95
|
+
try {
|
|
96
|
+
res = await safeFetch(`${origin}/api/v1/apps`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
99
|
+
body: body.toString(),
|
|
100
|
+
signal: AbortSignal.timeout(15_000),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
// safeFetch throws "Fetch blocked: <reason>" for a refused address. That is the SSRF gate
|
|
105
|
+
// reporting, and it is worth surfacing verbatim: it tells the user their instance is unreachable
|
|
106
|
+
// rather than leaving them at a spinner.
|
|
107
|
+
return { error: `could not reach the instance: ${err.message}` };
|
|
108
|
+
}
|
|
109
|
+
if (!res.ok)
|
|
110
|
+
return { error: `instance refused the registration (HTTP ${res.status})` };
|
|
111
|
+
const json = await res.json().catch((err) => {
|
|
112
|
+
// Logged rather than swallowed: an instance answering 200 with something that is not JSON is a
|
|
113
|
+
// different problem from one answering with JSON that lacks the fields, and the message below
|
|
114
|
+
// is the same for both. Without this line the two are indistinguishable in production.
|
|
115
|
+
logger.warn('connections: instance registration returned unparseable JSON', {
|
|
116
|
+
provider, origin, error: String(err),
|
|
117
|
+
});
|
|
118
|
+
return null;
|
|
119
|
+
});
|
|
120
|
+
const clientId = typeof json?.client_id === 'string' ? json.client_id : '';
|
|
121
|
+
const clientSecret = typeof json?.client_secret === 'string' ? json.client_secret : '';
|
|
122
|
+
if (!clientId || !clientSecret)
|
|
123
|
+
return { error: 'instance returned no usable client credentials' };
|
|
124
|
+
// Sealed through the same path as a user token. It is not personal data, but a table that holds
|
|
125
|
+
// one secret in the clear teaches the next one to do the same.
|
|
126
|
+
const stored = await storage.upsertProviderClient({
|
|
127
|
+
id: randomUUID(),
|
|
128
|
+
provider,
|
|
129
|
+
instance: origin,
|
|
130
|
+
clientId,
|
|
131
|
+
clientSecret: sealCredential({ shape: 'static', accessToken: clientSecret }, key),
|
|
132
|
+
registeredAt: new Date().toISOString(),
|
|
133
|
+
});
|
|
134
|
+
// Read back what the store actually kept: on a race that is the OTHER registration, and using our
|
|
135
|
+
// own local values would authorize against credentials the store does not have.
|
|
136
|
+
if (stored.clientId !== clientId) {
|
|
137
|
+
const opened = openCredential(stored.clientSecret, key);
|
|
138
|
+
if (!opened)
|
|
139
|
+
return { error: 'stored registration for this instance cannot be read' };
|
|
140
|
+
return { clientId: stored.clientId, clientSecret: opened.accessToken };
|
|
141
|
+
}
|
|
142
|
+
return { clientId, clientSecret };
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=instance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../../../../src/services/connections/instance.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file oauth.ts
|
|
3
|
+
* @description The authorization round for an outbound connection (TARGET-057): start it, and
|
|
4
|
+
* complete it when the provider redirects back.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THE URL MUST NOT CARRY. Everything the callback needs to make a decision — which provider,
|
|
7
|
+
* which instance, which mode, who asked — is stored server-side against a single-use `state` and
|
|
8
|
+
* never put in the redirect URL. Carrying it in the URL would let whoever reaches the callback
|
|
9
|
+
* choose which provider their code is redeemed against and whose connection it becomes.
|
|
10
|
+
*
|
|
11
|
+
* WHY THE STATE IS CONSUMED FIRST. `completeAuthorization` deletes the nonce BEFORE it exchanges
|
|
12
|
+
* the code. A replayed callback then finds nothing and stops, rather than racing the original and
|
|
13
|
+
* producing two connections from one authorization.
|
|
14
|
+
*
|
|
15
|
+
* THE GOOGLE PARAMETERS ARE NOT OPTIONAL. `access_type=offline` and `prompt=consent` are why a
|
|
16
|
+
* refresh token arrives at all. Without them a repeat authorization returns an access token and no
|
|
17
|
+
* refresh token, and the connection silently becomes a one-hour connection that dies and cannot
|
|
18
|
+
* renew — with nothing in the response to say so.
|
|
19
|
+
* @structure startAuthorization · completeAuthorization · resolveClient · fetchAccountIdentity
|
|
20
|
+
* @usage import { startAuthorization, completeAuthorization } from './oauth.js';
|
|
21
|
+
* @version-history
|
|
22
|
+
* v1.0.0 — 2026-08-02 — TARGET-057 Phase 1c.
|
|
23
|
+
*/
|
|
24
|
+
import { type OutboundProvider } from './providers.js';
|
|
25
|
+
import type { AimeatConfig } from '../../config.js';
|
|
26
|
+
import type { Storage } from '../../storage/interface.js';
|
|
27
|
+
import type { ConnectionMode, ConnectionRecord } from '../../models/connection-schemas.js';
|
|
28
|
+
export interface ConnectContext {
|
|
29
|
+
config: AimeatConfig;
|
|
30
|
+
storage: Storage;
|
|
31
|
+
providers: OutboundProvider[];
|
|
32
|
+
key: Buffer;
|
|
33
|
+
}
|
|
34
|
+
export type StartResult = {
|
|
35
|
+
ok: true;
|
|
36
|
+
authorizeUrl: string;
|
|
37
|
+
state: string;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
code: string;
|
|
41
|
+
reason: string;
|
|
42
|
+
};
|
|
43
|
+
export type CompleteResult = {
|
|
44
|
+
ok: true;
|
|
45
|
+
connection: ConnectionRecord;
|
|
46
|
+
returnUrl: string;
|
|
47
|
+
created: boolean;
|
|
48
|
+
} | {
|
|
49
|
+
ok: false;
|
|
50
|
+
code: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
53
|
+
/** The node's own callback address. One per node, and it must match the provider registration byte for byte. */
|
|
54
|
+
export declare function callbackUrl(config: AimeatConfig): string;
|
|
55
|
+
/**
|
|
56
|
+
* Begin an authorization. Returns the URL to send the user's browser to; the user grants consent at
|
|
57
|
+
* the PROVIDER, never here.
|
|
58
|
+
*/
|
|
59
|
+
export declare function startAuthorization(ctx: ConnectContext, input: {
|
|
60
|
+
principal: string;
|
|
61
|
+
provider: string;
|
|
62
|
+
instance?: string;
|
|
63
|
+
mode: ConnectionMode;
|
|
64
|
+
returnUrl: string;
|
|
65
|
+
}): Promise<StartResult>;
|
|
66
|
+
/**
|
|
67
|
+
* Complete an authorization: consume the state, exchange the code, learn who the account is, and
|
|
68
|
+
* store the connection.
|
|
69
|
+
*
|
|
70
|
+
* Re-authorising an account already connected UPDATES that row rather than adding a second one —
|
|
71
|
+
* which is what makes "reconnect" a repair rather than a way to accumulate duplicates.
|
|
72
|
+
*/
|
|
73
|
+
export declare function completeAuthorization(ctx: ConnectContext, input: {
|
|
74
|
+
state: string;
|
|
75
|
+
code: string;
|
|
76
|
+
}): Promise<CompleteResult>;
|
|
77
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../../../src/services/connections/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAMH,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAK3F,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,GAAG,EAAE,MAAM,CAAC;CACb;AASD,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhD,MAAM,MAAM,cAAc,GACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAC/E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAOhD,gHAAgH;AAChH,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAExD;AAsBD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,cAAc,EACnB,KAAK,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzG,OAAO,CAAC,WAAW,CAAC,CA8DtB;AAiFD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,cAAc,CAAC,CAqGzB"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file oauth.ts
|
|
3
|
+
* @description The authorization round for an outbound connection (TARGET-057): start it, and
|
|
4
|
+
* complete it when the provider redirects back.
|
|
5
|
+
*
|
|
6
|
+
* WHAT THE URL MUST NOT CARRY. Everything the callback needs to make a decision — which provider,
|
|
7
|
+
* which instance, which mode, who asked — is stored server-side against a single-use `state` and
|
|
8
|
+
* never put in the redirect URL. Carrying it in the URL would let whoever reaches the callback
|
|
9
|
+
* choose which provider their code is redeemed against and whose connection it becomes.
|
|
10
|
+
*
|
|
11
|
+
* WHY THE STATE IS CONSUMED FIRST. `completeAuthorization` deletes the nonce BEFORE it exchanges
|
|
12
|
+
* the code. A replayed callback then finds nothing and stops, rather than racing the original and
|
|
13
|
+
* producing two connections from one authorization.
|
|
14
|
+
*
|
|
15
|
+
* THE GOOGLE PARAMETERS ARE NOT OPTIONAL. `access_type=offline` and `prompt=consent` are why a
|
|
16
|
+
* refresh token arrives at all. Without them a repeat authorization returns an access token and no
|
|
17
|
+
* refresh token, and the connection silently becomes a one-hour connection that dies and cannot
|
|
18
|
+
* renew — with nothing in the response to say so.
|
|
19
|
+
* @structure startAuthorization · completeAuthorization · resolveClient · fetchAccountIdentity
|
|
20
|
+
* @usage import { startAuthorization, completeAuthorization } from './oauth.js';
|
|
21
|
+
* @version-history
|
|
22
|
+
* v1.0.0 — 2026-08-02 — TARGET-057 Phase 1c.
|
|
23
|
+
*/
|
|
24
|
+
import { randomUUID, randomBytes, createHash } from 'node:crypto';
|
|
25
|
+
import { safeFetch } from '../../utils/url-validator.js';
|
|
26
|
+
import { sealCredential } from './credential.js';
|
|
27
|
+
import { normalizeInstance, registerAtInstance } from './instance.js';
|
|
28
|
+
import { findProvider } from './providers.js';
|
|
29
|
+
/** How long an unfinished authorization stays valid. Long enough to read a consent screen. */
|
|
30
|
+
const STATE_TTL_MS = 10 * 60 * 1000;
|
|
31
|
+
/** base64url without padding — what PKCE and OAuth2 expect. */
|
|
32
|
+
function b64url(buf) {
|
|
33
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
34
|
+
}
|
|
35
|
+
/** The node's own callback address. One per node, and it must match the provider registration byte for byte. */
|
|
36
|
+
export function callbackUrl(config) {
|
|
37
|
+
return config.connectRedirectUri || `${config.baseUrl.replace(/\/$/, '')}/v1/connections/callback`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Client credentials for this provider: from config for a fixed endpoint, from the node's
|
|
41
|
+
* registration at the instance for a federated one (registering on first contact).
|
|
42
|
+
*/
|
|
43
|
+
async function resolveClient(ctx, provider, instance) {
|
|
44
|
+
if (!provider.instanceScoped) {
|
|
45
|
+
if (!provider.client)
|
|
46
|
+
return { error: provider.disabledReason ?? 'provider is not configured' };
|
|
47
|
+
return { clientId: provider.client.id, clientSecret: provider.client.secret };
|
|
48
|
+
}
|
|
49
|
+
if (!instance)
|
|
50
|
+
return { error: 'this provider needs an instance address' };
|
|
51
|
+
return registerAtInstance(ctx.storage, ctx.key, provider.id, instance, {
|
|
52
|
+
name: 'AIMEAT',
|
|
53
|
+
redirectUri: callbackUrl(ctx.config),
|
|
54
|
+
scopes: provider.scopes,
|
|
55
|
+
website: ctx.config.baseUrl,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Begin an authorization. Returns the URL to send the user's browser to; the user grants consent at
|
|
60
|
+
* the PROVIDER, never here.
|
|
61
|
+
*/
|
|
62
|
+
export async function startAuthorization(ctx, input) {
|
|
63
|
+
const provider = findProvider(ctx.providers, input.provider);
|
|
64
|
+
if (!provider)
|
|
65
|
+
return { ok: false, code: 'UNKNOWN_PROVIDER', reason: `no provider '${input.provider}'` };
|
|
66
|
+
if (!provider.enabled) {
|
|
67
|
+
// The reason travels with the refusal: an operator staring at "disabled" has nothing to act on.
|
|
68
|
+
return { ok: false, code: 'PROVIDER_DISABLED', reason: provider.disabledReason ?? 'provider is disabled' };
|
|
69
|
+
}
|
|
70
|
+
if (provider.credentialShape !== 'oauth2') {
|
|
71
|
+
return { ok: false, code: 'NOT_AN_OAUTH_PROVIDER', reason: `${provider.id} does not use an authorization round` };
|
|
72
|
+
}
|
|
73
|
+
let instance = null;
|
|
74
|
+
if (provider.instanceScoped) {
|
|
75
|
+
const norm = normalizeInstance(input.instance ?? '');
|
|
76
|
+
if (!norm.ok)
|
|
77
|
+
return { ok: false, code: 'BAD_INSTANCE', reason: norm.reason };
|
|
78
|
+
instance = norm.origin;
|
|
79
|
+
}
|
|
80
|
+
const client = await resolveClient(ctx, provider, instance);
|
|
81
|
+
if ('error' in client)
|
|
82
|
+
return { ok: false, code: 'CLIENT_UNAVAILABLE', reason: client.error };
|
|
83
|
+
const endpoints = provider.endpoints(instance);
|
|
84
|
+
if (!endpoints)
|
|
85
|
+
return { ok: false, code: 'NO_ENDPOINTS', reason: 'provider has no authorization endpoint' };
|
|
86
|
+
const state = b64url(randomBytes(24));
|
|
87
|
+
const verifier = b64url(randomBytes(32));
|
|
88
|
+
const payload = { provider: provider.id, instance, mode: input.mode };
|
|
89
|
+
await ctx.storage.createVerificationNonce({
|
|
90
|
+
id: randomUUID(),
|
|
91
|
+
// Binding the state to the principal is the CSRF gate: a code redeemed by anyone else lands on
|
|
92
|
+
// a nonce whose owner is not them, and the callback refuses.
|
|
93
|
+
owner: input.principal,
|
|
94
|
+
type: 'connect',
|
|
95
|
+
state,
|
|
96
|
+
// The PKCE verifier stays HERE. It is the half of the exchange that never travels.
|
|
97
|
+
nonce: verifier,
|
|
98
|
+
redirectUri: input.returnUrl,
|
|
99
|
+
payload: JSON.stringify(payload),
|
|
100
|
+
createdAt: new Date().toISOString(),
|
|
101
|
+
expiresAt: new Date(Date.now() + STATE_TTL_MS).toISOString(),
|
|
102
|
+
});
|
|
103
|
+
const q = new URLSearchParams({
|
|
104
|
+
response_type: 'code',
|
|
105
|
+
client_id: client.clientId,
|
|
106
|
+
redirect_uri: callbackUrl(ctx.config),
|
|
107
|
+
scope: provider.scopes.join(' '),
|
|
108
|
+
state,
|
|
109
|
+
});
|
|
110
|
+
if (provider.pkce) {
|
|
111
|
+
q.set('code_challenge', b64url(createHash('sha256').update(verifier).digest()));
|
|
112
|
+
q.set('code_challenge_method', 'S256');
|
|
113
|
+
}
|
|
114
|
+
if (provider.id === 'youtube') {
|
|
115
|
+
// Not optional, and the failure is silent: without these Google returns no refresh token on a
|
|
116
|
+
// repeat authorization and the connection quietly becomes a one-hour connection.
|
|
117
|
+
q.set('access_type', 'offline');
|
|
118
|
+
q.set('prompt', 'consent');
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, authorizeUrl: `${endpoints.authorize}?${q.toString()}`, state };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Ask the provider who the token belongs to.
|
|
124
|
+
*
|
|
125
|
+
* This lives in the node rather than in a publishing recipe because it is part of the CONNECTION's
|
|
126
|
+
* lifecycle, not of publishing: `externalId` is the dedupe key that stops one account becoming two
|
|
127
|
+
* rows, and `accountLabel` is what the owner reads in the panel. A label the app supplied would be
|
|
128
|
+
* a label the app chose.
|
|
129
|
+
*/
|
|
130
|
+
async function fetchAccountIdentity(provider, instance, accessToken) {
|
|
131
|
+
const auth = { Authorization: `Bearer ${accessToken}` };
|
|
132
|
+
try {
|
|
133
|
+
if (provider.id === 'mastodon') {
|
|
134
|
+
const r = await safeFetch(`${instance}/api/v1/accounts/verify_credentials`, {
|
|
135
|
+
headers: auth, signal: AbortSignal.timeout(15_000),
|
|
136
|
+
});
|
|
137
|
+
if (!r.ok)
|
|
138
|
+
return { error: `instance rejected the token (HTTP ${r.status})` };
|
|
139
|
+
const j = await r.json();
|
|
140
|
+
const id = typeof j.id === 'string' ? j.id : '';
|
|
141
|
+
const acct = typeof j.acct === 'string' ? j.acct : '';
|
|
142
|
+
if (!id)
|
|
143
|
+
return { error: 'instance returned no account id' };
|
|
144
|
+
const host = instance ? new URL(instance).hostname : '';
|
|
145
|
+
return { externalId: id, accountLabel: acct ? `@${acct}@${host}` : host };
|
|
146
|
+
}
|
|
147
|
+
if (provider.id === 'youtube') {
|
|
148
|
+
const r = await safeFetch('https://www.googleapis.com/youtube/v3/channels?part=snippet&mine=true', { headers: auth, signal: AbortSignal.timeout(15_000) });
|
|
149
|
+
if (!r.ok) {
|
|
150
|
+
// 403 here means the token is valid but not scoped for the read, which is a DIFFERENT
|
|
151
|
+
// problem from a bad token and has a different fix. Naming it saves the next person the
|
|
152
|
+
// hour it cost to find that youtube.upload alone cannot list your own channel.
|
|
153
|
+
return { error: r.status === 403
|
|
154
|
+
? 'Google accepted the sign-in but would not say which channel it is for. The connection was authorised before youtube.readonly was requested; disconnect and connect again.'
|
|
155
|
+
: `Google rejected the token (HTTP ${r.status})` };
|
|
156
|
+
}
|
|
157
|
+
const j = await r.json();
|
|
158
|
+
const item = Array.isArray(j.items) ? j.items[0] : undefined;
|
|
159
|
+
const id = typeof item?.id === 'string' ? item.id : '';
|
|
160
|
+
// A Google account with no YouTube channel authorizes fine and then has nothing to publish to.
|
|
161
|
+
// Saying so here is far kinder than an upload failing later for a reason nobody can read.
|
|
162
|
+
if (!id)
|
|
163
|
+
return { error: 'this Google account has no YouTube channel' };
|
|
164
|
+
const title = typeof item?.snippet?.title === 'string' ? item.snippet.title : id;
|
|
165
|
+
return { externalId: id, accountLabel: title };
|
|
166
|
+
}
|
|
167
|
+
if (provider.id === 'fake') {
|
|
168
|
+
// Test-only, and reached only when a base URL is configured. It goes through the same
|
|
169
|
+
// safeFetch + shape-checking path as the real ones so the tests exercise that code rather
|
|
170
|
+
// than a shortcut around it.
|
|
171
|
+
const base = provider.endpoints(null)?.token.replace(/\/token$/, '') ?? '';
|
|
172
|
+
const r = await safeFetch(`${base}/me`, { headers: auth, signal: AbortSignal.timeout(15_000) });
|
|
173
|
+
if (!r.ok)
|
|
174
|
+
return { error: `test provider rejected the token (HTTP ${r.status})` };
|
|
175
|
+
const j = await r.json();
|
|
176
|
+
const id = typeof j.id === 'string' ? j.id : '';
|
|
177
|
+
if (!id)
|
|
178
|
+
return { error: 'test provider returned no account id' };
|
|
179
|
+
return { externalId: id, accountLabel: typeof j.label === 'string' ? j.label : id };
|
|
180
|
+
}
|
|
181
|
+
return { error: `no identity lookup for ${provider.id}` };
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return { error: `could not reach the provider: ${err.message}` };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Complete an authorization: consume the state, exchange the code, learn who the account is, and
|
|
189
|
+
* store the connection.
|
|
190
|
+
*
|
|
191
|
+
* Re-authorising an account already connected UPDATES that row rather than adding a second one —
|
|
192
|
+
* which is what makes "reconnect" a repair rather than a way to accumulate duplicates.
|
|
193
|
+
*/
|
|
194
|
+
export async function completeAuthorization(ctx, input) {
|
|
195
|
+
const nonce = await ctx.storage.getVerificationNonce(input.state);
|
|
196
|
+
if (!nonce || nonce.type !== 'connect') {
|
|
197
|
+
return { ok: false, code: 'UNKNOWN_STATE', reason: 'this authorization is not one we started' };
|
|
198
|
+
}
|
|
199
|
+
// Consumed BEFORE the exchange: a replayed callback then finds nothing, rather than racing the
|
|
200
|
+
// original and producing two connections from one authorization.
|
|
201
|
+
await ctx.storage.deleteVerificationNonce(input.state);
|
|
202
|
+
if (new Date(nonce.expiresAt).getTime() < Date.now()) {
|
|
203
|
+
return { ok: false, code: 'STATE_EXPIRED', reason: 'the authorization took too long; start again' };
|
|
204
|
+
}
|
|
205
|
+
let payload;
|
|
206
|
+
try {
|
|
207
|
+
payload = JSON.parse(nonce.payload ?? '');
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return { ok: false, code: 'BAD_STATE', reason: 'the stored authorization is unreadable' };
|
|
211
|
+
}
|
|
212
|
+
const provider = findProvider(ctx.providers, payload.provider);
|
|
213
|
+
if (!provider?.enabled) {
|
|
214
|
+
return { ok: false, code: 'PROVIDER_DISABLED', reason: 'that provider is no longer available' };
|
|
215
|
+
}
|
|
216
|
+
const client = await resolveClient(ctx, provider, payload.instance);
|
|
217
|
+
if ('error' in client)
|
|
218
|
+
return { ok: false, code: 'CLIENT_UNAVAILABLE', reason: client.error };
|
|
219
|
+
const endpoints = provider.endpoints(payload.instance);
|
|
220
|
+
if (!endpoints)
|
|
221
|
+
return { ok: false, code: 'NO_ENDPOINTS', reason: 'provider has no token endpoint' };
|
|
222
|
+
const body = new URLSearchParams({
|
|
223
|
+
grant_type: 'authorization_code',
|
|
224
|
+
code: input.code,
|
|
225
|
+
redirect_uri: callbackUrl(ctx.config),
|
|
226
|
+
client_id: client.clientId,
|
|
227
|
+
client_secret: client.clientSecret,
|
|
228
|
+
});
|
|
229
|
+
if (provider.pkce)
|
|
230
|
+
body.set('code_verifier', nonce.nonce);
|
|
231
|
+
let token;
|
|
232
|
+
try {
|
|
233
|
+
const r = await safeFetch(endpoints.token, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
236
|
+
body: body.toString(),
|
|
237
|
+
signal: AbortSignal.timeout(20_000),
|
|
238
|
+
});
|
|
239
|
+
if (!r.ok)
|
|
240
|
+
return { ok: false, code: 'EXCHANGE_FAILED', reason: `the provider refused the code (HTTP ${r.status})` };
|
|
241
|
+
token = await r.json();
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
return { ok: false, code: 'EXCHANGE_FAILED', reason: `could not reach the provider: ${err.message}` };
|
|
245
|
+
}
|
|
246
|
+
const accessToken = typeof token.access_token === 'string' ? token.access_token : '';
|
|
247
|
+
if (!accessToken)
|
|
248
|
+
return { ok: false, code: 'EXCHANGE_FAILED', reason: 'the provider returned no access token' };
|
|
249
|
+
const refreshToken = typeof token.refresh_token === 'string' ? token.refresh_token : undefined;
|
|
250
|
+
const expiresAt = typeof token.expires_in === 'number'
|
|
251
|
+
? new Date(Date.now() + token.expires_in * 1000).toISOString()
|
|
252
|
+
// NOT an error and NOT zero: some providers issue tokens that never expire, and treating the
|
|
253
|
+
// absence as "expired" would park a working connection on its first refresh check.
|
|
254
|
+
: null;
|
|
255
|
+
const scopes = typeof token.scope === 'string' ? token.scope.split(/[\s,]+/).filter(Boolean) : provider.scopes;
|
|
256
|
+
const identity = await fetchAccountIdentity(provider, payload.instance, accessToken);
|
|
257
|
+
if ('error' in identity)
|
|
258
|
+
return { ok: false, code: 'IDENTITY_FAILED', reason: identity.error };
|
|
259
|
+
const credential = sealCredential({ shape: provider.credentialShape, accessToken, ...(refreshToken ? { refreshToken } : {}) }, ctx.key);
|
|
260
|
+
const now = new Date().toISOString();
|
|
261
|
+
const existing = await ctx.storage.findConnection(nonce.owner, provider.id, identity.externalId, payload.instance);
|
|
262
|
+
if (existing) {
|
|
263
|
+
// Re-authorising repairs the row it already has. updateConnectionCredential also clears the
|
|
264
|
+
// error state, because a successful exchange IS the evidence that whatever was wrong is not.
|
|
265
|
+
await ctx.storage.updateConnectionCredential(existing.id, credential, expiresAt, scopes);
|
|
266
|
+
const refreshed = await ctx.storage.getConnection(existing.id);
|
|
267
|
+
return { ok: true, connection: refreshed ?? existing, returnUrl: nonce.redirectUri, created: false };
|
|
268
|
+
}
|
|
269
|
+
const connection = {
|
|
270
|
+
id: randomUUID(),
|
|
271
|
+
principal: nonce.owner,
|
|
272
|
+
mode: payload.mode,
|
|
273
|
+
provider: provider.id,
|
|
274
|
+
instance: payload.instance,
|
|
275
|
+
accountLabel: identity.accountLabel,
|
|
276
|
+
externalId: identity.externalId,
|
|
277
|
+
credential,
|
|
278
|
+
credentialShape: provider.credentialShape,
|
|
279
|
+
scopes,
|
|
280
|
+
expiresAt,
|
|
281
|
+
status: 'active',
|
|
282
|
+
lastOkAt: now,
|
|
283
|
+
lastError: null,
|
|
284
|
+
createdAt: now,
|
|
285
|
+
updatedAt: now,
|
|
286
|
+
};
|
|
287
|
+
await ctx.storage.createConnection(connection);
|
|
288
|
+
return { ok: true, connection, returnUrl: nonce.redirectUri, created: true };
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../../../src/services/connections/oauth.ts"],"names":[],"mappings":"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"}
|