@stage-labs/metro 0.1.0-beta.175 → 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 +21 -3
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-driver.ts +42 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-interactive.ts +3 -56
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-outlook.ts +63 -0
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +15 -11
- package/runtime/node_modules/@metro-labs/daemon/src/stations/registry.ts +12 -0
- package/runtime/node_modules/@metro-labs/outlook/src/auth.ts +14 -6
- package/runtime/node_modules/@metro-labs/outlook/src/browser.ts +53 -0
- package/runtime/node_modules/@metro-labs/outlook/src/config.ts +5 -0
- package/runtime/node_modules/@metro-labs/outlook/src/login.ts +51 -13
- 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[],
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
isInteractiveStation,
|
|
27
27
|
type InteractiveStation,
|
|
28
28
|
} from '../stations/attach-interactive.js';
|
|
29
|
+
import type { StepInput } from '../stations/attach-driver.js';
|
|
29
30
|
import {
|
|
30
31
|
type AttachOwner,
|
|
31
32
|
type AttachView,
|
|
@@ -41,7 +42,7 @@ export interface AttachSessionApi {
|
|
|
41
42
|
submit: (
|
|
42
43
|
owner: AttachOwner,
|
|
43
44
|
attachId: string,
|
|
44
|
-
input:
|
|
45
|
+
input: StepInput,
|
|
45
46
|
) => Promise<AttachView>;
|
|
46
47
|
cancel: (owner: AttachOwner, attachId: string) => Promise<void>;
|
|
47
48
|
}
|
|
@@ -245,6 +246,18 @@ async function handleDetach(
|
|
|
245
246
|
detached: true,
|
|
246
247
|
activated: await activate(deps, ref.station),
|
|
247
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
|
+
}
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
async function applied(deps: AccountApiDeps): Promise<boolean> {
|
|
@@ -319,9 +332,14 @@ async function handleStep(
|
|
|
319
332
|
attachId: string,
|
|
320
333
|
): Promise<void> {
|
|
321
334
|
const body = await readJsonBody(req);
|
|
335
|
+
const field = (key: string): unknown => bodyField(body, key);
|
|
322
336
|
const view = await deps.attachSessions.submit(owner, attachId, {
|
|
323
|
-
code:
|
|
324
|
-
password:
|
|
337
|
+
code: field('code'),
|
|
338
|
+
password: field('password'),
|
|
339
|
+
state: field('state'),
|
|
340
|
+
mode: field('mode'),
|
|
341
|
+
error: field('error'),
|
|
342
|
+
errorDescription: field('errorDescription'),
|
|
325
343
|
});
|
|
326
344
|
sendJson(req, res, 200, view);
|
|
327
345
|
}
|
|
@@ -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(
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type AttachStep = 'code' | 'password' | 'scan' | 'pair' | 'device' | 'browser';
|
|
2
|
+
|
|
3
|
+
export interface AttachPrompt {
|
|
4
|
+
step: AttachStep;
|
|
5
|
+
prompt: string;
|
|
6
|
+
qr?: string;
|
|
7
|
+
pairingCode?: string;
|
|
8
|
+
userCode?: string;
|
|
9
|
+
verificationUri?: string;
|
|
10
|
+
authorizeUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AttachOutcome {
|
|
14
|
+
config: Record<string, unknown>;
|
|
15
|
+
identity: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DriverHooks {
|
|
19
|
+
prompt: (p: AttachPrompt) => void;
|
|
20
|
+
done: (o: AttachOutcome) => void;
|
|
21
|
+
fail: (message: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface StepInput {
|
|
25
|
+
code?: unknown;
|
|
26
|
+
password?: unknown;
|
|
27
|
+
state?: unknown;
|
|
28
|
+
mode?: unknown;
|
|
29
|
+
error?: unknown;
|
|
30
|
+
errorDescription?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AttachDriver {
|
|
34
|
+
submit: (input: StepInput) => Promise<void>;
|
|
35
|
+
cancel: () => Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface StartedAttach {
|
|
39
|
+
driver: AttachDriver;
|
|
40
|
+
prompt: AttachPrompt;
|
|
41
|
+
expiresAt?: number;
|
|
42
|
+
}
|
|
@@ -1,42 +1,11 @@
|
|
|
1
1
|
import { StationAttachError } from './attach.js';
|
|
2
|
+
import type { DriverHooks, StartedAttach } from './attach-driver.js';
|
|
3
|
+
import { startOutlook } from './attach-outlook.js';
|
|
2
4
|
|
|
3
5
|
export const INTERACTIVE_STATIONS = ['telegram', 'whatsapp', 'outlook'] as const;
|
|
4
6
|
|
|
5
7
|
export type InteractiveStation = (typeof INTERACTIVE_STATIONS)[number];
|
|
6
8
|
|
|
7
|
-
export type AttachStep = 'code' | 'password' | 'scan' | 'pair' | 'device';
|
|
8
|
-
|
|
9
|
-
export interface AttachPrompt {
|
|
10
|
-
step: AttachStep;
|
|
11
|
-
prompt: string;
|
|
12
|
-
qr?: string;
|
|
13
|
-
pairingCode?: string;
|
|
14
|
-
userCode?: string;
|
|
15
|
-
verificationUri?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface AttachOutcome {
|
|
19
|
-
config: Record<string, unknown>;
|
|
20
|
-
identity: Record<string, string>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface DriverHooks {
|
|
24
|
-
prompt: (p: AttachPrompt) => void;
|
|
25
|
-
done: (o: AttachOutcome) => void;
|
|
26
|
-
fail: (message: string) => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface AttachDriver {
|
|
30
|
-
submit: (input: { code?: unknown; password?: unknown }) => Promise<void>;
|
|
31
|
-
cancel: () => Promise<void>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface StartedAttach {
|
|
35
|
-
driver: AttachDriver;
|
|
36
|
-
prompt: AttachPrompt;
|
|
37
|
-
expiresAt?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
9
|
const CODE_PROMPT =
|
|
41
10
|
'Telegram sent a login code to that number. Enter it here.';
|
|
42
11
|
const PASSWORD_PROMPT =
|
|
@@ -153,34 +122,12 @@ async function startWhatsapp(
|
|
|
153
122
|
};
|
|
154
123
|
}
|
|
155
124
|
|
|
156
|
-
const DEVICE_PROMPT =
|
|
157
|
-
'Open the Microsoft sign-in page, type this code, and sign in with the mailbox this agent should read.';
|
|
158
|
-
|
|
159
|
-
async function startOutlook(hooks: DriverHooks): Promise<StartedAttach> {
|
|
160
|
-
const { OutlookLogin, OutlookLoginError } = await import('@metro-labs/outlook/login');
|
|
161
|
-
const login = new OutlookLogin({ onDone: hooks.done, onFailed: hooks.fail });
|
|
162
|
-
const code = await login.start().catch((err: unknown) =>
|
|
163
|
-
refuse(err instanceof OutlookLoginError ? err : null, 'Microsoft refused to start the sign-in'),
|
|
164
|
-
);
|
|
165
|
-
return {
|
|
166
|
-
prompt: { step: 'device', prompt: DEVICE_PROMPT, userCode: code.userCode, verificationUri: code.verificationUri },
|
|
167
|
-
expiresAt: code.expiresAt,
|
|
168
|
-
driver: {
|
|
169
|
-
cancel: () => login.cancel(),
|
|
170
|
-
submit: () =>
|
|
171
|
-
Promise.reject(
|
|
172
|
-
new StationAttachError('this sign-in finishes on the Microsoft page, there is nothing to submit here', 409),
|
|
173
|
-
),
|
|
174
|
-
},
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
125
|
export async function startInteractiveAttach(
|
|
179
126
|
station: InteractiveStation,
|
|
180
127
|
input: Record<string, unknown>,
|
|
181
128
|
hooks: DriverHooks,
|
|
182
129
|
): Promise<StartedAttach> {
|
|
183
|
-
if (station === 'outlook') return startOutlook(hooks);
|
|
130
|
+
if (station === 'outlook') return startOutlook(input, hooks);
|
|
184
131
|
return station === 'telegram'
|
|
185
132
|
? startTelegramUser(input, hooks)
|
|
186
133
|
: startWhatsapp(input, hooks);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { OutlookLogin } from '@metro-labs/outlook/login';
|
|
2
|
+
import { StationAttachError } from './attach.js';
|
|
3
|
+
import type { DriverHooks, StartedAttach, StepInput } from './attach-driver.js';
|
|
4
|
+
|
|
5
|
+
const BROWSER_PROMPT =
|
|
6
|
+
'Sign in with Microsoft in a new tab, with the mailbox this agent should read. This page updates on its own once you are done.';
|
|
7
|
+
const DEVICE_PROMPT =
|
|
8
|
+
'Open the Microsoft sign-in page, type this code, and sign in with the mailbox this agent should read.';
|
|
9
|
+
const OTHER_ATTEMPT = 'This sign-in link belongs to another attempt. Start again from the Channels page.';
|
|
10
|
+
const BROWSER_TTL_MS = 15 * 60_000;
|
|
11
|
+
|
|
12
|
+
const text = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
|
|
13
|
+
|
|
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
|
+
const said = (err: unknown, fallback: string): string =>
|
|
17
|
+
err instanceof OutlookLoginError && err.message !== '' ? err.message : fallback;
|
|
18
|
+
const { browser, mailbox } = (() => {
|
|
19
|
+
try {
|
|
20
|
+
const wanted = parseMailbox(input.mailbox);
|
|
21
|
+
return { browser: new OutlookBrowserLogin({ mailbox: wanted }), mailbox: wanted };
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw new StationAttachError(said(err, 'Microsoft refused to start the sign-in'), 400);
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
let device: OutlookLogin | null = null;
|
|
27
|
+
|
|
28
|
+
const toDevice = async (): Promise<void> => {
|
|
29
|
+
if (device !== null) return;
|
|
30
|
+
const login = new DeviceLogin({ onDone: hooks.done, onFailed: hooks.fail }, { mailbox });
|
|
31
|
+
const code = await login.start().catch((err: unknown) => {
|
|
32
|
+
throw new StationAttachError(said(err, 'Microsoft refused to start the sign-in'), 400);
|
|
33
|
+
});
|
|
34
|
+
device = login;
|
|
35
|
+
hooks.prompt({ step: 'device', prompt: DEVICE_PROMPT, userCode: code.userCode, verificationUri: code.verificationUri });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const finish = async (input: StepInput): Promise<void> => {
|
|
39
|
+
const state = text(input.state);
|
|
40
|
+
if (state !== browser.state) throw new StationAttachError(OTHER_ATTEMPT, 400);
|
|
41
|
+
const error = text(input.error);
|
|
42
|
+
if (error !== '') {
|
|
43
|
+
hooks.fail(failureOf({ error, error_description: text(input.errorDescription) }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const code = text(input.code);
|
|
47
|
+
if (code === '') throw new StationAttachError('Microsoft did not send a sign-in code back', 400);
|
|
48
|
+
try {
|
|
49
|
+
hooks.done(await browser.finish(code, state));
|
|
50
|
+
} catch (err) {
|
|
51
|
+
hooks.fail(said(err, 'Metro could not finish the Microsoft sign-in.'));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
prompt: { step: 'browser', prompt: BROWSER_PROMPT, authorizeUrl: browser.authorizeUrl },
|
|
57
|
+
expiresAt: Date.now() + BROWSER_TTL_MS,
|
|
58
|
+
driver: {
|
|
59
|
+
cancel: () => device?.cancel() ?? Promise.resolve(),
|
|
60
|
+
submit: (input) => (input.mode === 'device' ? toDevice() : finish(input)),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -3,16 +3,16 @@ import { ApiError } from '@metro-labs/http/api-error';
|
|
|
3
3
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
4
4
|
import type { StationName } from '@metro-labs/core/station-names';
|
|
5
5
|
import { ensureStationDeps } from './runtime-deps.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from './attach-
|
|
6
|
+
import { startInteractiveAttach, type InteractiveStation } from './attach-interactive.js';
|
|
7
|
+
import type {
|
|
8
|
+
AttachDriver,
|
|
9
|
+
AttachOutcome,
|
|
10
|
+
AttachPrompt,
|
|
11
|
+
AttachStep,
|
|
12
|
+
DriverHooks,
|
|
13
|
+
StartedAttach,
|
|
14
|
+
StepInput,
|
|
15
|
+
} from './attach-driver.js';
|
|
16
16
|
|
|
17
17
|
export type StartAttach = (
|
|
18
18
|
station: InteractiveStation,
|
|
@@ -42,6 +42,7 @@ export interface AttachView {
|
|
|
42
42
|
pairingCode: string | null;
|
|
43
43
|
userCode: string | null;
|
|
44
44
|
verificationUri: string | null;
|
|
45
|
+
authorizeUrl: string | null;
|
|
45
46
|
accountId: string | null;
|
|
46
47
|
identity: Record<string, string>;
|
|
47
48
|
activated: boolean;
|
|
@@ -86,6 +87,7 @@ function blankView(attachId: string, station: string): AttachView {
|
|
|
86
87
|
pairingCode: null,
|
|
87
88
|
userCode: null,
|
|
88
89
|
verificationUri: null,
|
|
90
|
+
authorizeUrl: null,
|
|
89
91
|
accountId: null,
|
|
90
92
|
identity: {},
|
|
91
93
|
activated: false,
|
|
@@ -138,6 +140,7 @@ export class AttachSessions {
|
|
|
138
140
|
pairingCode: null,
|
|
139
141
|
userCode: null,
|
|
140
142
|
verificationUri: null,
|
|
143
|
+
authorizeUrl: null,
|
|
141
144
|
step: null,
|
|
142
145
|
expiresAt: Date.now() + SETTLED_TTL_MS,
|
|
143
146
|
});
|
|
@@ -187,6 +190,7 @@ export class AttachSessions {
|
|
|
187
190
|
session.view.pairingCode = p.pairingCode ?? null;
|
|
188
191
|
session.view.userCode = p.userCode ?? null;
|
|
189
192
|
session.view.verificationUri = p.verificationUri ?? null;
|
|
193
|
+
session.view.authorizeUrl = p.authorizeUrl ?? null;
|
|
190
194
|
},
|
|
191
195
|
done: (o: AttachOutcome): void => {
|
|
192
196
|
this.finish(attachId, station, o).catch((err: unknown) => {
|
|
@@ -247,7 +251,7 @@ export class AttachSessions {
|
|
|
247
251
|
async submit(
|
|
248
252
|
owner: AttachOwner,
|
|
249
253
|
attachId: string,
|
|
250
|
-
input:
|
|
254
|
+
input: StepInput,
|
|
251
255
|
): Promise<AttachView> {
|
|
252
256
|
const session = this.own(owner, attachId);
|
|
253
257
|
const driver = session.driver;
|
|
@@ -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
|
+
}
|
|
@@ -29,6 +29,7 @@ type Body = Record<string, unknown>;
|
|
|
29
29
|
|
|
30
30
|
const DEVICE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
31
31
|
const CONSENT_CODES = new Set([65001, 90094, 90095]);
|
|
32
|
+
const POLICY_CODES = new Set([530035, 53003]);
|
|
32
33
|
|
|
33
34
|
const text = (v: unknown): string => (typeof v === 'string' ? v : '');
|
|
34
35
|
const seconds = (v: unknown, fallback: number): number =>
|
|
@@ -39,7 +40,7 @@ export function requireClientId(id = clientId()): string {
|
|
|
39
40
|
return id;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
async function postForm(path: string, fields: Record<string, string>, fetchImpl: FetchLike): Promise<{ status: number; body: Body }> {
|
|
43
|
+
export async function postForm(path: string, fields: Record<string, string>, fetchImpl: FetchLike): Promise<{ status: number; body: Body }> {
|
|
43
44
|
let res: Response;
|
|
44
45
|
try {
|
|
45
46
|
res = await fetchImpl(`${loginBase()}/oauth2/v2.0/${path}`, {
|
|
@@ -63,13 +64,20 @@ function codesOf(body: Body): number[] {
|
|
|
63
64
|
const needsConsent = (error: string, codes: number[]): boolean =>
|
|
64
65
|
error === 'consent_required' || codes.some((c) => CONSENT_CODES.has(c));
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
const error = text(body.error);
|
|
68
|
-
const codes = codesOf(body);
|
|
67
|
+
function knownFailure(error: string, codes: number[], id: string): string | null {
|
|
69
68
|
if (needsConsent(error, codes))
|
|
70
|
-
return `An administrator of this Microsoft 365 organization must approve Metro once before it can read this mailbox. They can do it at https://login.microsoftonline.com/
|
|
69
|
+
return `An administrator of this Microsoft 365 organization must approve Metro once before it can read this mailbox. They can do it at https://login.microsoftonline.com/organizations/adminconsent?client_id=${id} and then you can start again.`;
|
|
70
|
+
if (codes.some((c) => POLICY_CODES.has(c)))
|
|
71
|
+
return 'Your company blocks this kind of sign-in. Ask your Microsoft 365 administrator, or use the Microsoft sign-in page instead of a code.';
|
|
71
72
|
if (error === 'expired_token' || codes.includes(70020)) return 'The sign-in code expired before it was used. Start again.';
|
|
72
73
|
if (error === 'access_denied' || codes.includes(65004)) return 'The sign-in was declined, so nothing was connected.';
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function failureOf(body: Body, id = clientId()): string {
|
|
78
|
+
const error = text(body.error);
|
|
79
|
+
const known = knownFailure(error, codesOf(body), id);
|
|
80
|
+
if (known !== null) return known;
|
|
73
81
|
const detail = text(body.error_description).split(/\r?\n/)[0] ?? '';
|
|
74
82
|
return detail === '' ? `Microsoft refused the sign-in (${error === '' ? 'no reason given' : error}).` : `Microsoft refused the sign-in: ${detail}`;
|
|
75
83
|
}
|
|
@@ -93,7 +101,7 @@ export function tenantOf(...jwts: string[]): string | null {
|
|
|
93
101
|
return null;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
function tokensOf(body: Body, now: number, previous?: string): Tokens {
|
|
104
|
+
export function tokensOf(body: Body, now: number, previous?: string): Tokens {
|
|
97
105
|
const accessToken = text(body.access_token);
|
|
98
106
|
const refreshToken = text(body.refresh_token) || (previous ?? '');
|
|
99
107
|
if (accessToken === '' || refreshToken === '') throw new OutlookAuthError('Microsoft answered without the tokens Metro needs.');
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { failureOf, OutlookAuthError, postForm, requireClientId, tenantOf, tokensOf, type FetchLike, type Tokens } from './auth.js';
|
|
3
|
+
import { loginBase, redirectUri, SCOPES } from './config.js';
|
|
4
|
+
|
|
5
|
+
export interface Pkce {
|
|
6
|
+
verifier: string;
|
|
7
|
+
challenge: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function pkcePair(verifier = randomBytes(32).toString('base64url')): Pkce {
|
|
11
|
+
return { verifier, challenge: createHash('sha256').update(verifier).digest('base64url') };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const newState = (): string => randomBytes(24).toString('base64url');
|
|
15
|
+
|
|
16
|
+
export function authorizeUrl(challenge: string, state: string, loginHint: string | null = null): string {
|
|
17
|
+
const query = new URLSearchParams({
|
|
18
|
+
...(loginHint === null ? {} : { login_hint: loginHint }),
|
|
19
|
+
client_id: requireClientId(),
|
|
20
|
+
response_type: 'code',
|
|
21
|
+
redirect_uri: redirectUri(),
|
|
22
|
+
response_mode: 'query',
|
|
23
|
+
scope: SCOPES,
|
|
24
|
+
state,
|
|
25
|
+
code_challenge: challenge,
|
|
26
|
+
code_challenge_method: 'S256',
|
|
27
|
+
prompt: 'select_account',
|
|
28
|
+
});
|
|
29
|
+
return `${loginBase()}/oauth2/v2.0/authorize?${query.toString()}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Redeemed {
|
|
33
|
+
tokens: Tokens;
|
|
34
|
+
tenantId: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function redeemCode(code: string, verifier: string, fetchImpl: FetchLike, now = Date.now()): Promise<Redeemed> {
|
|
38
|
+
const { status, body } = await postForm(
|
|
39
|
+
'token',
|
|
40
|
+
{
|
|
41
|
+
grant_type: 'authorization_code',
|
|
42
|
+
client_id: requireClientId(),
|
|
43
|
+
code,
|
|
44
|
+
code_verifier: verifier,
|
|
45
|
+
redirect_uri: redirectUri(),
|
|
46
|
+
scope: SCOPES,
|
|
47
|
+
},
|
|
48
|
+
fetchImpl,
|
|
49
|
+
);
|
|
50
|
+
if (status !== 200) throw new OutlookAuthError(failureOf(body));
|
|
51
|
+
const tokens = tokensOf(body, now);
|
|
52
|
+
return { tokens, tenantId: tenantOf(typeof body.id_token === 'string' ? body.id_token : '', tokens.accessToken) };
|
|
53
|
+
}
|
|
@@ -27,5 +27,10 @@ const base = (raw: string | undefined, fallback: string): string => {
|
|
|
27
27
|
export const loginBase = (): string =>
|
|
28
28
|
base(process.env.METRO_OUTLOOK_LOGIN_URL, 'https://login.microsoftonline.com/common');
|
|
29
29
|
|
|
30
|
+
export const redirectUri = (): string => {
|
|
31
|
+
const value = trimmed(process.env.METRO_OUTLOOK_REDIRECT);
|
|
32
|
+
return value === '' ? 'https://metro.box/' : value;
|
|
33
|
+
};
|
|
34
|
+
|
|
30
35
|
export const graphBase = (): string =>
|
|
31
36
|
base(process.env.METRO_OUTLOOK_GRAPH_URL, 'https://graph.microsoft.com/v1.0');
|
|
@@ -5,11 +5,16 @@ import {
|
|
|
5
5
|
type DeviceCode,
|
|
6
6
|
type FetchLike,
|
|
7
7
|
type Poll,
|
|
8
|
+
type Tokens,
|
|
8
9
|
} from './auth.js';
|
|
9
|
-
import {
|
|
10
|
+
import { authorizeUrl, newState, pkcePair, redeemCode, type Pkce } from './browser.js';
|
|
11
|
+
import { verifyMailbox, type Mailbox } from './me.js';
|
|
12
|
+
|
|
13
|
+
export { parseMailbox } from './me.js';
|
|
10
14
|
|
|
11
15
|
export { OutlookAuthError as OutlookLoginError } from './auth.js';
|
|
12
16
|
export { NOT_SET_UP } from './config.js';
|
|
17
|
+
export { failureOf } from './auth.js';
|
|
13
18
|
|
|
14
19
|
export interface OutlookLoginResult {
|
|
15
20
|
config: Record<string, unknown>;
|
|
@@ -24,20 +29,63 @@ export interface OutlookLoginEvents {
|
|
|
24
29
|
export interface OutlookLoginDeps {
|
|
25
30
|
fetch?: FetchLike;
|
|
26
31
|
now?: () => number;
|
|
32
|
+
mailbox?: string | null;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
const SLOW_DOWN_MS = 5_000;
|
|
30
36
|
|
|
37
|
+
function resultOf(tokens: Tokens, tenantId: string | null, mailbox: Mailbox, now: number): OutlookLoginResult {
|
|
38
|
+
return {
|
|
39
|
+
config: {
|
|
40
|
+
accountEmail: mailbox.email,
|
|
41
|
+
refreshToken: tokens.refreshToken,
|
|
42
|
+
accessToken: tokens.accessToken,
|
|
43
|
+
expiresAt: tokens.expiresAt,
|
|
44
|
+
tenantId,
|
|
45
|
+
createdAt: new Date(now).toISOString(),
|
|
46
|
+
},
|
|
47
|
+
identity: { email: mailbox.email, ...(mailbox.name === '' ? {} : { name: mailbox.name }) },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class OutlookBrowserLogin {
|
|
52
|
+
readonly state = newState();
|
|
53
|
+
readonly authorizeUrl: string;
|
|
54
|
+
private readonly pkce: Pkce = pkcePair();
|
|
55
|
+
private used = false;
|
|
56
|
+
private readonly fetchImpl: FetchLike;
|
|
57
|
+
private readonly now: () => number;
|
|
58
|
+
private readonly mailbox: string | null;
|
|
59
|
+
|
|
60
|
+
constructor(deps: OutlookLoginDeps = {}) {
|
|
61
|
+
this.fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
62
|
+
this.now = deps.now ?? Date.now;
|
|
63
|
+
this.mailbox = deps.mailbox ?? null;
|
|
64
|
+
this.authorizeUrl = authorizeUrl(this.pkce.challenge, this.state, this.mailbox);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async finish(code: string, state: string): Promise<OutlookLoginResult> {
|
|
68
|
+
if (state !== this.state)
|
|
69
|
+
throw new OutlookAuthError('This sign-in link belongs to another attempt. Start again from the Channels page.');
|
|
70
|
+
if (this.used) throw new OutlookAuthError('This sign-in was already used. Start again from the Channels page.');
|
|
71
|
+
this.used = true;
|
|
72
|
+
const { tokens, tenantId } = await redeemCode(code, this.pkce.verifier, this.fetchImpl, this.now());
|
|
73
|
+
return resultOf(tokens, tenantId, await verifyMailbox(tokens.accessToken, this.fetchImpl, this.mailbox), this.now());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
31
77
|
export class OutlookLogin {
|
|
32
78
|
private code: DeviceCode | null = null;
|
|
33
79
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
34
80
|
private stopped = false;
|
|
35
81
|
private readonly fetchImpl: FetchLike;
|
|
36
82
|
private readonly now: () => number;
|
|
83
|
+
private readonly mailbox: string | null;
|
|
37
84
|
|
|
38
85
|
constructor(private readonly events: OutlookLoginEvents, deps: OutlookLoginDeps = {}) {
|
|
39
86
|
this.fetchImpl = deps.fetch ?? ((input, init) => fetch(input, init));
|
|
40
87
|
this.now = deps.now ?? Date.now;
|
|
88
|
+
this.mailbox = deps.mailbox ?? null;
|
|
41
89
|
}
|
|
42
90
|
|
|
43
91
|
async start(): Promise<DeviceCode> {
|
|
@@ -94,19 +142,9 @@ export class OutlookLogin {
|
|
|
94
142
|
this.fail(poll.message);
|
|
95
143
|
return;
|
|
96
144
|
}
|
|
97
|
-
const mailbox = await verifyMailbox(poll.tokens.accessToken, this.fetchImpl);
|
|
145
|
+
const mailbox = await verifyMailbox(poll.tokens.accessToken, this.fetchImpl, this.mailbox);
|
|
98
146
|
if (this.stopped) return;
|
|
99
147
|
this.stopped = true;
|
|
100
|
-
this.events.onDone(
|
|
101
|
-
config: {
|
|
102
|
-
accountEmail: mailbox.email,
|
|
103
|
-
refreshToken: poll.tokens.refreshToken,
|
|
104
|
-
accessToken: poll.tokens.accessToken,
|
|
105
|
-
expiresAt: poll.tokens.expiresAt,
|
|
106
|
-
tenantId: poll.tenantId,
|
|
107
|
-
createdAt: new Date(this.now()).toISOString(),
|
|
108
|
-
},
|
|
109
|
-
identity: { email: mailbox.email, ...(mailbox.name === '' ? {} : { name: mailbox.name }) },
|
|
110
|
-
});
|
|
148
|
+
this.events.onDone(resultOf(poll.tokens, poll.tenantId, mailbox, this.now()));
|
|
111
149
|
}
|
|
112
150
|
}
|
|
@@ -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