@foxden-app/foxclaw 0.4.0 → 0.4.1

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.
@@ -4,6 +4,7 @@ export interface AuthMirrorRuntime {
4
4
  label?: string;
5
5
  authDir: string;
6
6
  notify?: (message: string) => Promise<void>;
7
+ validate?: (context: AuthMirrorValidationContext) => Promise<AuthMirrorValidationResult | boolean>;
7
8
  }
8
9
  export interface AuthMirrorStatus {
9
10
  candidateName: string;
@@ -11,6 +12,15 @@ export interface AuthMirrorStatus {
11
12
  sourceLabel: string;
12
13
  syncedAt: string;
13
14
  }
15
+ export interface AuthMirrorValidationContext {
16
+ candidateName: string;
17
+ accountId: string;
18
+ lastRefreshMs: number;
19
+ }
20
+ export interface AuthMirrorValidationResult {
21
+ ok: boolean;
22
+ reason?: string | null;
23
+ }
14
24
  export declare class AuthCandidateMirror {
15
25
  private readonly canonicalDir;
16
26
  private readonly runtimes;
@@ -29,6 +39,7 @@ export declare class AuthCandidateMirror {
29
39
  syncRuntimeCandidate(runtimeId: string, candidateName: string): Promise<boolean>;
30
40
  private scan;
31
41
  private propagateValidatedCandidate;
42
+ private validateRuntimeCandidate;
32
43
  private withActivity;
33
44
  private ensureCanonicalDefaultCandidate;
34
45
  private collectCandidateNames;
@@ -96,6 +96,15 @@ export class AuthCandidateMirror {
96
96
  if (record.lastRefreshMs <= previousRefresh) {
97
97
  return false;
98
98
  }
99
+ const validation = await this.validateRuntimeCandidate(runtime, name, record);
100
+ if (!validation.ok) {
101
+ this.logger.warn('auth.mirror.validation_failed', {
102
+ runtimeId: runtime.id,
103
+ name,
104
+ reason: validation.reason ?? 'unknown',
105
+ });
106
+ return false;
107
+ }
99
108
  await atomicWrite(canonicalPath, record.raw);
100
109
  for (const target of this.runtimes) {
101
110
  if (target.id !== runtime.id) {
@@ -116,6 +125,25 @@ export class AuthCandidateMirror {
116
125
  await Promise.allSettled(this.runtimes.map((target) => target.notify?.(message)));
117
126
  return true;
118
127
  }
128
+ async validateRuntimeCandidate(runtime, name, record) {
129
+ if (!runtime.validate) {
130
+ return { ok: true };
131
+ }
132
+ try {
133
+ const result = await runtime.validate({
134
+ candidateName: name,
135
+ accountId: record.accountId,
136
+ lastRefreshMs: record.lastRefreshMs,
137
+ });
138
+ if (typeof result === 'boolean') {
139
+ return { ok: result };
140
+ }
141
+ return { ok: Boolean(result.ok), reason: result.reason ?? null };
142
+ }
143
+ catch (error) {
144
+ return { ok: false, reason: formatError(error) };
145
+ }
146
+ }
119
147
  async withActivity(operation) {
120
148
  this.activeOperations += 1;
121
149
  try {
package/dist/main.js CHANGED
@@ -197,6 +197,7 @@ async function runServeCli() {
197
197
  id: runtime.id,
198
198
  label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
199
199
  authDir: runtime.home,
200
+ validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
200
201
  notify: async (message) => {
201
202
  const chatId = store.getTelegramPrivateChatId(runtime.id);
202
203
  if (chatId) {
@@ -412,6 +413,26 @@ async function runServeCli() {
412
413
  throw error;
413
414
  }
414
415
  }
416
+ async function validateRefreshedAuthCandidate(runtime, candidateName) {
417
+ const authPath = path.join(runtime.home, 'auth.json');
418
+ const candidatePath = path.join(runtime.home, candidateName);
419
+ const [currentTarget, candidateTarget] = await Promise.all([
420
+ fs.promises.realpath(authPath).catch(() => null),
421
+ fs.promises.realpath(candidatePath).catch(() => null),
422
+ ]);
423
+ if (!currentTarget || !candidateTarget || currentTarget !== candidateTarget) {
424
+ return { ok: false, reason: 'candidate is not the current auth target' };
425
+ }
426
+ if (!runtime.app.isConnected()) {
427
+ return { ok: false, reason: 'source app-server is not connected' };
428
+ }
429
+ const account = await runtime.app.readAccount();
430
+ if (!account || account.type !== 'chatgpt' || account.requiresOpenaiAuth) {
431
+ return { ok: false, reason: 'source app-server did not report an active ChatGPT account' };
432
+ }
433
+ await runtime.app.readAccountRateLimits();
434
+ return { ok: true };
435
+ }
415
436
  async function initConfig() {
416
437
  const envPath = process.env.FOXCLAW_ENV?.trim() || DEFAULT_ENV_PATH;
417
438
  fs.mkdirSync(path.dirname(envPath), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",