@stage-labs/metro 0.1.0-beta.176 → 0.1.0-beta.177
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/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/core/package.json +1 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/account-files.ts +41 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +2 -0
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +12 -0
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +6 -5
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +12 -0
- package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +2 -1
- package/runtime/node_modules/@metro-labs/outlook/src/login.ts +10 -3
- package/runtime/node_modules/@metro-labs/outlook/src/me.ts +19 -3
- package/runtime/node_modules/@metro-labs/outlook/src/state.ts +3 -7
- package/runtime/node_modules/@metro-labs/outlook/src/station.ts +3 -0
- package/runtime/node_modules/@metro-labs/threema/src/groups.ts +5 -4
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +3 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +3 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/token-store.ts +3 -7
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.177",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.177",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"./lines": "./src/lines.ts",
|
|
20
20
|
"./endpoints": "./src/endpoints.ts",
|
|
21
21
|
"./stations/types": "./src/stations/types.ts",
|
|
22
|
+
"./stations/account-files": "./src/stations/account-files.ts",
|
|
22
23
|
"./stations/station-runtime": "./src/stations/station-runtime.ts",
|
|
23
24
|
"./stations/account-store": "./src/stations/account-store.ts",
|
|
24
25
|
"./stations/attachments": "./src/stations/attachments.ts",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { log } from '../log.js';
|
|
5
|
+
|
|
6
|
+
export interface AccountFiles {
|
|
7
|
+
path: (accountId: string) => string;
|
|
8
|
+
forget: (accountId: string) => void;
|
|
9
|
+
forgetExcept: (keptAccountIds: readonly string[]) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const safeSegment = (s: string): string => s.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
13
|
+
|
|
14
|
+
function listed(dir: string): string[] {
|
|
15
|
+
try {
|
|
16
|
+
return readdirSync(dir);
|
|
17
|
+
} catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function accountFiles(dirEnv: string, prefix: string): AccountFiles {
|
|
23
|
+
const dir = (): string => process.env[dirEnv] ?? join(homedir(), '.metro');
|
|
24
|
+
const name = (accountId: string): string => `${prefix}${safeSegment(accountId)}.json`;
|
|
25
|
+
const remove = (file: string, reason: string): void => {
|
|
26
|
+
rmSync(join(dir(), file), { force: true });
|
|
27
|
+
log.info({ file: join(dir(), file) }, reason);
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
path: (accountId) => join(dir(), name(accountId)),
|
|
31
|
+
forget: (accountId) => {
|
|
32
|
+
remove(name(accountId), 'station: removed the files of a detached account');
|
|
33
|
+
},
|
|
34
|
+
forgetExcept: (keptAccountIds) => {
|
|
35
|
+
const kept = new Set(keptAccountIds.map(name));
|
|
36
|
+
for (const file of listed(dir()))
|
|
37
|
+
if (file.startsWith(prefix) && file.endsWith('.json') && !kept.has(file))
|
|
38
|
+
remove(file, 'station: removed the files of an account that is no longer attached');
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -108,6 +108,8 @@ export interface Station {
|
|
|
108
108
|
readFilters?: ReadonlySet<ReadFilter>;
|
|
109
109
|
approvals?: boolean;
|
|
110
110
|
attachmentMode: AttachmentMode;
|
|
111
|
+
forget?: (accountId: string) => void;
|
|
112
|
+
forgetExcept?: (keptAccountIds: readonly string[]) => void;
|
|
111
113
|
sendAttachments?(
|
|
112
114
|
line: string,
|
|
113
115
|
atts: CanonicalAttachment[],
|
|
@@ -246,6 +246,18 @@ async function handleDetach(
|
|
|
246
246
|
detached: true,
|
|
247
247
|
activated: await activate(deps, ref.station),
|
|
248
248
|
});
|
|
249
|
+
forgetAccount(ref.station, ref.accountId);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function forgetAccount(station: StationName, accountId: string): void {
|
|
253
|
+
try {
|
|
254
|
+
stationByName(station)?.forget?.(accountId);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
log.warn(
|
|
257
|
+
{ station, account: accountId, err: errMsg(err) },
|
|
258
|
+
'account-api: could not remove the files of a detached account',
|
|
259
|
+
);
|
|
260
|
+
}
|
|
249
261
|
}
|
|
250
262
|
|
|
251
263
|
async function applied(deps: AccountApiDeps): Promise<boolean> {
|
|
@@ -36,9 +36,11 @@ import { createMetroMcp } from '../mcp/index.js';
|
|
|
36
36
|
import { gatherAccountsForAgents } from '../mcp/accounts.js';
|
|
37
37
|
import {
|
|
38
38
|
accountStationCapabilities,
|
|
39
|
+
forgetOrphans,
|
|
39
40
|
stationByName,
|
|
40
41
|
} from '../stations/registry.js';
|
|
41
42
|
import { prepareAccount } from '../stations/attach.js';
|
|
43
|
+
import { knownAccounts } from '../agents/map.js';
|
|
42
44
|
import { materializeFrom, reloadFrom } from '../stations/materialize.js';
|
|
43
45
|
import type { StationName } from '@metro-labs/core/station-names';
|
|
44
46
|
import { startUploadReaper } from '../files/upload-store.js';
|
|
@@ -116,6 +118,7 @@ installBearerSessions(agentsDir(), localOwner);
|
|
|
116
118
|
migrateAgentLayout();
|
|
117
119
|
log.info({ agent: await ensureLocalAgent() }, 'local daemon: agent');
|
|
118
120
|
await materializeFrom(fileSource);
|
|
121
|
+
forgetOrphans(knownAccounts());
|
|
119
122
|
supervisor.start();
|
|
120
123
|
const metroMcp = await createMetroMcp();
|
|
121
124
|
webhookServer = await startWebhookServer(
|
|
@@ -127,7 +127,7 @@ export async function startInteractiveAttach(
|
|
|
127
127
|
input: Record<string, unknown>,
|
|
128
128
|
hooks: DriverHooks,
|
|
129
129
|
): Promise<StartedAttach> {
|
|
130
|
-
if (station === 'outlook') return startOutlook(hooks);
|
|
130
|
+
if (station === 'outlook') return startOutlook(input, hooks);
|
|
131
131
|
return station === 'telegram'
|
|
132
132
|
? startTelegramUser(input, hooks)
|
|
133
133
|
: startWhatsapp(input, hooks);
|
|
@@ -11,13 +11,14 @@ const BROWSER_TTL_MS = 15 * 60_000;
|
|
|
11
11
|
|
|
12
12
|
const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
13
13
|
|
|
14
|
-
export async function startOutlook(hooks: DriverHooks): Promise<StartedAttach> {
|
|
15
|
-
const { OutlookBrowserLogin, OutlookLogin: DeviceLogin, OutlookLoginError, failureOf } = await import('@metro-labs/outlook/login');
|
|
14
|
+
export async function startOutlook(input: Record<string, unknown>, hooks: DriverHooks): Promise<StartedAttach> {
|
|
15
|
+
const { OutlookBrowserLogin, OutlookLogin: DeviceLogin, OutlookLoginError, failureOf, parseMailbox } = await import('@metro-labs/outlook/login');
|
|
16
16
|
const said = (err: unknown, fallback: string): string =>
|
|
17
17
|
err instanceof OutlookLoginError && err.message !== '' ? err.message : fallback;
|
|
18
|
-
const browser = (() => {
|
|
18
|
+
const { browser, mailbox } = (() => {
|
|
19
19
|
try {
|
|
20
|
-
|
|
20
|
+
const wanted = parseMailbox(input.mailbox);
|
|
21
|
+
return { browser: new OutlookBrowserLogin({ mailbox: wanted }), mailbox: wanted };
|
|
21
22
|
} catch (err) {
|
|
22
23
|
throw new StationAttachError(said(err, 'Microsoft refused to start the sign-in'), 400);
|
|
23
24
|
}
|
|
@@ -26,7 +27,7 @@ export async function startOutlook(hooks: DriverHooks): Promise<StartedAttach> {
|
|
|
26
27
|
|
|
27
28
|
const toDevice = async (): Promise<void> => {
|
|
28
29
|
if (device !== null) return;
|
|
29
|
-
const login = new DeviceLogin({ onDone: hooks.done, onFailed: hooks.fail });
|
|
30
|
+
const login = new DeviceLogin({ onDone: hooks.done, onFailed: hooks.fail }, { mailbox });
|
|
30
31
|
const code = await login.start().catch((err: unknown) => {
|
|
31
32
|
throw new StationAttachError(said(err, 'Microsoft refused to start the sign-in'), 400);
|
|
32
33
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Station } from '@metro-labs/core/stations/types';
|
|
2
2
|
import { Line } from '@metro-labs/core/lines';
|
|
3
|
+
import { errMsg, log } from '@metro-labs/core/log';
|
|
3
4
|
import { xmtpStation } from '@metro-labs/xmtp';
|
|
4
5
|
import { telegramBotStation } from '@metro-labs/telegram-bot';
|
|
5
6
|
import { telegramStation } from '@metro-labs/telegram';
|
|
@@ -37,3 +38,14 @@ export const accountStationCapabilities = (): Record<string, string[]> => {
|
|
|
37
38
|
if (s.hasAccounts) out[s.name] = [...s.messageVerbs].sort();
|
|
38
39
|
return out;
|
|
39
40
|
};
|
|
41
|
+
|
|
42
|
+
export function forgetOrphans(known: readonly { station: string; id: string }[]): void {
|
|
43
|
+
for (const s of STATIONS) {
|
|
44
|
+
if (s.forgetExcept === undefined) continue;
|
|
45
|
+
try {
|
|
46
|
+
s.forgetExcept(known.filter((a) => a.station === s.name).map((a) => a.id));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
log.warn({ station: s.name, err: errMsg(err) }, 'stations: could not remove the files of detached accounts');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -13,8 +13,9 @@ export function pkcePair(verifier = randomBytes(32).toString('base64url')): Pkce
|
|
|
13
13
|
|
|
14
14
|
export const newState = (): string => randomBytes(24).toString('base64url');
|
|
15
15
|
|
|
16
|
-
export function authorizeUrl(challenge: string, state: string): string {
|
|
16
|
+
export function authorizeUrl(challenge: string, state: string, loginHint: string | null = null): string {
|
|
17
17
|
const query = new URLSearchParams({
|
|
18
|
+
...(loginHint === null ? {} : { login_hint: loginHint }),
|
|
18
19
|
client_id: requireClientId(),
|
|
19
20
|
response_type: 'code',
|
|
20
21
|
redirect_uri: redirectUri(),
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { authorizeUrl, newState, pkcePair, redeemCode, type Pkce } from './browser.js';
|
|
11
11
|
import { verifyMailbox, type Mailbox } from './me.js';
|
|
12
12
|
|
|
13
|
+
export { parseMailbox } from './me.js';
|
|
14
|
+
|
|
13
15
|
export { OutlookAuthError as OutlookLoginError } from './auth.js';
|
|
14
16
|
export { NOT_SET_UP } from './config.js';
|
|
15
17
|
export { failureOf } from './auth.js';
|
|
@@ -27,6 +29,7 @@ export interface OutlookLoginEvents {
|
|
|
27
29
|
export interface OutlookLoginDeps {
|
|
28
30
|
fetch?: FetchLike;
|
|
29
31
|
now?: () => number;
|
|
32
|
+
mailbox?: string | null;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const SLOW_DOWN_MS = 5_000;
|
|
@@ -52,11 +55,13 @@ export class OutlookBrowserLogin {
|
|
|
52
55
|
private used = false;
|
|
53
56
|
private readonly fetchImpl: FetchLike;
|
|
54
57
|
private readonly now: () => number;
|
|
58
|
+
private readonly mailbox: string | null;
|
|
55
59
|
|
|
56
60
|
constructor(deps: OutlookLoginDeps = {}) {
|
|
57
61
|
this.fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
58
62
|
this.now = deps.now ?? Date.now;
|
|
59
|
-
this.
|
|
63
|
+
this.mailbox = deps.mailbox ?? null;
|
|
64
|
+
this.authorizeUrl = authorizeUrl(this.pkce.challenge, this.state, this.mailbox);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
async finish(code: string, state: string): Promise<OutlookLoginResult> {
|
|
@@ -65,7 +70,7 @@ export class OutlookBrowserLogin {
|
|
|
65
70
|
if (this.used) throw new OutlookAuthError('This sign-in was already used. Start again from the Channels page.');
|
|
66
71
|
this.used = true;
|
|
67
72
|
const { tokens, tenantId } = await redeemCode(code, this.pkce.verifier, this.fetchImpl, this.now());
|
|
68
|
-
return resultOf(tokens, tenantId, await verifyMailbox(tokens.accessToken, this.fetchImpl), this.now());
|
|
73
|
+
return resultOf(tokens, tenantId, await verifyMailbox(tokens.accessToken, this.fetchImpl, this.mailbox), this.now());
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
|
|
@@ -75,10 +80,12 @@ export class OutlookLogin {
|
|
|
75
80
|
private stopped = false;
|
|
76
81
|
private readonly fetchImpl: FetchLike;
|
|
77
82
|
private readonly now: () => number;
|
|
83
|
+
private readonly mailbox: string | null;
|
|
78
84
|
|
|
79
85
|
constructor(private readonly events: OutlookLoginEvents, deps: OutlookLoginDeps = {}) {
|
|
80
86
|
this.fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
81
87
|
this.now = deps.now ?? Date.now;
|
|
88
|
+
this.mailbox = deps.mailbox ?? null;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
async start(): Promise<DeviceCode> {
|
|
@@ -135,7 +142,7 @@ export class OutlookLogin {
|
|
|
135
142
|
this.fail(poll.message);
|
|
136
143
|
return;
|
|
137
144
|
}
|
|
138
|
-
const mailbox = await verifyMailbox(poll.tokens.accessToken, this.fetchImpl);
|
|
145
|
+
const mailbox = await verifyMailbox(poll.tokens.accessToken, this.fetchImpl, this.mailbox);
|
|
139
146
|
if (this.stopped) return;
|
|
140
147
|
this.stopped = true;
|
|
141
148
|
this.events.onDone(resultOf(poll.tokens, poll.tenantId, mailbox, this.now()));
|
|
@@ -8,7 +8,20 @@ export interface Mailbox {
|
|
|
8
8
|
|
|
9
9
|
const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const MAILBOX_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
12
|
+
|
|
13
|
+
export function parseMailbox(value: unknown): string | null {
|
|
14
|
+
const mailbox = text(value).toLowerCase();
|
|
15
|
+
if (mailbox === '') return null;
|
|
16
|
+
if (!MAILBOX_RE.test(mailbox))
|
|
17
|
+
throw new OutlookAuthError('Type the whole address of the mailbox, like andy@company.com, or leave it blank.');
|
|
18
|
+
return mailbox;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const wrongMailbox = (actual: string, wanted: string): string =>
|
|
22
|
+
`You signed in as ${actual}, not ${wanted}. Nothing was connected. Start again and pick ${wanted} on Microsoft's page, or use "Use another account".`;
|
|
23
|
+
|
|
24
|
+
export async function verifyMailbox(accessToken: string, fetchImpl: FetchLike, wanted: string | null = null): Promise<Mailbox> {
|
|
12
25
|
let res: Response;
|
|
13
26
|
try {
|
|
14
27
|
res = await fetchImpl(`${graphBase()}/me?$select=mail,userPrincipalName,displayName`, {
|
|
@@ -19,7 +32,10 @@ export async function verifyMailbox(accessToken: string, fetchImpl: FetchLike):
|
|
|
19
32
|
}
|
|
20
33
|
if (!res.ok) throw new OutlookAuthError(`Microsoft Graph refused the new sign-in (${String(res.status)}), so nothing was connected.`);
|
|
21
34
|
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
22
|
-
const
|
|
35
|
+
const mail = text(body.mail).toLowerCase();
|
|
36
|
+
const principal = text(body.userPrincipalName).toLowerCase();
|
|
37
|
+
const email = mail || principal;
|
|
23
38
|
if (email === '') throw new OutlookAuthError('Microsoft Graph did not say which mailbox this is, so nothing was connected.');
|
|
24
|
-
|
|
39
|
+
if (wanted !== null && wanted !== mail && wanted !== principal) throw new OutlookAuthError(wrongMailbox(email, wanted));
|
|
40
|
+
return { email, name: text(body.displayName) };
|
|
25
41
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { homedir } from 'node:os';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
2
|
+
import { accountFiles } from '@metro-labs/core/stations/account-files';
|
|
4
3
|
|
|
5
4
|
export interface AccountState {
|
|
6
5
|
accessToken: string;
|
|
@@ -13,12 +12,9 @@ export interface AccountState {
|
|
|
13
12
|
|
|
14
13
|
const SEEN_MAX = 500;
|
|
15
14
|
|
|
16
|
-
const
|
|
15
|
+
export const stateFiles = accountFiles('OUTLOOK_STATE_DIR', 'outlook-state-');
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
const dir = process.env.OUTLOOK_STATE_DIR ?? join(homedir(), '.metro');
|
|
20
|
-
return join(dir, `outlook-state-${safeSegment(accountId)}.json`);
|
|
21
|
-
}
|
|
17
|
+
const statePath =(accountId: string): string => stateFiles.path(accountId);
|
|
22
18
|
|
|
23
19
|
const text = (v: unknown): string => (typeof v === 'string' ? v : '');
|
|
24
20
|
const nullable = (v: unknown): string | null => (typeof v === 'string' && v !== '' ? v : null);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReadFilter, Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { stateFiles } from './state.js';
|
|
2
3
|
|
|
3
4
|
export const outlookStation: Station = {
|
|
4
5
|
name: 'outlook',
|
|
@@ -8,5 +9,7 @@ export const outlookStation: Station = {
|
|
|
8
9
|
readFilters: new Set<ReadFilter>(['account', 'query', 'from', 'until', 'unread_only', 'message_id']),
|
|
9
10
|
approvals: false,
|
|
10
11
|
attachmentMode: 'canonical',
|
|
12
|
+
forget: stateFiles.forget,
|
|
13
|
+
forgetExcept: stateFiles.forgetExcept,
|
|
11
14
|
tools: [],
|
|
12
15
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { accountFiles } from '@metro-labs/core/stations/account-files';
|
|
4
4
|
import type { GroupRef } from './messages.js';
|
|
5
5
|
|
|
6
6
|
export const GROUP_RE = /^([A-Z0-9]{8}|\*[A-Z0-9]{7})-([0-9a-f]{16})$/i;
|
|
@@ -19,8 +19,9 @@ export function parseGroupKey(resource: string): GroupRef | null {
|
|
|
19
19
|
return m === null ? null : { creator: (m[1] ?? '').toUpperCase(), groupId: (m[2] ?? '').toLowerCase() };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
export const groupFiles = accountFiles('THREEMA_GROUPS_DIR', 'threema-groups-');
|
|
23
|
+
|
|
24
|
+
const groupsFile = (accountId: string): string => groupFiles.path(accountId);
|
|
24
25
|
|
|
25
26
|
function readRosters(accountId: string): Map<string, GroupRoster> {
|
|
26
27
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
|
+
import { groupFiles } from './groups.js';
|
|
2
3
|
|
|
3
4
|
export const threemaStation: Station = {
|
|
4
5
|
name: 'threema',
|
|
@@ -6,5 +7,7 @@ export const threemaStation: Station = {
|
|
|
6
7
|
hasTrain: true,
|
|
7
8
|
messageVerbs: new Set<Verb>(['send', 'reply', 'react', 'unreact']),
|
|
8
9
|
attachmentMode: 'canonical',
|
|
10
|
+
forget: groupFiles.forget,
|
|
11
|
+
forgetExcept: groupFiles.forgetExcept,
|
|
9
12
|
tools: [],
|
|
10
13
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Station, Verb } from '@metro-labs/core/stations/types';
|
|
2
2
|
import { PROFILE_FIELDS, type ProfileField } from '@metro-labs/core/stations/profile';
|
|
3
|
+
import { tokenFiles } from './token-store.js';
|
|
3
4
|
|
|
4
5
|
export const whatsappStation: Station = {
|
|
5
6
|
name: 'whatsapp',
|
|
@@ -17,5 +18,7 @@ export const whatsappStation: Station = {
|
|
|
17
18
|
profileFields: new Set<ProfileField>(PROFILE_FIELDS),
|
|
18
19
|
readsProfiles: true,
|
|
19
20
|
resolvesSenders: true,
|
|
21
|
+
forget: tokenFiles.forget,
|
|
22
|
+
forgetExcept: tokenFiles.forgetExcept,
|
|
20
23
|
tools: [],
|
|
21
24
|
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { homedir } from 'node:os';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
1
|
import { readJson, writeSecure } from '@metro-labs/core/secure-fs';
|
|
2
|
+
import { accountFiles } from '@metro-labs/core/stations/account-files';
|
|
4
3
|
import { errMsg } from '@metro-labs/core/log';
|
|
5
4
|
|
|
6
5
|
export const PERSISTED_KEY_TYPES = ['tctoken', 'lid-mapping'] as const;
|
|
@@ -14,12 +13,9 @@ const WRITE_DEBOUNCE_MS = 1000;
|
|
|
14
13
|
export const isPersistedKeyType = (type: string): boolean =>
|
|
15
14
|
PERSISTED.has(type);
|
|
16
15
|
|
|
17
|
-
const
|
|
16
|
+
export const tokenFiles = accountFiles('WHATSAPP_TOKEN_DIR', 'whatsapp-tokens-');
|
|
18
17
|
|
|
19
|
-
export
|
|
20
|
-
const dir = process.env.WHATSAPP_TOKEN_DIR ?? join(homedir(), '.metro');
|
|
21
|
-
return join(dir, `whatsapp-tokens-${safeSegment(accountId)}.json`);
|
|
22
|
-
}
|
|
18
|
+
export const tokenStorePath = (accountId: string): string => tokenFiles.path(accountId);
|
|
23
19
|
|
|
24
20
|
export function persistedSubset(table: KeyTable): KeyTable {
|
|
25
21
|
const out: KeyTable = {};
|
package/runtime/runtime.json
CHANGED