@llblab/pi-telegram 0.24.6 → 0.24.7

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/lib/sync.ts CHANGED
@@ -110,11 +110,19 @@ export interface TelegramLeaderHealthRuntime {
110
110
  export interface TelegramManualThreadDisconnectDeps<TSyncState> {
111
111
  instanceId: string;
112
112
  getCurrentThreadRecord: () =>
113
- | { target: TelegramTarget; instanceId?: string; owner?: { kind?: string } }
113
+ | {
114
+ target: TelegramTarget;
115
+ instanceId?: string;
116
+ profileKey?: string;
117
+ owner?: { kind?: string };
118
+ }
114
119
  | undefined;
115
120
  topicTargetStore: Pick<
116
121
  TelegramTopicTargetStore,
117
- "markOfflineByInstanceId" | "persist"
122
+ | "markStaleByTarget"
123
+ | "persist"
124
+ | "upsertPendingCleanup"
125
+ | "removePendingCleanup"
118
126
  >;
119
127
  callApi: <TResponse>(
120
128
  method: string,
@@ -175,22 +183,44 @@ export function createTelegramManualThreadDisconnectHandler<
175
183
  }
176
184
  }
177
185
  } else {
178
- const stillOwnsLeaderEpoch = () =>
179
- !deps.getCurrentLeaderEpoch ||
180
- (leaderEpoch !== undefined &&
181
- deps.getCurrentLeaderEpoch() === leaderEpoch);
186
+ const target = currentRecord.target as TelegramTarget & {
187
+ threadId: number;
188
+ };
189
+ const runtimeGeneration = currentRecord.instanceId ?? deps.instanceId;
190
+ const intent: ThreadReconciler.TelegramThreadCleanupIntent = {
191
+ id: `cleanup:${deps.instanceId}:${runtimeGeneration}:${target.chatId}:${target.threadId}`,
192
+ owner: isManualFollower ? "manual-follower" : "leader",
193
+ instanceId: deps.instanceId,
194
+ runtimeGeneration,
195
+ ...(currentRecord.profileKey
196
+ ? { profileKey: currentRecord.profileKey }
197
+ : {}),
198
+ target,
199
+ requestedAtMs: (deps.getNowMs ?? Date.now)(),
200
+ };
201
+ deps.topicTargetStore.upsertPendingCleanup(intent);
202
+ await deps.topicTargetStore.persist();
182
203
  const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
183
- ThreadReconciler.planDisconnectedInstanceThreadCleanup({
184
- target: currentRecord.target as TelegramTarget & {
185
- threadId: number;
186
- },
187
- instanceId: deps.instanceId,
188
- leaderEpoch,
204
+ ThreadReconciler.planThreadReconciliation({
205
+ nowMs: (deps.getNowMs ?? Date.now)(),
206
+ currentLeaderEpoch: leaderEpoch,
207
+ records: [],
208
+ pendingCleanups: [intent],
189
209
  }),
190
210
  {
191
211
  callApi(method, body) {
192
212
  return deps.callApi(method, body);
193
213
  },
214
+ markStaleByTarget(targetToMark, syncStatus, lastSyncError) {
215
+ return deps.topicTargetStore.markStaleByTarget(
216
+ targetToMark,
217
+ syncStatus,
218
+ lastSyncError,
219
+ );
220
+ },
221
+ removeCleanupIntentById(id) {
222
+ return deps.topicTargetStore.removePendingCleanup(id);
223
+ },
194
224
  persist() {
195
225
  return deps.topicTargetStore.persist();
196
226
  },
@@ -203,14 +233,6 @@ export function createTelegramManualThreadDisconnectHandler<
203
233
  "Telegram thread deletion was not confirmed; inspect /telegram-status --debug and retry /telegram-disconnect.",
204
234
  );
205
235
  }
206
- if (!stillOwnsLeaderEpoch()) return deps.stopPolling();
207
- const offlineChanged =
208
- deps.topicTargetStore.markOfflineByInstanceId(deps.instanceId) > 0;
209
- if (!stillOwnsLeaderEpoch()) return deps.stopPolling();
210
- if (offlineChanged) {
211
- await deps.topicTargetStore.persist();
212
- if (!stillOwnsLeaderEpoch()) return deps.stopPolling();
213
- }
214
236
  }
215
237
  const leaderTarget = deps.getLeaderTarget();
216
238
  if (
@@ -48,6 +48,16 @@ export interface TelegramThreadPendingProvision {
48
48
  leaderEpoch?: number | string;
49
49
  }
50
50
 
51
+ export interface TelegramThreadCleanupIntent {
52
+ id: string;
53
+ owner: "leader" | "manual-follower";
54
+ instanceId: string;
55
+ runtimeGeneration: string;
56
+ profileKey?: string;
57
+ target: ThreadTarget;
58
+ requestedAtMs: number;
59
+ }
60
+
51
61
  export interface TelegramUnboundThreadMessageObservation {
52
62
  target: TelegramTarget & { threadId: number };
53
63
  observedAtMs: number;
@@ -130,6 +140,24 @@ export type ThreadReconciliationAction =
130
140
  messageId?: number;
131
141
  leaderEpoch?: number | string;
132
142
  }
143
+ | {
144
+ kind: "close-delete-graceful-shutdown-topic";
145
+ target: TelegramTarget & { threadId: number };
146
+ reason: "graceful-shutdown";
147
+ cleanupIntentId: string;
148
+ instanceId: string;
149
+ runtimeGeneration: string;
150
+ leaderEpoch?: number | string;
151
+ }
152
+ | {
153
+ kind: "cancel-superseded-graceful-shutdown-cleanup";
154
+ target: TelegramTarget & { threadId: number };
155
+ reason: "replacement-registration";
156
+ cleanupIntentId: string;
157
+ instanceId: string;
158
+ runtimeGeneration: string;
159
+ leaderEpoch?: number | string;
160
+ }
133
161
  | {
134
162
  kind: "close-delete-expired-pending-provision-topic";
135
163
  target: TelegramTarget & { threadId: number };
@@ -186,6 +214,7 @@ export interface ThreadReconciliationApplyPorts {
186
214
  ) => boolean;
187
215
  persist?: () => Promise<void>;
188
216
  removePendingProvisionById?: (id: string) => boolean;
217
+ removeCleanupIntentById?: (id: string) => boolean;
189
218
  getCurrentLeaderEpoch?: () => number | string | undefined;
190
219
  recordRuntimeEvent?: (
191
220
  category: string,
@@ -201,6 +230,7 @@ export interface ThreadReconciliationInput {
201
230
  reservations?: readonly ThreadReconciliationReservation[];
202
231
  observations?: readonly ThreadReconciliationObservation[];
203
232
  pendingProvisions?: readonly TelegramThreadPendingProvision[];
233
+ pendingCleanups?: readonly TelegramThreadCleanupIntent[];
204
234
  unboundMessages?: readonly TelegramUnboundThreadMessageObservation[];
205
235
  reservedMessages?: readonly TelegramReservedThreadMessageObservation[];
206
236
  proactiveReservationCleanup?: boolean;
@@ -291,6 +321,8 @@ function isCleanupAction(action: ThreadReconciliationAction): boolean {
291
321
  action.kind === "close-delete-replaced-follower-topic" ||
292
322
  action.kind === "close-delete-previous-leader-topic" ||
293
323
  action.kind === "close-delete-disconnected-instance-topic" ||
324
+ action.kind === "close-delete-graceful-shutdown-topic" ||
325
+ action.kind === "cancel-superseded-graceful-shutdown-cleanup" ||
294
326
  action.kind === "close-delete-expired-pending-provision-topic"
295
327
  );
296
328
  }
@@ -505,6 +537,26 @@ export async function applyThreadReconciliationPlan(
505
537
  shouldPersist;
506
538
  continue;
507
539
  }
540
+ if (action.kind === "cancel-superseded-graceful-shutdown-cleanup") {
541
+ if (shouldSkipForStaleLeaderEpoch(action, ports)) {
542
+ incompleteActions.push(action);
543
+ continue;
544
+ }
545
+ const changed =
546
+ ports.removeCleanupIntentById?.(action.cleanupIntentId) ?? false;
547
+ shouldPersist = changed || shouldPersist;
548
+ ports.recordRuntimeEvent?.(
549
+ "telegram",
550
+ "Cancelled superseded Telegram topic cleanup",
551
+ {
552
+ phase: "thread-reconciler-cleanup-superseded",
553
+ instanceId: action.instanceId,
554
+ chatId: action.target.chatId,
555
+ threadId: action.target.threadId,
556
+ },
557
+ );
558
+ continue;
559
+ }
508
560
  if (action.kind === "close-stale-replaced-topic") {
509
561
  if (shouldSkipForStaleLeaderEpoch(action, ports)) {
510
562
  incompleteActions.push(action);
@@ -558,6 +610,7 @@ export async function applyThreadReconciliationPlan(
558
610
  action.kind === "close-delete-replaced-follower-topic" ||
559
611
  action.kind === "close-delete-previous-leader-topic" ||
560
612
  action.kind === "close-delete-disconnected-instance-topic" ||
613
+ action.kind === "close-delete-graceful-shutdown-topic" ||
561
614
  action.kind === "close-delete-expired-pending-provision-topic"
562
615
  ) {
563
616
  if (shouldSkipForStaleLeaderEpoch(action, ports)) {
@@ -647,7 +700,9 @@ export async function applyThreadReconciliationPlan(
647
700
  ? "Previous leader Telegram topic deleted"
648
701
  : action.kind === "close-delete-disconnected-instance-topic"
649
702
  ? "Disconnected instance Telegram topic deleted"
650
- : "Expired pending provision Telegram topic deleted",
703
+ : action.kind === "close-delete-graceful-shutdown-topic"
704
+ ? "Graceful shutdown Telegram topic deleted"
705
+ : "Expired pending provision Telegram topic deleted",
651
706
  {
652
707
  phase:
653
708
  action.kind === "close-delete-unbound-topic"
@@ -661,7 +716,10 @@ export async function applyThreadReconciliationPlan(
661
716
  : action.kind ===
662
717
  "close-delete-disconnected-instance-topic"
663
718
  ? "thread-reconciler-disconnected-instance-topic-delete"
664
- : "thread-reconciler-expired-pending-provision-topic-delete",
719
+ : action.kind ===
720
+ "close-delete-graceful-shutdown-topic"
721
+ ? "thread-reconciler-graceful-shutdown-topic-delete"
722
+ : "thread-reconciler-expired-pending-provision-topic-delete",
665
723
  chatId: action.target.chatId,
666
724
  threadId: action.target.threadId,
667
725
  ...("messageId" in action ? { messageId: action.messageId } : {}),
@@ -678,6 +736,16 @@ export async function applyThreadReconciliationPlan(
678
736
  if (changed) persistFences.push(action);
679
737
  shouldPersist = changed || shouldPersist;
680
738
  }
739
+ if (
740
+ action.kind === "close-delete-graceful-shutdown-topic" &&
741
+ deleteConfirmed
742
+ ) {
743
+ if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
744
+ const changed =
745
+ ports.removeCleanupIntentById?.(action.cleanupIntentId) ?? false;
746
+ if (changed) persistFences.push(action);
747
+ shouldPersist = changed || shouldPersist;
748
+ }
681
749
  }
682
750
  }
683
751
  if (shouldPersist) {
@@ -736,6 +804,36 @@ export function planThreadReconciliation(
736
804
  );
737
805
 
738
806
  const actions: ThreadReconciliationAction[] = [];
807
+ for (const cleanup of input.pendingCleanups ?? []) {
808
+ const superseded = input.records.some(
809
+ (record) =>
810
+ isCurrentRecord(record) &&
811
+ targetKey(record.target) === targetKey(cleanup.target) &&
812
+ record.instanceId !== cleanup.instanceId,
813
+ );
814
+ const common = {
815
+ target: cleanup.target,
816
+ cleanupIntentId: cleanup.id,
817
+ instanceId: cleanup.instanceId,
818
+ runtimeGeneration: cleanup.runtimeGeneration,
819
+ ...(input.currentLeaderEpoch !== undefined
820
+ ? { leaderEpoch: input.currentLeaderEpoch }
821
+ : {}),
822
+ };
823
+ if (superseded) {
824
+ actions.push({
825
+ ...common,
826
+ kind: "cancel-superseded-graceful-shutdown-cleanup",
827
+ reason: "replacement-registration",
828
+ });
829
+ } else {
830
+ actions.push({
831
+ ...common,
832
+ kind: "close-delete-graceful-shutdown-topic",
833
+ reason: "graceful-shutdown",
834
+ });
835
+ }
836
+ }
739
837
  for (const provision of input.pendingProvisions ?? []) {
740
838
  if (!provision.target) continue;
741
839
  if (!isPendingProvisionExpired(provision, input.nowMs)) continue;
package/lib/threads.ts CHANGED
@@ -92,6 +92,9 @@ export interface TelegramThreadPendingProvision {
92
92
  leaderEpoch?: number | string;
93
93
  }
94
94
 
95
+ export type TelegramThreadCleanupIntent =
96
+ ThreadReconciler.TelegramThreadCleanupIntent;
97
+
95
98
  type TelegramProvisionRecoveryFile = Record<
96
99
  string,
97
100
  {
@@ -160,6 +163,7 @@ export interface TelegramTopicTargetFile {
160
163
  identities?: TelegramThreadIdentityRecord[];
161
164
  reservations?: TelegramThreadReservation[];
162
165
  pendingProvisions?: TelegramThreadPendingProvision[];
166
+ pendingCleanups?: TelegramThreadCleanupIntent[];
163
167
  syncObservations?: TelegramTopicSyncObservation[];
164
168
  }
165
169
 
@@ -229,6 +233,7 @@ export interface TelegramTopicTargetStore {
229
233
  ) => { slot?: string; threadName?: string } | undefined;
230
234
  listReservations: () => TelegramThreadReservation[];
231
235
  listPendingProvisions: () => TelegramThreadPendingProvision[];
236
+ listPendingCleanups: () => TelegramThreadCleanupIntent[];
232
237
  listSyncObservations: () => TelegramTopicSyncObservation[];
233
238
  reserveThread: (reservation: TelegramThreadReservation) => void;
234
239
  upsertPendingProvision: (provision: TelegramThreadPendingProvision) => void;
@@ -237,6 +242,8 @@ export interface TelegramTopicTargetStore {
237
242
  target: TelegramTarget & { threadId: number },
238
243
  ) => Promise<boolean>;
239
244
  removePendingProvision: (id: string) => boolean;
245
+ upsertPendingCleanup: (intent: TelegramThreadCleanupIntent) => void;
246
+ removePendingCleanup: (id: string) => boolean;
240
247
  getBotState: () => TelegramBotStateSnapshot;
241
248
  setBotState: (state: Partial<TelegramBotStateSnapshot>) => void;
242
249
  setStatusSnapshot: (snapshot: {
@@ -876,6 +883,56 @@ function normalizePendingProvision(
876
883
  };
877
884
  }
878
885
 
886
+ function normalizePendingCleanup(
887
+ value: unknown,
888
+ ): TelegramThreadCleanupIntent | undefined {
889
+ if (!value || typeof value !== "object" || Array.isArray(value))
890
+ return undefined;
891
+ const record = value as Record<string, unknown>;
892
+ const targetValue = record.target;
893
+ if (
894
+ !targetValue ||
895
+ typeof targetValue !== "object" ||
896
+ Array.isArray(targetValue)
897
+ ) {
898
+ return undefined;
899
+ }
900
+ const targetRecord = targetValue as Record<string, unknown>;
901
+ const owner = record.owner;
902
+ if (owner !== "leader" && owner !== "manual-follower") return undefined;
903
+ if (typeof record.id !== "string" || record.id.length === 0) return undefined;
904
+ if (typeof record.instanceId !== "string" || record.instanceId.length === 0)
905
+ return undefined;
906
+ if (
907
+ typeof record.runtimeGeneration !== "string" ||
908
+ record.runtimeGeneration.length === 0
909
+ ) {
910
+ return undefined;
911
+ }
912
+ if (
913
+ typeof targetRecord.chatId !== "number" ||
914
+ typeof targetRecord.threadId !== "number" ||
915
+ !Number.isInteger(targetRecord.threadId) ||
916
+ typeof record.requestedAtMs !== "number"
917
+ ) {
918
+ return undefined;
919
+ }
920
+ return {
921
+ id: record.id,
922
+ owner,
923
+ instanceId: record.instanceId,
924
+ runtimeGeneration: record.runtimeGeneration,
925
+ ...(typeof record.profileKey === "string"
926
+ ? { profileKey: record.profileKey }
927
+ : {}),
928
+ target: {
929
+ chatId: targetRecord.chatId,
930
+ threadId: targetRecord.threadId,
931
+ },
932
+ requestedAtMs: record.requestedAtMs,
933
+ };
934
+ }
935
+
879
936
  function normalizeReservation(
880
937
  value: unknown,
881
938
  ): TelegramThreadReservation | undefined {
@@ -970,6 +1027,12 @@ function parseTopicTargetFile(value: unknown): TelegramTopicTargetFile {
970
1027
  return normalized ? [normalized] : [];
971
1028
  })
972
1029
  : [],
1030
+ pendingCleanups: Array.isArray(file.pendingCleanups)
1031
+ ? file.pendingCleanups.flatMap((intent) => {
1032
+ const normalized = normalizePendingCleanup(intent);
1033
+ return normalized ? [normalized] : [];
1034
+ })
1035
+ : [],
973
1036
  syncObservations: Array.isArray(file.syncObservations)
974
1037
  ? file.syncObservations.flatMap((observation) => {
975
1038
  const normalized = normalizeSyncObservation(observation);
@@ -1084,6 +1147,7 @@ export function createTelegramTopicTargetStore(
1084
1147
  let identities = new Map<string, TelegramThreadIdentityRecord>();
1085
1148
  let reservations: TelegramThreadReservation[] = [];
1086
1149
  let pendingProvisions: TelegramThreadPendingProvision[] = [];
1150
+ let pendingCleanups: TelegramThreadCleanupIntent[] = [];
1087
1151
  let syncObservations: TelegramTopicSyncObservation[] = [];
1088
1152
  let followerRecoveryHints = new Map<
1089
1153
  string,
@@ -1140,6 +1204,7 @@ export function createTelegramTopicTargetStore(
1140
1204
  identities = new Map();
1141
1205
  reservations = [];
1142
1206
  pendingProvisions = [];
1207
+ pendingCleanups = [];
1143
1208
  syncObservations = [];
1144
1209
  followerRecoveryHints = new Map();
1145
1210
  statusSnapshot = {};
@@ -1157,6 +1222,7 @@ export function createTelegramTopicTargetStore(
1157
1222
  identities = new Map();
1158
1223
  reservations = [];
1159
1224
  pendingProvisions = [];
1225
+ pendingCleanups = [];
1160
1226
  syncObservations = [];
1161
1227
  followerRecoveryHints = new Map();
1162
1228
  loaded = true;
@@ -1207,6 +1273,10 @@ export function createTelegramTopicTargetStore(
1207
1273
  : {}),
1208
1274
  };
1209
1275
  });
1276
+ pendingCleanups = (file.pendingCleanups ?? []).map((intent) => ({
1277
+ ...intent,
1278
+ target: { ...intent.target },
1279
+ }));
1210
1280
  syncObservations = (file.syncObservations ?? []).map((observation) => ({
1211
1281
  ...observation,
1212
1282
  target: { ...observation.target },
@@ -1269,6 +1339,10 @@ export function createTelegramTopicTargetStore(
1269
1339
  ...provision,
1270
1340
  ...(provision.target ? { target: { ...provision.target } } : {}),
1271
1341
  })),
1342
+ pendingCleanups: pendingCleanups.map((intent) => ({
1343
+ ...intent,
1344
+ target: { ...intent.target },
1345
+ })),
1272
1346
  syncObservations: syncObservations.map((observation) => ({
1273
1347
  ...observation,
1274
1348
  target: { ...observation.target },
@@ -1355,6 +1429,12 @@ export function createTelegramTopicTargetStore(
1355
1429
  ...(provision.target ? { target: { ...provision.target } } : {}),
1356
1430
  }));
1357
1431
  },
1432
+ listPendingCleanups() {
1433
+ return pendingCleanups.map((intent) => ({
1434
+ ...intent,
1435
+ target: { ...intent.target },
1436
+ }));
1437
+ },
1358
1438
  listSyncObservations() {
1359
1439
  return syncObservations.map((observation) => ({
1360
1440
  ...observation,
@@ -1425,6 +1505,21 @@ export function createTelegramTopicTargetStore(
1425
1505
  if (changed) markDirty();
1426
1506
  return changed;
1427
1507
  },
1508
+ upsertPendingCleanup(intent) {
1509
+ const next = { ...intent, target: { ...intent.target } };
1510
+ pendingCleanups = pendingCleanups.filter(
1511
+ (existing) => existing.id !== next.id,
1512
+ );
1513
+ pendingCleanups.push(next);
1514
+ markDirty();
1515
+ },
1516
+ removePendingCleanup(id) {
1517
+ const before = pendingCleanups.length;
1518
+ pendingCleanups = pendingCleanups.filter((intent) => intent.id !== id);
1519
+ const changed = pendingCleanups.length !== before;
1520
+ if (changed) markDirty();
1521
+ return changed;
1522
+ },
1428
1523
  getBotState() {
1429
1524
  return Object.fromEntries(
1430
1525
  Object.entries(botState).filter(([, value]) => value !== undefined),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.24.6",
3
+ "version": "0.24.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -30,7 +30,8 @@
30
30
  "test": "node --experimental-strip-types --test --test-reporter=dot tests/*.test.ts",
31
31
  "test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
32
32
  "typecheck": "tsc --noEmit",
33
- "audit": "node --experimental-strip-types scripts/audit-dependencies.ts",
33
+ "audit": "npm audit --omit=peer",
34
+ "audit:host": "npm audit",
34
35
  "pack:check": "npm pack --dry-run",
35
36
  "validate": "npm run typecheck && npm test && npm run audit && npm run pack:check"
36
37
  },
@@ -74,29 +75,5 @@
74
75
  "devDependencies": {
75
76
  "@types/node": "latest",
76
77
  "typescript": "latest"
77
- },
78
- "overrides": {
79
- "brace-expansion": "5.0.7",
80
- "protobufjs": "7.6.5",
81
- "undici": "8.5.0",
82
- "ws": "8.21.0",
83
- "@earendil-works/pi-coding-agent": {
84
- "brace-expansion": "5.0.7",
85
- "protobufjs": "7.6.5",
86
- "undici": "8.5.0",
87
- "ws": "8.21.0",
88
- "@google/genai": {
89
- "protobufjs": "7.6.5",
90
- "ws": "8.21.0"
91
- },
92
- "@earendil-works/pi-ai": {
93
- "protobufjs": "7.6.5",
94
- "ws": "8.21.0",
95
- "@google/genai": {
96
- "protobufjs": "7.6.5",
97
- "ws": "8.21.0"
98
- }
99
- }
100
- }
101
78
  }
102
79
  }
@@ -1,78 +0,0 @@
1
- /**
2
- * Dependency audit command adapter
3
- * Runs raw npm audit, prints its output, and applies the fail-closed repository policy
4
- */
5
-
6
- import { spawnSync } from "node:child_process";
7
- import { readFileSync } from "node:fs";
8
- import path from "node:path";
9
-
10
- import {
11
- evaluateDependencyAudit,
12
- type AuditReport,
13
- } from "./dependency-audit-policy.ts";
14
-
15
- function readInstalledPackageVersion(root: string, nodePath: string): string {
16
- if (
17
- path.isAbsolute(nodePath) ||
18
- nodePath.includes("..") ||
19
- !nodePath.startsWith("node_modules/")
20
- ) {
21
- throw new Error(`unsafe installed package path: ${nodePath}`);
22
- }
23
- const packageJsonPath = path.join(root, nodePath, "package.json");
24
- const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
25
- version?: unknown;
26
- };
27
- if (typeof parsed.version !== "string") {
28
- throw new Error(`installed package has no valid version: ${nodePath}`);
29
- }
30
- return parsed.version;
31
- }
32
-
33
- function run(): void {
34
- const result = spawnSync("npm", ["audit", "--json"], {
35
- cwd: process.cwd(),
36
- encoding: "utf8",
37
- stdio: ["ignore", "pipe", "pipe"],
38
- });
39
- if (result.stdout) process.stdout.write(result.stdout);
40
- if (result.stderr) process.stderr.write(result.stderr);
41
- if (result.error) throw result.error;
42
- if (result.signal || (result.status !== 0 && result.status !== 1)) {
43
- throw new Error(
44
- `npm audit command failed: status=${String(result.status)} signal=${String(result.signal)}`,
45
- );
46
- }
47
-
48
- let report: AuditReport;
49
- try {
50
- report = JSON.parse(result.stdout) as AuditReport;
51
- } catch (error) {
52
- throw new Error(`could not parse npm audit JSON: ${String(error)}`);
53
- }
54
- const evaluation = evaluateDependencyAudit(
55
- report,
56
- (nodePath) => readInstalledPackageVersion(process.cwd(), nodePath),
57
- );
58
- const expectedStatus = evaluation.vulnerabilityCount === 0 ? 0 : 1;
59
- if (result.status !== expectedStatus) {
60
- throw new Error(
61
- `npm audit exit status mismatch: expected ${expectedStatus}, got ${String(result.status)}`,
62
- );
63
- }
64
- if (evaluation.vulnerabilityCount === 0) {
65
- console.log("Dependency audit passed with zero vulnerabilities.");
66
- return;
67
- }
68
- console.warn(
69
- `Accepted ${evaluation.vulnerabilityCount} audit graph entries rooted only in approved sources ${evaluation.acceptedAdvisorySources.join(", ")}; exception expires after 2026-08-21 UTC.`,
70
- );
71
- }
72
-
73
- try {
74
- run();
75
- } catch (error) {
76
- console.error(error instanceof Error ? error.message : String(error));
77
- process.exitCode = 1;
78
- }