@oyasmi/pipiclaw 0.5.8 → 0.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.
Files changed (59) hide show
  1. package/README.md +39 -3
  2. package/dist/agent/channel-runner.d.ts +5 -0
  3. package/dist/agent/channel-runner.js +59 -15
  4. package/dist/agent/prompt-builder.js +6 -0
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +2 -1
  7. package/dist/memory/consolidation.js +11 -2
  8. package/dist/memory/session.js +2 -2
  9. package/dist/memory/sidecar-worker.d.ts +1 -0
  10. package/dist/memory/sidecar-worker.js +56 -1
  11. package/dist/paths.d.ts +2 -0
  12. package/dist/paths.js +2 -0
  13. package/dist/runtime/bootstrap.d.ts +2 -1
  14. package/dist/runtime/bootstrap.js +74 -23
  15. package/dist/runtime/delivery.js +56 -5
  16. package/dist/runtime/dingtalk.d.ts +2 -0
  17. package/dist/runtime/dingtalk.js +14 -7
  18. package/dist/runtime/events.d.ts +3 -0
  19. package/dist/runtime/events.js +30 -5
  20. package/dist/security/command-guard.js +4 -0
  21. package/dist/security/config.d.ts +6 -0
  22. package/dist/security/config.js +57 -6
  23. package/dist/security/network.d.ts +28 -0
  24. package/dist/security/network.js +246 -0
  25. package/dist/security/path-guard.js +4 -0
  26. package/dist/security/platform.d.ts +1 -0
  27. package/dist/security/platform.js +3 -0
  28. package/dist/security/types.d.ts +16 -1
  29. package/dist/settings.d.ts +4 -1
  30. package/dist/settings.js +31 -6
  31. package/dist/shared/config-diagnostics.d.ts +7 -0
  32. package/dist/shared/config-diagnostics.js +3 -0
  33. package/dist/subagents/discovery.d.ts +1 -1
  34. package/dist/subagents/discovery.js +1 -1
  35. package/dist/subagents/tool.d.ts +2 -0
  36. package/dist/subagents/tool.js +24 -2
  37. package/dist/tools/config.d.ts +37 -0
  38. package/dist/tools/config.js +170 -0
  39. package/dist/tools/index.d.ts +3 -0
  40. package/dist/tools/index.js +23 -1
  41. package/dist/tools/web-fetch.d.ts +17 -0
  42. package/dist/tools/web-fetch.js +29 -0
  43. package/dist/tools/web-search.d.ts +16 -0
  44. package/dist/tools/web-search.js +29 -0
  45. package/dist/web/client.d.ts +41 -0
  46. package/dist/web/client.js +193 -0
  47. package/dist/web/config.d.ts +19 -0
  48. package/dist/web/config.js +35 -0
  49. package/dist/web/extract.d.ts +7 -0
  50. package/dist/web/extract.js +122 -0
  51. package/dist/web/fetch.d.ts +23 -0
  52. package/dist/web/fetch.js +150 -0
  53. package/dist/web/format.d.ts +21 -0
  54. package/dist/web/format.js +38 -0
  55. package/dist/web/search-providers.d.ts +15 -0
  56. package/dist/web/search-providers.js +199 -0
  57. package/dist/web/search.d.ts +19 -0
  58. package/dist/web/search.js +52 -0
  59. package/package.json +9 -2
@@ -1,5 +1,6 @@
1
1
  import * as log from "../log.js";
2
2
  const MIN_UPDATE_INTERVAL_MS = 800;
3
+ const NO_CONTENT = "";
3
4
  class ChannelDeliveryController {
4
5
  constructor(event, bot, store) {
5
6
  this.event = event;
@@ -12,9 +13,12 @@ class ChannelDeliveryController {
12
13
  this.running = false;
13
14
  this.closed = false;
14
15
  this.finalResponseDelivered = false;
16
+ this.cardWarmupScheduled = false;
17
+ this.cardWarmupTriggered = false;
15
18
  this.progressWindowStartedAt = 0;
16
19
  this.lastDeliveredAt = 0;
17
20
  this.timer = null;
21
+ this.cardWarmupTimer = null;
18
22
  this.flushWaiters = [];
19
23
  }
20
24
  buildContext() {
@@ -37,19 +41,57 @@ class ChannelDeliveryController {
37
41
  setTyping: async (_isTyping) => { },
38
42
  setWorking: async (_working) => { },
39
43
  deleteMessage: async () => this.silence(),
44
+ primeCard: (delayMs) => this.primeCard(delayMs),
40
45
  flush: async () => this.flush(),
41
46
  close: async () => this.close(),
42
47
  };
43
48
  }
49
+ primeCard(delayMs) {
50
+ if (this.closed || this.finalResponseDelivered || this.cardWarmupScheduled || this.cardWarmupTriggered) {
51
+ return;
52
+ }
53
+ this.cardWarmupScheduled = true;
54
+ this.cardWarmupTimer = setTimeout(() => {
55
+ this.cardWarmupScheduled = false;
56
+ this.cardWarmupTimer = null;
57
+ void this.triggerCardWarmup();
58
+ }, Math.max(0, delayMs));
59
+ }
60
+ async triggerCardWarmup() {
61
+ if (this.closed || this.finalResponseDelivered || this.desiredRevision > 0) {
62
+ return;
63
+ }
64
+ this.cardWarmupTriggered = true;
65
+ try {
66
+ await this.bot.ensureCard(this.event.channelId);
67
+ }
68
+ catch (err) {
69
+ log.logWarning(`[${this.event.channelId}] Failed to warm AI card`, err instanceof Error ? err.message : String(err));
70
+ this.bot.discardCard(this.event.channelId);
71
+ }
72
+ }
73
+ clearCardWarmup() {
74
+ this.cardWarmupScheduled = false;
75
+ if (this.cardWarmupTimer) {
76
+ clearTimeout(this.cardWarmupTimer);
77
+ this.cardWarmupTimer = null;
78
+ }
79
+ }
80
+ archiveBotResponse(text) {
81
+ void this.store.logBotResponse(this.event.channelId, text, Date.now().toString()).catch((err) => {
82
+ log.logWarning(`[${this.event.channelId}] Failed to archive bot response`, err instanceof Error ? err.message : String(err));
83
+ });
84
+ }
44
85
  async appendProgress(text, shouldLog) {
45
86
  if (this.closed || this.finalResponseDelivered || !text.trim())
46
87
  return;
88
+ this.clearCardWarmup();
47
89
  this.progressText = this.progressText ? `${this.progressText}\n\n${text}` : text;
48
90
  if (this.progressWindowStartedAt === 0) {
49
91
  this.progressWindowStartedAt = Date.now();
50
92
  }
51
93
  if (shouldLog) {
52
- await this.store.logBotResponse(this.event.channelId, text, Date.now().toString());
94
+ this.archiveBotResponse(text);
53
95
  }
54
96
  this.mode = "progress";
55
97
  this.bumpRevision(false);
@@ -57,8 +99,9 @@ class ChannelDeliveryController {
57
99
  async sendFinal(text, shouldLog) {
58
100
  if (this.closed || this.finalResponseDelivered)
59
101
  return this.finalResponseDelivered;
102
+ this.clearCardWarmup();
60
103
  if (shouldLog) {
61
- await this.store.logBotResponse(this.event.channelId, text, Date.now().toString());
104
+ this.archiveBotResponse(text);
62
105
  }
63
106
  const delivered = await this.bot.sendPlain(this.event.channelId, text);
64
107
  if (!delivered) {
@@ -72,6 +115,7 @@ class ChannelDeliveryController {
72
115
  async replaceWithFinal(text) {
73
116
  if (this.closed || this.finalResponseDelivered)
74
117
  return;
118
+ this.clearCardWarmup();
75
119
  this.progressText = text;
76
120
  this.mode = "finalize-with-fallback";
77
121
  this.bumpRevision(true);
@@ -79,6 +123,7 @@ class ChannelDeliveryController {
79
123
  async silence() {
80
124
  if (this.closed)
81
125
  return;
126
+ this.clearCardWarmup();
82
127
  this.finalResponseDelivered = true;
83
128
  this.mode = "silent";
84
129
  this.bumpRevision(true);
@@ -138,8 +183,8 @@ class ChannelDeliveryController {
138
183
  }
139
184
  }
140
185
  else if (mode === "finalize-existing") {
141
- if (content) {
142
- touchedRemote = await this.bot.finalizeExistingCard(this.event.channelId, this.progressText);
186
+ if (content || this.cardWarmupTriggered) {
187
+ touchedRemote = await this.bot.finalizeExistingCard(this.event.channelId, content ? this.progressText : NO_CONTENT);
143
188
  if (!touchedRemote) {
144
189
  this.bot.discardCard(this.event.channelId);
145
190
  }
@@ -160,7 +205,12 @@ class ChannelDeliveryController {
160
205
  }
161
206
  }
162
207
  else if (mode === "silent") {
163
- this.bot.discardCard(this.event.channelId);
208
+ if (this.cardWarmupTriggered) {
209
+ touchedRemote = await this.bot.finalizeExistingCard(this.event.channelId, NO_CONTENT);
210
+ }
211
+ if (!touchedRemote) {
212
+ this.bot.discardCard(this.event.channelId);
213
+ }
164
214
  }
165
215
  }
166
216
  catch (err) {
@@ -209,6 +259,7 @@ class ChannelDeliveryController {
209
259
  return;
210
260
  }
211
261
  this.closed = true;
262
+ this.clearCardWarmup();
212
263
  await this.flush();
213
264
  }
214
265
  }
@@ -34,6 +34,7 @@ export interface DingTalkContext {
34
34
  setTyping: (isTyping: boolean) => Promise<void>;
35
35
  setWorking: (working: boolean) => Promise<void>;
36
36
  deleteMessage: () => Promise<void>;
37
+ primeCard: (delayMs: number) => void;
37
38
  flush: () => Promise<void>;
38
39
  close: () => Promise<void>;
39
40
  }
@@ -61,6 +62,7 @@ export declare class DingTalkBot {
61
62
  private isReconnecting;
62
63
  private isStopped;
63
64
  private reconnectAttempts;
65
+ private hasReportedReady;
64
66
  private processedIds;
65
67
  private processedIdsOrder;
66
68
  constructor(handler: DingTalkHandler, config: DingTalkConfig);
@@ -78,6 +78,7 @@ export class DingTalkBot {
78
78
  this.isReconnecting = false;
79
79
  this.isStopped = false;
80
80
  this.reconnectAttempts = 0;
81
+ this.hasReportedReady = false;
81
82
  // Deduplication cache (Set for O(1) lookup, order array for FIFO eviction)
82
83
  this.processedIds = new Set();
83
84
  this.processedIdsOrder = [];
@@ -171,9 +172,6 @@ export class DingTalkBot {
171
172
  log.logWarning("DingTalk: cardTemplateId not configured — AI Card streaming will not work");
172
173
  }
173
174
  log.logInfo(`DingTalk: initializing stream (clientId=${this.config.clientId.substring(0, 8)}…)`);
174
- if (process.env.DINGTALK_FORCE_PROXY !== "true") {
175
- axios.defaults.proxy = false;
176
- }
177
175
  this.clearAllTimers();
178
176
  this.client = new DWClient({
179
177
  clientId: this.config.clientId,
@@ -183,8 +181,10 @@ export class DingTalkBot {
183
181
  this.client.registerCallbackListener(TOPIC_ROBOT, (msg) => {
184
182
  return this.handleRawMessage(msg);
185
183
  });
186
- log.logConnected();
187
- await this.doReconnect(true); // Initial connection
184
+ const connected = await this.doReconnect(true); // Initial connection
185
+ if (!connected) {
186
+ log.logWarning("DingTalk: initial stream connection not ready yet; retrying in background");
187
+ }
188
188
  }
189
189
  handleRawMessage(msg) {
190
190
  // 1. Immediate ACK
@@ -216,16 +216,17 @@ export class DingTalkBot {
216
216
  }
217
217
  async doReconnect(immediate = false) {
218
218
  if (this.isReconnecting || this.isStopped || !this.client)
219
- return;
219
+ return false;
220
220
  this.isReconnecting = true;
221
221
  let connectionFailed = false;
222
+ let connected = false;
222
223
  if (!immediate && this.reconnectAttempts > 0) {
223
224
  const delay = Math.min(1000 * 2 ** this.reconnectAttempts + Math.random() * 1000, 30000);
224
225
  log.logInfo(`DingTalk: waiting ${Math.round(delay / 1000)}s before reconnecting...`);
225
226
  await this.waitForDelay(delay);
226
227
  if (this.isStopped || !this.client) {
227
228
  this.isReconnecting = false;
228
- return;
229
+ return false;
229
230
  }
230
231
  }
231
232
  try {
@@ -237,6 +238,11 @@ export class DingTalkBot {
237
238
  this.lastSocketAvailableTime = Date.now();
238
239
  this.reconnectAttempts = 0; // Success, reset backoff
239
240
  log.logInfo("DingTalk: connected to stream.");
241
+ if (!this.hasReportedReady) {
242
+ log.logConnected();
243
+ this.hasReportedReady = true;
244
+ }
245
+ connected = true;
240
246
  // Setup keep alive
241
247
  this.clearKeepAliveTimer();
242
248
  this.keepAliveTimer = this.setTrackedInterval(() => {
@@ -294,6 +300,7 @@ export class DingTalkBot {
294
300
  if (connectionFailed && !this.isStopped) {
295
301
  this.scheduleReconnect(0, false);
296
302
  }
303
+ return connected;
297
304
  }
298
305
  async stop() {
299
306
  log.logInfo("DingTalk: stopping bot");
@@ -42,6 +42,9 @@ export declare class EventsWatcher {
42
42
  private handlePeriodic;
43
43
  private execute;
44
44
  private deleteFile;
45
+ private getInvalidMarkerPath;
46
+ private markInvalid;
47
+ private clearInvalidMarker;
45
48
  private sleep;
46
49
  }
47
50
  /**
@@ -1,5 +1,5 @@
1
1
  import { Cron } from "croner";
2
- import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch } from "fs";
2
+ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
3
3
  import { readFile } from "fs/promises";
4
4
  import { join } from "path";
5
5
  import * as log from "../log.js";
@@ -128,10 +128,11 @@ export class EventsWatcher {
128
128
  }
129
129
  if (!event) {
130
130
  log.logWarning(`Failed to parse event file after ${MAX_RETRIES} retries: ${filename}`, lastError?.message);
131
- this.deleteFile(filename);
131
+ this.markInvalid(filename, lastError?.message ?? "Unknown event parse error");
132
132
  return;
133
133
  }
134
134
  this.knownFiles.add(filename);
135
+ this.clearInvalidMarker(filename);
135
136
  switch (event.type) {
136
137
  case "immediate":
137
138
  this.handleImmediate(filename, event);
@@ -196,7 +197,7 @@ export class EventsWatcher {
196
197
  const now = Date.now();
197
198
  if (!Number.isFinite(atTime)) {
198
199
  log.logWarning(`Invalid one-shot time for ${filename}: ${event.at}`);
199
- this.deleteFile(filename);
200
+ this.markInvalid(filename, `Invalid one-shot time: ${event.at}`);
200
201
  return;
201
202
  }
202
203
  if (atTime <= now) {
@@ -207,7 +208,7 @@ export class EventsWatcher {
207
208
  const delay = atTime - now;
208
209
  if (delay > MAX_TIMEOUT_MS) {
209
210
  log.logWarning(`One-shot event exceeds maximum supported delay for ${filename}: ${event.at}. Use a periodic cron event instead.`);
210
- this.deleteFile(filename);
211
+ this.markInvalid(filename, `One-shot event exceeds maximum supported delay: ${event.at}`);
211
212
  return;
212
213
  }
213
214
  log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);
@@ -230,7 +231,7 @@ export class EventsWatcher {
230
231
  }
231
232
  catch (err) {
232
233
  log.logWarning(`Invalid cron schedule for ${filename}: ${event.schedule}`, String(err));
233
- this.deleteFile(filename);
234
+ this.markInvalid(filename, `Invalid cron schedule: ${event.schedule}\n${String(err)}`);
234
235
  }
235
236
  }
236
237
  execute(filename, event, deleteAfter = true) {
@@ -279,8 +280,32 @@ export class EventsWatcher {
279
280
  log.logWarning(`Failed to delete event file: ${filename}`, String(err));
280
281
  }
281
282
  }
283
+ this.clearInvalidMarker(filename);
282
284
  this.knownFiles.delete(filename);
283
285
  }
286
+ getInvalidMarkerPath(filename) {
287
+ return join(this.eventsDir, `${filename}.error.txt`);
288
+ }
289
+ markInvalid(filename, message) {
290
+ try {
291
+ writeFileSync(this.getInvalidMarkerPath(filename), [`timestamp: ${new Date().toISOString()}`, `file: ${filename}`, "", message.trim()].join("\n"), "utf-8");
292
+ }
293
+ catch (err) {
294
+ log.logWarning(`Failed to write event error marker: ${filename}`, String(err));
295
+ }
296
+ this.knownFiles.add(filename);
297
+ }
298
+ clearInvalidMarker(filename) {
299
+ const markerPath = this.getInvalidMarkerPath(filename);
300
+ try {
301
+ unlinkSync(markerPath);
302
+ }
303
+ catch (err) {
304
+ if (err instanceof Error && "code" in err && err.code !== "ENOENT") {
305
+ log.logWarning(`Failed to delete event error marker: ${filename}`, String(err));
306
+ }
307
+ }
308
+ }
284
309
  sleep(ms) {
285
310
  return new Promise((resolve) => setTimeout(resolve, ms));
286
311
  }
@@ -1,4 +1,5 @@
1
1
  import { basename } from "node:path";
2
+ import { isWindowsPlatform } from "./platform.js";
2
3
  const WHITESPACE = /\s+/;
3
4
  function stripNullAndNormalize(text) {
4
5
  return text.replace(/\0/g, "").normalize("NFKC");
@@ -402,6 +403,9 @@ export function guardCommand(command, config) {
402
403
  if (!config.enabled) {
403
404
  return { allowed: true };
404
405
  }
406
+ if (isWindowsPlatform()) {
407
+ return { allowed: true };
408
+ }
405
409
  const atoms = splitCommandChain(command);
406
410
  const normalizedWhole = stripNullAndNormalize(command);
407
411
  for (const allowPattern of config.allowPatterns) {
@@ -1,4 +1,10 @@
1
+ import type { ConfigDiagnostic } from "../shared/config-diagnostics.js";
1
2
  import type { SecurityConfig } from "./types.js";
3
+ export interface LoadedSecurityConfig {
4
+ config: SecurityConfig;
5
+ diagnostics: ConfigDiagnostic[];
6
+ }
2
7
  export declare const DEFAULT_SECURITY_CONFIG: SecurityConfig;
3
8
  export declare function getSecurityConfigPath(appHomeDir?: string): string;
9
+ export declare function loadSecurityConfigWithDiagnostics(appHomeDir?: string): LoadedSecurityConfig;
4
10
  export declare function loadSecurityConfig(appHomeDir?: string): SecurityConfig;
@@ -18,6 +18,12 @@ export const DEFAULT_SECURITY_CONFIG = {
18
18
  writeDeny: [],
19
19
  resolveSymlinks: true,
20
20
  },
21
+ networkGuard: {
22
+ enabled: true,
23
+ allowedCidrs: [],
24
+ allowedHosts: [],
25
+ maxRedirects: 5,
26
+ },
21
27
  audit: {
22
28
  logBlocked: true,
23
29
  },
@@ -28,13 +34,30 @@ function asStringArray(value) {
28
34
  function asOptionalString(value) {
29
35
  return typeof value === "string" && value.trim() ? value : undefined;
30
36
  }
31
- function mergeSecurityConfig(source) {
37
+ function pushInvalidSecurityDiagnostic(diagnostics, configPath, field, message) {
38
+ diagnostics.push({
39
+ source: "security",
40
+ path: configPath,
41
+ severity: "warning",
42
+ message: `${field}: ${message}`,
43
+ });
44
+ }
45
+ function mergeSecurityConfig(source, configPath, diagnostics) {
32
46
  if (!isRecord(source)) {
47
+ pushInvalidSecurityDiagnostic(diagnostics, configPath, "root", "expected a JSON object; using defaults");
33
48
  return DEFAULT_SECURITY_CONFIG;
34
49
  }
35
50
  const commandGuard = isRecord(source.commandGuard) ? source.commandGuard : {};
36
51
  const pathGuard = isRecord(source.pathGuard) ? source.pathGuard : {};
52
+ const networkGuard = isRecord(source.networkGuard) ? source.networkGuard : {};
37
53
  const audit = isRecord(source.audit) ? source.audit : {};
54
+ if (networkGuard.maxRedirects !== undefined) {
55
+ const maxRedirects = networkGuard.maxRedirects;
56
+ const isValidMaxRedirects = typeof maxRedirects === "number" && Number.isFinite(maxRedirects) && maxRedirects > 0;
57
+ if (!isValidMaxRedirects) {
58
+ pushInvalidSecurityDiagnostic(diagnostics, configPath, "networkGuard.maxRedirects", "expected a positive integer; using default");
59
+ }
60
+ }
38
61
  return {
39
62
  enabled: typeof source.enabled === "boolean" ? source.enabled : DEFAULT_SECURITY_CONFIG.enabled,
40
63
  commandGuard: {
@@ -57,6 +80,18 @@ function mergeSecurityConfig(source) {
57
80
  ? pathGuard.resolveSymlinks
58
81
  : DEFAULT_SECURITY_CONFIG.pathGuard.resolveSymlinks,
59
82
  },
83
+ networkGuard: {
84
+ enabled: typeof networkGuard.enabled === "boolean"
85
+ ? networkGuard.enabled
86
+ : DEFAULT_SECURITY_CONFIG.networkGuard.enabled,
87
+ allowedCidrs: asStringArray(networkGuard.allowedCidrs),
88
+ allowedHosts: asStringArray(networkGuard.allowedHosts),
89
+ maxRedirects: typeof networkGuard.maxRedirects === "number" &&
90
+ Number.isFinite(networkGuard.maxRedirects) &&
91
+ networkGuard.maxRedirects > 0
92
+ ? Math.floor(networkGuard.maxRedirects)
93
+ : DEFAULT_SECURITY_CONFIG.networkGuard.maxRedirects,
94
+ },
60
95
  audit: {
61
96
  logBlocked: typeof audit.logBlocked === "boolean" ? audit.logBlocked : DEFAULT_SECURITY_CONFIG.audit.logBlocked,
62
97
  logFile: asOptionalString(audit.logFile),
@@ -66,17 +101,33 @@ function mergeSecurityConfig(source) {
66
101
  export function getSecurityConfigPath(appHomeDir = APP_HOME_DIR) {
67
102
  return join(appHomeDir, "security.json");
68
103
  }
69
- export function loadSecurityConfig(appHomeDir = APP_HOME_DIR) {
104
+ export function loadSecurityConfigWithDiagnostics(appHomeDir = APP_HOME_DIR) {
70
105
  const configPath = getSecurityConfigPath(appHomeDir);
71
106
  if (!existsSync(configPath)) {
72
- return DEFAULT_SECURITY_CONFIG;
107
+ return { config: DEFAULT_SECURITY_CONFIG, diagnostics: [] };
73
108
  }
74
109
  try {
75
110
  const raw = JSON.parse(readFileSync(configPath, "utf-8"));
76
- return mergeSecurityConfig(raw);
111
+ const diagnostics = [];
112
+ return {
113
+ config: mergeSecurityConfig(raw, configPath, diagnostics),
114
+ diagnostics,
115
+ };
77
116
  }
78
117
  catch (error) {
79
- console.warn(`Failed to load security config from ${configPath}: ${error}`);
80
- return DEFAULT_SECURITY_CONFIG;
118
+ return {
119
+ config: DEFAULT_SECURITY_CONFIG,
120
+ diagnostics: [
121
+ {
122
+ source: "security",
123
+ path: configPath,
124
+ severity: "error",
125
+ message: error instanceof Error ? error.message : String(error),
126
+ },
127
+ ],
128
+ };
81
129
  }
82
130
  }
131
+ export function loadSecurityConfig(appHomeDir = APP_HOME_DIR) {
132
+ return loadSecurityConfigWithDiagnostics(appHomeDir).config;
133
+ }
@@ -0,0 +1,28 @@
1
+ import type { SecurityConfig } from "./types.js";
2
+ type ValidationStage = "request" | "redirect";
3
+ export interface NetworkGuardContext {
4
+ config: SecurityConfig;
5
+ }
6
+ export interface ValidatedNetworkTarget {
7
+ url: string;
8
+ hostname: string;
9
+ resolvedAddress?: string;
10
+ }
11
+ export declare class NetworkGuardError extends Error {
12
+ readonly url: string;
13
+ readonly stage: ValidationStage;
14
+ readonly category: string;
15
+ readonly resolvedHost?: string;
16
+ readonly resolvedAddress?: string;
17
+ constructor(options: {
18
+ url: string;
19
+ stage: ValidationStage;
20
+ category: string;
21
+ message: string;
22
+ resolvedHost?: string;
23
+ resolvedAddress?: string;
24
+ });
25
+ }
26
+ export declare function validateNetworkTarget(url: string, context: NetworkGuardContext): Promise<ValidatedNetworkTarget>;
27
+ export declare function validateRedirectTarget(url: string, context: NetworkGuardContext): Promise<ValidatedNetworkTarget>;
28
+ export {};