aimeat 2.4.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 +186 -20
- package/dist/locales/fi.json +186 -20
- 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/css/views/transparency.css +5 -0
- 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/landing.js +4 -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/ai-transparency-card.js +22 -17
- 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/public/views/transparency.js +50 -0
- 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 +182 -0
- package/dist/scripts/check-ai-disclosure.js.map +1 -1
- package/dist/src/cli/connect/ai-provenance-carry.d.ts +247 -0
- package/dist/src/cli/connect/ai-provenance-carry.d.ts.map +1 -0
- package/dist/src/cli/connect/ai-provenance-carry.js +334 -0
- package/dist/src/cli/connect/ai-provenance-carry.js.map +1 -0
- 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/agent-messages.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/agent-messages.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-messages.js +5 -2
- package/dist/src/cli/connect/mcp/tools/agent-messages.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-tasks.js +5 -2
- package/dist/src/cli/connect/mcp/tools/agent-tasks.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/apps.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.js +13 -5
- package/dist/src/cli/connect/mcp/tools/apps.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/boards.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/boards.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/boards.js +5 -2
- package/dist/src/cli/connect/mcp/tools/boards.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.d.ts +5 -0
- package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.js +25 -5
- package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.js +11 -6
- package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/exchange.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/exchange.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/exchange.js +8 -2
- package/dist/src/cli/connect/mcp/tools/exchange.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.js +7 -3
- package/dist/src/cli/connect/mcp/tools/knowledge.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js +7 -2
- package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/workspaces.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.js +8 -2
- package/dist/src/cli/connect/mcp/tools/workspaces.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.d.ts +3 -0
- package/dist/src/cli/connect/tool-call-defs-core.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.js +7 -0
- package/dist/src/cli/connect/tool-call-defs-core.js.map +1 -1
- package/dist/src/cli/connect/tool-call.d.ts +3 -0
- package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call.js +11 -1
- package/dist/src/cli/connect/tool-call.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/data/public-pages.d.ts +12 -0
- package/dist/src/data/public-pages.d.ts.map +1 -1
- package/dist/src/data/public-pages.js +12 -0
- package/dist/src/data/public-pages.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +969 -85
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.d.ts +45 -2
- package/dist/src/mcp/ai-provenance-input.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.js +48 -2
- 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-transparency.d.ts +6 -1
- package/dist/src/routes/ai-transparency.d.ts.map +1 -1
- package/dist/src/routes/ai-transparency.js +17 -3
- package/dist/src/routes/ai-transparency.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/markdown-negotiation.d.ts +17 -0
- package/dist/src/services/markdown-negotiation.d.ts.map +1 -1
- package/dist/src/services/markdown-negotiation.js +15 -0
- package/dist/src/services/markdown-negotiation.js.map +1 -1
- 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,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file build-stamp.ts
|
|
3
|
+
* @description Read the stamp that `pnpm build` writes into dist/, and answer one question: is the
|
|
4
|
+
* artifact we are executing older than the source it was built from? The CLI daemon consumers
|
|
5
|
+
* spawn (`aimeat connect serve`) runs from dist/, and a stale dist does not fail — it silently
|
|
6
|
+
* drops fields that exist in source, which has now cost three debugging rounds.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately NOT modelled on getSoftwareVersion() in version.ts. That resolver climbs OUT of
|
|
9
|
+
* dist/ to read the live package.json, which is right for advertising a version and precisely
|
|
10
|
+
* why it can never detect staleness: it always reports the source. This one resolves a single
|
|
11
|
+
* path inside dist/ and nowhere else, so "no stamp" unambiguously means "not running from dist".
|
|
12
|
+
* @structure readBuildStamp(): BuildStamp | null; checkBuildFreshness(): BuildFreshness
|
|
13
|
+
* @usage import { checkBuildFreshness } from '../utils/build-stamp.js';
|
|
14
|
+
* @version-history
|
|
15
|
+
* v1.0.0 — 2026-08-02 — Initial stale-dist detection for the serve daemon.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
/** dist/src/utils/build-stamp.js → dist/. In dev (src/utils) this points at a path that does not exist. */
|
|
21
|
+
function stampPath() {
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
return join(here, '..', '..', 'build-stamp.json');
|
|
24
|
+
}
|
|
25
|
+
let cached;
|
|
26
|
+
/** The stamp written into dist/ at build time, or null when not running from a stamped dist. */
|
|
27
|
+
export function readBuildStamp() {
|
|
28
|
+
if (cached !== undefined)
|
|
29
|
+
return cached;
|
|
30
|
+
try {
|
|
31
|
+
cached = JSON.parse(readFileSync(stampPath(), 'utf-8'));
|
|
32
|
+
// eslint-disable-next-line aimeat/no-silent-catch -- absent stamp is the normal dev (tsx) case
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
cached = null;
|
|
36
|
+
}
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
/** Newest mtime (epoch ms) in a tree, or 0 when absent. Mirrors scripts/write-build-stamp.mjs. */
|
|
40
|
+
function newestMtime(dir) {
|
|
41
|
+
let newest = 0;
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
45
|
+
}
|
|
46
|
+
// eslint-disable-next-line aimeat/no-silent-catch -- unreadable dir contributes nothing
|
|
47
|
+
catch {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.isSymbolicLink())
|
|
52
|
+
continue;
|
|
53
|
+
const full = join(dir, entry.name);
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
newest = Math.max(newest, newestMtime(full));
|
|
56
|
+
}
|
|
57
|
+
else if (entry.isFile()) {
|
|
58
|
+
try {
|
|
59
|
+
newest = Math.max(newest, statSync(full).mtimeMs);
|
|
60
|
+
}
|
|
61
|
+
// eslint-disable-next-line aimeat/no-silent-catch -- vanished mid-walk
|
|
62
|
+
catch { /* vanished mid-walk */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return newest;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Slack for mtime comparison, in ms. Some filesystems round mtimes to whole seconds, so a build
|
|
69
|
+
* that recompiled everything correctly must not report itself stale over sub-second jitter.
|
|
70
|
+
*/
|
|
71
|
+
export const MTIME_SLACK_MS = 1000;
|
|
72
|
+
/**
|
|
73
|
+
* The freshness decision, with the filesystem already reduced to one number. Pure so the rules —
|
|
74
|
+
* no stamp means source, no source means unknown, and only a real gap means stale — can be tested
|
|
75
|
+
* without a fixture tree.
|
|
76
|
+
*
|
|
77
|
+
* `newestOnDisk` is the newest source mtime in epoch ms, or 0 when there is no source to compare
|
|
78
|
+
* (a published tarball ships dist/ only). That case answers `unknown`, not `fresh`: we decline to
|
|
79
|
+
* claim a freshness we cannot verify.
|
|
80
|
+
*/
|
|
81
|
+
export function compareFreshness(stamp, newestOnDisk) {
|
|
82
|
+
if (!stamp)
|
|
83
|
+
return { state: 'source', stamp: null, message: null };
|
|
84
|
+
if (newestOnDisk === 0)
|
|
85
|
+
return { state: 'unknown', stamp, message: null };
|
|
86
|
+
if (newestOnDisk > stamp.newest_source_mtime + MTIME_SLACK_MS) {
|
|
87
|
+
return {
|
|
88
|
+
state: 'stale',
|
|
89
|
+
stamp,
|
|
90
|
+
message: `built ${stamp.built_at} from source last touched ${new Date(stamp.newest_source_mtime).toISOString()}, `
|
|
91
|
+
+ `but source on disk is newer (${new Date(newestOnDisk).toISOString()}). `
|
|
92
|
+
+ 'This process is running OLD code — run `pnpm build`.',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { state: 'fresh', stamp, message: null };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Announce which artifact is running, and shout when it is behind its source. Lives here rather
|
|
99
|
+
* than at the call site because describing a build is this module's job, and because every
|
|
100
|
+
* long-running entry point that runs from dist/ wants the same two lines.
|
|
101
|
+
*
|
|
102
|
+
* Warns; never exits. A stale build still serves everything that has not changed, so refusing to
|
|
103
|
+
* start would turn a missing-field bug into an outage.
|
|
104
|
+
*/
|
|
105
|
+
export function announceBuild(freshness, write) {
|
|
106
|
+
if (freshness.state === 'stale') {
|
|
107
|
+
write(`[serve] STALE BUILD — ${freshness.message}`);
|
|
108
|
+
write('[serve] Fields added to source since that build will be dropped without an error.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!freshness.stamp) {
|
|
112
|
+
write('[serve] build: running from source (tsx) — no dist to be stale');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const { version, commit, dirty, built_at: builtAt } = freshness.stamp;
|
|
116
|
+
const short = commit ? commit.slice(0, 8) + (dirty ? '-dirty' : '') : 'no-git';
|
|
117
|
+
write(`[serve] build: v${version} ${short} built ${builtAt}${freshness.state === 'fresh' ? ' (fresh)' : ''}`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build identity for a status payload. A client that sees `state === 'stale'` knows a missing field
|
|
121
|
+
* is the local daemon dropping it, not the node refusing it — the question that has cost three
|
|
122
|
+
* debugging rounds, each ending at a dist/ older than the merge.
|
|
123
|
+
*/
|
|
124
|
+
export function buildIdentity(freshness) {
|
|
125
|
+
return {
|
|
126
|
+
state: freshness.state,
|
|
127
|
+
version: freshness.stamp?.version ?? null,
|
|
128
|
+
commit: freshness.stamp?.commit ?? null,
|
|
129
|
+
dirty: freshness.stamp?.dirty ?? null,
|
|
130
|
+
built_at: freshness.stamp?.built_at ?? null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** Compare the stamped source state against what is on disk now. */
|
|
134
|
+
export function checkBuildFreshness() {
|
|
135
|
+
const stamp = readBuildStamp();
|
|
136
|
+
if (!stamp)
|
|
137
|
+
return compareFreshness(null, 0);
|
|
138
|
+
const pkgRoot = join(dirname(stampPath()), '..');
|
|
139
|
+
const dirs = stamp.source_dirs ?? ['src', 'bin', 'scripts'];
|
|
140
|
+
return compareFreshness(stamp, Math.max(0, ...dirs.map((d) => newestMtime(join(pkgRoot, d)))));
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=build-stamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-stamp.js","sourceRoot":"","sources":["../../../src/utils/build-stamp.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,4 +21,18 @@ export declare function deliveryTargetFor(recipientGhii: string): string;
|
|
|
21
21
|
export declare function conversationIdFor(a: string, b: string): string;
|
|
22
22
|
/** Short single-line preview of a (markdown) message body for notifications + thread lists. */
|
|
23
23
|
export declare function messagePreview(body: string, max?: number): string;
|
|
24
|
+
/**
|
|
25
|
+
* Preview for a message that may have nothing to preview.
|
|
26
|
+
*
|
|
27
|
+
* A voice message is attachments and no body, so the plain body preview is an empty string — which
|
|
28
|
+
* reads on a notification as a message that arrived with nothing in it. Falls back to describing what
|
|
29
|
+
* was actually sent: a sender-supplied transcript first (it IS what they said), then the attachment.
|
|
30
|
+
*/
|
|
31
|
+
export declare function messagePreviewWithAttachments(body: string, attachments?: Array<{
|
|
32
|
+
kind?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
transcript?: {
|
|
35
|
+
text?: string;
|
|
36
|
+
};
|
|
37
|
+
}>, max?: number): string;
|
|
24
38
|
//# sourceMappingURL=messaging.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messaging.d.ts","sourceRoot":"","sources":["../../../src/utils/messaging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH;;;kGAGkG;AAClG,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAM,GAAG,MAAM,CAG9D"}
|
|
1
|
+
{"version":3,"file":"messaging.d.ts","sourceRoot":"","sources":["../../../src/utils/messaging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH;;;kGAGkG;AAClG,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAI/D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAG9D;AAED,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAM,GAAG,MAAM,CAG9D;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,KAAK,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,EACrF,GAAG,SAAM,GACR,MAAM,CAUR"}
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"messaging.js","sourceRoot":"","sources":["../../../src/utils/messaging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C;;;kGAGkG;AAClG,MAAM,UAAU,iBAAiB,CAAC,aAAqB;IACrD,MAAM,CAAC,GAAG,cAAc,CAAC,aAAa,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACtG,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAS,EAAE,CAAS;IACpD,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC"}
|
|
1
|
+
{"version":3,"file":"messaging.js","sourceRoot":"","sources":["../../../src/utils/messaging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C;;;kGAGkG;AAClG,MAAM,UAAU,iBAAiB,CAAC,aAAqB;IACrD,MAAM,CAAC,GAAG,cAAc,CAAC,aAAa,CAAC,CAAC;IACxC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,aAAa,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI;QAAE,OAAO,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IACtG,OAAO,aAAa,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAS,EAAE,CAAS;IACpD,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,+FAA+F;AAC/F,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IACpD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9C,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,6BAA6B,CAC3C,IAAY,EACZ,WAAqF,EACrF,GAAG,GAAG,GAAG;IAET,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACvC,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,IAAI,KAAK,CAAC,UAAU,EAAE,IAAI;QAAE,OAAO,cAAc,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9E,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,kBAAkB,CAAC;IACtD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,UAAU,CAAC;IAC9C,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,UAAU,CAAC;IAC9C,OAAO,MAAM,KAAK,CAAC,IAAI,IAAI,YAAY,EAAE,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file connect/index.js
|
|
3
|
+
* @description The aimeat-connect client library. Exposes `AIMEAT.connect` — the browser side of a
|
|
4
|
+
* principal's own accounts at external services (TARGET-057).
|
|
5
|
+
*
|
|
6
|
+
* WHAT THIS LIBRARY NEVER HAS. A token. Not the access token, not the refresh token, not the
|
|
7
|
+
* ciphertext. The node holds the credential and an app receives provider, label and status; when
|
|
8
|
+
* an app needs to know whether an account can do something, it ASKS, because it cannot know what
|
|
9
|
+
* a provider's own scope names mean. That is not a limitation of this library, it is the point of
|
|
10
|
+
* it, and it is the whole difference from pasting a bearer key into a config file.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IT OWNS instead is the set of things every app would otherwise get wrong: opening the
|
|
13
|
+
* provider window and noticing when it closes, showing `needs_reauth` as something a person can
|
|
14
|
+
* act on rather than a red error, telling a Bluesky user where an app password comes from BEFORE
|
|
15
|
+
* they go looking, and warning an Instagram user about the Business-account requirement BEFORE
|
|
16
|
+
* the attempt rather than after it fails for a reason nobody can read.
|
|
17
|
+
* @structure list() · providers() · start() · attach() · revoke() · on()/off() · panel()
|
|
18
|
+
* @usage <script src="/v1/libs/aimeat-auth.js"></script><script src="/v1/libs/aimeat-connect.js"></script>
|
|
19
|
+
* const accounts = await AIMEAT.connect.list();
|
|
20
|
+
* await AIMEAT.connect.start('mastodon', { instance: 'mastodon.social' });
|
|
21
|
+
* @version-history
|
|
22
|
+
* v1.0.0 — 2026-08-02 — Initial (TARGET-057 phase 3).
|
|
23
|
+
*/
|
|
24
|
+
import { makeSession } from '../_core/session.js';
|
|
25
|
+
import { attach } from '../_core/namespace.js';
|
|
26
|
+
import { mountConnectPanel } from './panel.js';
|
|
27
|
+
import { PROVIDER_NOTES } from './notes.js';
|
|
28
|
+
|
|
29
|
+
const { authFetch } = makeSession('aimeat-connect.js');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} Connection
|
|
33
|
+
* @property {string} id
|
|
34
|
+
* @property {string} provider
|
|
35
|
+
* @property {'personal'|'shared'} mode
|
|
36
|
+
* @property {string} accountLabel What the provider calls this account. Never app-supplied.
|
|
37
|
+
* @property {'active'|'needs_reauth'|'revoked'} status
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** Listeners for "the set of connections changed". */
|
|
41
|
+
const listeners = new Set();
|
|
42
|
+
|
|
43
|
+
function announce() {
|
|
44
|
+
for (const fn of listeners) {
|
|
45
|
+
try { fn(); } catch (err) {
|
|
46
|
+
// One bad listener must not stop the others, but a swallowed handler error is how a panel
|
|
47
|
+
// silently stops updating, so it is reported.
|
|
48
|
+
console.warn('[aimeat-connect] a change listener threw', err);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The caller's own connections. Never anyone else's — the node scopes this, not the caller. */
|
|
54
|
+
async function list() {
|
|
55
|
+
const res = await authFetch('/v1/connections');
|
|
56
|
+
return (res?.data?.connections ?? []);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Providers this node can connect to. An unconfigured one is simply absent. */
|
|
60
|
+
async function providers() {
|
|
61
|
+
const res = await authFetch('/v1/connections/providers');
|
|
62
|
+
return (res?.data?.providers ?? []);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Begin connecting an account, and resolve when the round has finished.
|
|
67
|
+
*
|
|
68
|
+
* The consent happens at the PROVIDER in a window this opens; nothing about the user's credentials
|
|
69
|
+
* passes through the page. Resolution is detected by the window closing plus a re-read of the list,
|
|
70
|
+
* because a cross-origin child window cannot be asked what happened to it — and a promise that
|
|
71
|
+
* resolved on the window opening would report success for a cancelled connection.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} provider
|
|
74
|
+
* @param {{instance?: string, mode?: 'personal'|'shared', signal?: AbortSignal}} [opts]
|
|
75
|
+
* @returns {Promise<{connected: boolean, connection?: Connection}>}
|
|
76
|
+
*/
|
|
77
|
+
async function start(provider, opts = {}) {
|
|
78
|
+
const before = await list();
|
|
79
|
+
const res = await authFetch('/v1/connections/start', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
provider,
|
|
83
|
+
instance: opts.instance,
|
|
84
|
+
mode: opts.mode ?? 'personal',
|
|
85
|
+
// The callback returns the browser here; a same-origin path only, which the node enforces.
|
|
86
|
+
// A static page whose only job is to close the pop-up, because the backend renders no HTML.
|
|
87
|
+
return_url: '/connection-done.html',
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
const url = res?.data?.authorize_url;
|
|
91
|
+
if (!url) throw new Error('the node did not return an authorization URL');
|
|
92
|
+
|
|
93
|
+
const win = window.open(url, 'aimeat-connect', 'width=620,height=760,noopener=no');
|
|
94
|
+
if (!win) throw new Error('the connect window was blocked; allow pop-ups for this site');
|
|
95
|
+
|
|
96
|
+
await waitForRound(before.length, opts.signal);
|
|
97
|
+
|
|
98
|
+
const after = await list();
|
|
99
|
+
const known = new Set(before.map((c) => c.id));
|
|
100
|
+
const added = after.find((c) => !known.has(c.id));
|
|
101
|
+
// A re-authorisation repairs an existing row rather than adding one, so "nothing new appeared"
|
|
102
|
+
// is not the same as "nothing happened" — a status that moved off needs_reauth counts too.
|
|
103
|
+
const repaired = after.find((c) => {
|
|
104
|
+
const prev = before.find((p) => p.id === c.id);
|
|
105
|
+
return prev && prev.status !== 'active' && c.status === 'active';
|
|
106
|
+
});
|
|
107
|
+
announce();
|
|
108
|
+
const connection = added ?? repaired;
|
|
109
|
+
return { connected: Boolean(connection), connection };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Wait for the round to finish, WITHOUT touching the pop-up window.
|
|
114
|
+
*
|
|
115
|
+
* This node sets a Cross-Origin-Opener-Policy, which severs the opener relationship as soon as the
|
|
116
|
+
* pop-up navigates to the provider. `win.closed` is then unreadable — the first version of this
|
|
117
|
+
* polled it and logged ten "COOP would block the window.closed call" errors per connection while
|
|
118
|
+
* appearing to work, because the flow completed for other reasons.
|
|
119
|
+
*
|
|
120
|
+
* Two signals instead, and neither goes near the window:
|
|
121
|
+
* - the done page announces on a same-origin BroadcastChannel, which COOP does not affect
|
|
122
|
+
* - the connection list is polled, which is what ACTUALLY decides success and also covers the
|
|
123
|
+
* user who closes the pop-up before it ever reaches the done page
|
|
124
|
+
*
|
|
125
|
+
* Resolves either way; gives up after the timeout rather than waiting forever on a cancelled round.
|
|
126
|
+
*/
|
|
127
|
+
async function waitForRound(countBefore, signal) {
|
|
128
|
+
const DEADLINE = Date.now() + 180_000;
|
|
129
|
+
let channel = null;
|
|
130
|
+
let announced = false;
|
|
131
|
+
let aborted = false;
|
|
132
|
+
try {
|
|
133
|
+
channel = new BroadcastChannel('aimeat-connect');
|
|
134
|
+
channel.onmessage = () => { announced = true; };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.warn('[aimeat-connect] BroadcastChannel unavailable; falling back to polling', err);
|
|
137
|
+
}
|
|
138
|
+
if (signal) signal.addEventListener('abort', () => { aborted = true; }, { once: true });
|
|
139
|
+
try {
|
|
140
|
+
while (Date.now() < DEADLINE && !aborted) {
|
|
141
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
142
|
+
if (announced) return true;
|
|
143
|
+
const now = await list();
|
|
144
|
+
if (now.length > countBefore) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
} finally {
|
|
148
|
+
if (channel) channel.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Connect an account by SUPPLYING a credential, for a provider with no authorization round.
|
|
154
|
+
*
|
|
155
|
+
* Separate from start() because the two must never overlap: attaching a provider that HAS a consent
|
|
156
|
+
* screen would be a way to skip it, and the node refuses that in both directions. A provider's
|
|
157
|
+
* `attachFields` (from providers()) is what tells them apart.
|
|
158
|
+
*
|
|
159
|
+
* @param {string} provider
|
|
160
|
+
* @param {Record<string,string>} fields
|
|
161
|
+
*/
|
|
162
|
+
async function attachAccount(provider, fields) {
|
|
163
|
+
const res = await authFetch('/v1/connections/attach', {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
body: JSON.stringify({ provider, mode: 'personal', fields }),
|
|
166
|
+
});
|
|
167
|
+
announce();
|
|
168
|
+
return { connected: true, connection: res?.data?.connection };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Disconnect an account. The node tells the provider first where it can, and removes the credential
|
|
173
|
+
* either way — so this always ends with the account disconnected here.
|
|
174
|
+
*/
|
|
175
|
+
async function revoke(connectionId) {
|
|
176
|
+
const res = await authFetch(`/v1/connections/${encodeURIComponent(connectionId)}`, { method: 'DELETE' });
|
|
177
|
+
announce();
|
|
178
|
+
return { revoked: true, toldProvider: Boolean(res?.data?.told_provider) };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Subscribe to "the set of connections changed". Returns an unsubscribe function. */
|
|
182
|
+
function on(fn) {
|
|
183
|
+
listeners.add(fn);
|
|
184
|
+
return () => listeners.delete(fn);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function off(fn) { listeners.delete(fn); }
|
|
188
|
+
|
|
189
|
+
const connect = {
|
|
190
|
+
list, providers, start, attach: attachAccount, revoke, on, off,
|
|
191
|
+
/** Per-provider things a user must be told BEFORE they try. See notes.js. */
|
|
192
|
+
notes: PROVIDER_NOTES,
|
|
193
|
+
/** Mount the ready-made panel. See panel.js. */
|
|
194
|
+
panel: (opts) => mountConnectPanel(connect, opts),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
attach('connect', connect);
|
|
198
|
+
|
|
199
|
+
export default connect;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file connect/notes.js
|
|
3
|
+
* @description What a user has to be told BEFORE they try to connect a provider (TARGET-057).
|
|
4
|
+
*
|
|
5
|
+
* Every note here exists because the failure it prevents is unreadable when it happens. An
|
|
6
|
+
* Instagram personal account authorises perfectly and then cannot publish, with an error that
|
|
7
|
+
* names neither the account type nor the fix. A Bluesky user hunting for a password that is not
|
|
8
|
+
* their password finds their password. A Telegram bot that was never added to the channel fails
|
|
9
|
+
* at the last step of the flow rather than the first.
|
|
10
|
+
*
|
|
11
|
+
* These are strings and not logic on purpose: they are the copy the panel shows, and an app that
|
|
12
|
+
* builds its own surface should be able to show the same words rather than invent worse ones.
|
|
13
|
+
* @structure PROVIDER_NOTES — keyed by provider id
|
|
14
|
+
* @usage import { PROVIDER_NOTES } from './notes.js'; PROVIDER_NOTES.bluesky.before
|
|
15
|
+
* @version-history
|
|
16
|
+
* v1.0.0 — 2026-08-02 — Initial (TARGET-057 phase 3).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} ProviderNote
|
|
21
|
+
* @property {string} [needs] What the user must supply, if anything beyond pressing the button.
|
|
22
|
+
* @property {string} [before] Shown BEFORE the attempt. The failure it prevents is unreadable.
|
|
23
|
+
* @property {string} [where] Where in the provider's own UI to find what they need.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** @type {Record<string, ProviderNote>} */
|
|
27
|
+
export const PROVIDER_NOTES = {
|
|
28
|
+
mastodon: {
|
|
29
|
+
needs: 'The address of your instance, for example mastodon.social.',
|
|
30
|
+
before: 'Your account lives at one instance, and the same name at another instance is a different account.',
|
|
31
|
+
},
|
|
32
|
+
youtube: {
|
|
33
|
+
before: 'Sign in with the Google account that owns the channel. A Google account with no YouTube channel will connect and then have nowhere to publish.',
|
|
34
|
+
},
|
|
35
|
+
bluesky: {
|
|
36
|
+
needs: 'An app password. This is NOT your Bluesky password.',
|
|
37
|
+
where: 'Bluesky → Settings → Privacy and security → App passwords.',
|
|
38
|
+
before: 'This is the one provider that asks you to copy a secret to us instead of approving on their site. You can revoke it from Bluesky at any time, and it cannot be used to sign in as you.',
|
|
39
|
+
},
|
|
40
|
+
instagram: {
|
|
41
|
+
before: 'Instagram publishing needs a Business or Creator account linked to a Facebook Page. A personal account will connect and then refuse to publish, with an error that does not say why.',
|
|
42
|
+
},
|
|
43
|
+
telegram: {
|
|
44
|
+
needs: 'A bot token from @BotFather, and the bot added to your channel with permission to post.',
|
|
45
|
+
before: 'Adding the bot to the channel is a step only you can do; without it the connection works and publishing does not.',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file connect/panel.js
|
|
3
|
+
* @description The ready-made connected-accounts panel (TARGET-057). Mounted by the profile's
|
|
4
|
+
* Access tab and available to any app that wants the same surface.
|
|
5
|
+
*
|
|
6
|
+
* THE STATUS ROW IS THE POINT. A connection that needs re-authorising is not an error and must not
|
|
7
|
+
* look like one: it is a thing the owner can fix in two clicks, and it is shown as a button that
|
|
8
|
+
* fixes it. Rendering it as red text is how a working feature becomes a support question.
|
|
9
|
+
*
|
|
10
|
+
* Like the IAM panel, this emits neutral `aim-*` hooks and a stylesheet built on `currentColor`
|
|
11
|
+
* and inherited font, so it takes the host page's design instead of imposing one. Nothing here
|
|
12
|
+
* hardcodes a brand colour.
|
|
13
|
+
* @structure mountConnectPanel(connect, opts)
|
|
14
|
+
* @usage AIMEAT.connect.panel({ target: '#accounts' })
|
|
15
|
+
* @version-history
|
|
16
|
+
* v1.0.0 — 2026-08-02 — Initial (TARGET-057 phase 3).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const STYLE_ID = 'aimeat-connect-style';
|
|
20
|
+
|
|
21
|
+
function el(tag, opts, children) {
|
|
22
|
+
const node = document.createElement(tag);
|
|
23
|
+
const o = opts || {};
|
|
24
|
+
if (o.cls) node.className = o.cls;
|
|
25
|
+
if (o.text != null) node.textContent = o.text;
|
|
26
|
+
if (o.attrs) for (const k of Object.keys(o.attrs)) node.setAttribute(k, o.attrs[k]);
|
|
27
|
+
if (o.on) for (const k of Object.keys(o.on)) node.addEventListener(k, o.on[k]);
|
|
28
|
+
for (const c of children || []) if (c) node.appendChild(c);
|
|
29
|
+
return node;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Plain, inherited, and scrollable in its own right. The horizontal scroll lives on the list rather
|
|
34
|
+
* than the page: a long account label must not make the whole profile scroll sideways.
|
|
35
|
+
*/
|
|
36
|
+
function injectStyle() {
|
|
37
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
38
|
+
const css = `
|
|
39
|
+
.aim-conn { font: inherit; color: inherit; }
|
|
40
|
+
.aim-conn-list { display: flex; flex-direction: column; gap: .5rem; margin: .75rem 0; }
|
|
41
|
+
.aim-conn-row { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem .75rem;
|
|
42
|
+
padding: .6rem .75rem; border: 1px solid currentColor; border-radius: .5rem; opacity: .95; }
|
|
43
|
+
.aim-conn-main { display: flex; flex-direction: column; min-width: 0; flex: 1 1 12rem; }
|
|
44
|
+
.aim-conn-label { font-weight: 600; overflow-wrap: anywhere; }
|
|
45
|
+
.aim-conn-meta { font-size: .85em; opacity: .7; }
|
|
46
|
+
.aim-conn-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
|
|
47
|
+
.aim-conn-btn { font: inherit; color: inherit; background: transparent; cursor: pointer;
|
|
48
|
+
border: 1px solid currentColor; border-radius: .4rem; padding: .35rem .7rem; }
|
|
49
|
+
.aim-conn-btn[disabled] { opacity: .5; cursor: default; }
|
|
50
|
+
.aim-conn-note { font-size: .85em; opacity: .75; margin: .35rem 0 0; }
|
|
51
|
+
.aim-conn-empty { opacity: .7; margin: .75rem 0; }
|
|
52
|
+
.aim-conn-add { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-top: .75rem; }
|
|
53
|
+
.aim-conn-input { font: inherit; color: inherit; background: transparent; padding: .35rem .5rem;
|
|
54
|
+
border: 1px solid currentColor; border-radius: .4rem; min-width: 0; flex: 1 1 12rem; }
|
|
55
|
+
.aim-conn-err { font-size: .9em; margin: .5rem 0 0; }
|
|
56
|
+
`;
|
|
57
|
+
const style = document.createElement('style');
|
|
58
|
+
style.id = STYLE_ID;
|
|
59
|
+
style.textContent = css;
|
|
60
|
+
document.head.appendChild(style);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_STRINGS = {
|
|
64
|
+
title: 'Connected accounts',
|
|
65
|
+
intro: 'Accounts you have connected so apps can act at those services on your behalf. The account credential stays on this node; an app is only ever told which account it may use.',
|
|
66
|
+
empty: 'You have not connected any accounts yet.',
|
|
67
|
+
add: 'Connect',
|
|
68
|
+
connecting: 'Connecting…',
|
|
69
|
+
disconnect: 'Disconnect',
|
|
70
|
+
reconnect: 'Reconnect',
|
|
71
|
+
needsReauth: 'needs reconnecting',
|
|
72
|
+
instancePlaceholder: 'instance address, e.g. mastodon.social',
|
|
73
|
+
confirm: 'Disconnect this account? Apps that publish to it will stop being able to.',
|
|
74
|
+
toldProvider: 'Disconnected, and the service was told.',
|
|
75
|
+
notToldProvider: 'Disconnected here. The service could not be reached to be told, so check it there too.',
|
|
76
|
+
unavailable: 'Connecting accounts is not enabled on this node.',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Mount the panel.
|
|
81
|
+
*
|
|
82
|
+
* @param {any} connect The AIMEAT.connect surface.
|
|
83
|
+
* @param {{target?: string|Element, strings?: Record<string,string>, styles?: boolean}} [opts]
|
|
84
|
+
*/
|
|
85
|
+
export async function mountConnectPanel(connect, opts = {}) {
|
|
86
|
+
const host = typeof opts.target === 'string' ? document.querySelector(opts.target) : opts.target;
|
|
87
|
+
// Named rather than a bare null-deref: "target not found" is the only thing that goes wrong here
|
|
88
|
+
// and the selector is usually a typo.
|
|
89
|
+
if (!host) throw new Error('aimeat-connect: panel target not found');
|
|
90
|
+
if (opts.styles !== false) injectStyle();
|
|
91
|
+
const s = { ...DEFAULT_STRINGS, ...(opts.strings || {}) };
|
|
92
|
+
|
|
93
|
+
const root = el('div', { cls: 'aim-conn' });
|
|
94
|
+
host.replaceChildren(root);
|
|
95
|
+
|
|
96
|
+
let providers = [];
|
|
97
|
+
let busy = false;
|
|
98
|
+
|
|
99
|
+
async function render() {
|
|
100
|
+
let accounts;
|
|
101
|
+
try {
|
|
102
|
+
[accounts, providers] = await Promise.all([connect.list(), connect.providers()]);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// A capability that is switched off answers 503; saying so beats an empty panel that looks
|
|
105
|
+
// like "you have no accounts".
|
|
106
|
+
root.replaceChildren(el('p', { cls: 'aim-conn-err', text: `${s.unavailable} (${(err && err.message) || err})` }));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rows = accounts.map((c) => {
|
|
111
|
+
const needsReauth = c.status === 'needs_reauth';
|
|
112
|
+
return el('div', { cls: 'aim-conn-row' }, [
|
|
113
|
+
el('div', { cls: 'aim-conn-main' }, [
|
|
114
|
+
el('span', { cls: 'aim-conn-label', text: c.accountLabel }),
|
|
115
|
+
el('span', {
|
|
116
|
+
cls: 'aim-conn-meta',
|
|
117
|
+
text: needsReauth ? `${c.provider} · ${s.needsReauth}` : c.provider,
|
|
118
|
+
}),
|
|
119
|
+
]),
|
|
120
|
+
el('div', { cls: 'aim-conn-actions' }, [
|
|
121
|
+
// Shown as a FIX, not as an error. Two clicks and it works again.
|
|
122
|
+
needsReauth
|
|
123
|
+
? el('button', {
|
|
124
|
+
cls: 'aim-conn-btn', text: s.reconnect, attrs: { type: 'button' },
|
|
125
|
+
on: { click: () => void beginConnect(c.provider, null) },
|
|
126
|
+
})
|
|
127
|
+
: null,
|
|
128
|
+
el('button', {
|
|
129
|
+
cls: 'aim-conn-btn', text: s.disconnect, attrs: { type: 'button' },
|
|
130
|
+
on: {
|
|
131
|
+
click: () => {
|
|
132
|
+
if (!window.confirm(s.confirm)) return;
|
|
133
|
+
void (async () => {
|
|
134
|
+
const r = await connect.revoke(c.id);
|
|
135
|
+
await render();
|
|
136
|
+
// Honest about which half happened: a token still live at the provider is
|
|
137
|
+
// something the owner may want to go and remove there.
|
|
138
|
+
window.alert(r.toldProvider ? s.toldProvider : s.notToldProvider);
|
|
139
|
+
})();
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
]),
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const list = rows.length
|
|
148
|
+
? el('div', { cls: 'aim-conn-list' }, rows)
|
|
149
|
+
: el('p', { cls: 'aim-conn-empty', text: s.empty });
|
|
150
|
+
|
|
151
|
+
root.replaceChildren(
|
|
152
|
+
el('p', { cls: 'aim-conn-note', text: s.intro }),
|
|
153
|
+
list,
|
|
154
|
+
...providers.map(renderAdd),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderAdd(p) {
|
|
159
|
+
const note = (connect.notes && connect.notes[p.id]) || {};
|
|
160
|
+
let instanceInput = null;
|
|
161
|
+
if (p.instanceScoped) {
|
|
162
|
+
instanceInput = el('input', {
|
|
163
|
+
cls: 'aim-conn-input',
|
|
164
|
+
attrs: { type: 'text', placeholder: s.instancePlaceholder, 'aria-label': s.instancePlaceholder },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// A provider with attachFields is connected by supplying a credential; one without has a
|
|
168
|
+
// consent screen. Rendering from the declaration means the panel needs to know nothing about
|
|
169
|
+
// any particular provider, and a provider with NEITHER cannot get a button at all.
|
|
170
|
+
const attachInputs = (p.attachFields || []).map((f) => el('input', {
|
|
171
|
+
cls: 'aim-conn-input',
|
|
172
|
+
attrs: {
|
|
173
|
+
type: f.secret ? 'password' : 'text',
|
|
174
|
+
autocomplete: f.secret ? 'new-password' : 'off',
|
|
175
|
+
placeholder: f.placeholder || f.label,
|
|
176
|
+
'aria-label': f.label,
|
|
177
|
+
'data-field': f.name,
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
const btn = el('button', {
|
|
181
|
+
cls: 'aim-conn-btn',
|
|
182
|
+
text: `${s.add} ${p.label}`,
|
|
183
|
+
attrs: { type: 'button' },
|
|
184
|
+
on: {
|
|
185
|
+
click: () => {
|
|
186
|
+
if (!p.attachFields) return void beginConnect(p.id, instanceInput);
|
|
187
|
+
const fields = {};
|
|
188
|
+
for (const input of attachInputs) fields[input.getAttribute('data-field')] = input.value;
|
|
189
|
+
void beginAttach(p.id, fields, attachInputs);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
return el('div', {}, [
|
|
194
|
+
el('div', { cls: 'aim-conn-add' }, [instanceInput, ...attachInputs, btn]),
|
|
195
|
+
// Before the attempt, always. Each of these prevents a failure whose message does not
|
|
196
|
+
// explain itself.
|
|
197
|
+
note.needs ? el('p', { cls: 'aim-conn-note', text: note.needs }) : null,
|
|
198
|
+
note.where ? el('p', { cls: 'aim-conn-note', text: note.where }) : null,
|
|
199
|
+
note.before ? el('p', { cls: 'aim-conn-note', text: note.before }) : null,
|
|
200
|
+
]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function beginConnect(providerId, instanceInput) {
|
|
204
|
+
if (busy) return;
|
|
205
|
+
busy = true;
|
|
206
|
+
const instance = instanceInput ? instanceInput.value.trim() : undefined;
|
|
207
|
+
try {
|
|
208
|
+
await connect.start(providerId, { instance });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const msg = (err && err.message) || String(err);
|
|
211
|
+
root.appendChild(el('p', { cls: 'aim-conn-err', text: msg }));
|
|
212
|
+
} finally {
|
|
213
|
+
busy = false;
|
|
214
|
+
await render();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function beginAttach(providerId, fields, inputs) {
|
|
219
|
+
if (busy) return;
|
|
220
|
+
busy = true;
|
|
221
|
+
try {
|
|
222
|
+
await connect.attach(providerId, fields);
|
|
223
|
+
// Cleared on success, so a secret does not sit in a form field after it has been stored.
|
|
224
|
+
for (const i of inputs) i.value = '';
|
|
225
|
+
} catch (err) {
|
|
226
|
+
// The node's reason is the useful half: "that is your account password, not an app password"
|
|
227
|
+
// is actionable in a way that a generic failure is not.
|
|
228
|
+
root.appendChild(el('p', { cls: 'aim-conn-err', text: (err && err.message) || String(err) }));
|
|
229
|
+
} finally {
|
|
230
|
+
busy = false;
|
|
231
|
+
await render();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
connect.on(() => void render());
|
|
236
|
+
await render();
|
|
237
|
+
return { refresh: render };
|
|
238
|
+
}
|