@wolpertingerlabs/drawlatch 1.0.0-alpha.2 → 1.0.0-alpha.29

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 (119) hide show
  1. package/CONNECTIONS.md +3 -0
  2. package/README.md +395 -465
  3. package/bin/drawlatch.js +1018 -49
  4. package/dist/auth/auth.d.ts +10 -0
  5. package/dist/auth/auth.js +146 -0
  6. package/dist/auth/env-writer.d.ts +6 -0
  7. package/dist/auth/env-writer.js +52 -0
  8. package/dist/auth/password.d.ts +8 -0
  9. package/dist/auth/password.js +28 -0
  10. package/dist/auth/sessions.d.ts +13 -0
  11. package/dist/auth/sessions.js +85 -0
  12. package/dist/cli/generate-keys.d.ts +4 -4
  13. package/dist/cli/generate-keys.js +30 -25
  14. package/dist/connections/{anthropic.json → ai/anthropic.json} +19 -1
  15. package/dist/connections/{devin.json → ai/devin.json} +7 -1
  16. package/dist/connections/{google-ai.json → ai/google-ai.json} +7 -1
  17. package/dist/connections/{openai.json → ai/openai.json} +7 -1
  18. package/dist/connections/{openrouter.json → ai/openrouter.json} +7 -1
  19. package/dist/connections/developer-tools/github.json +138 -0
  20. package/dist/connections/{hex.json → developer-tools/hex.json} +7 -1
  21. package/dist/connections/developer-tools/linear.json +75 -0
  22. package/dist/connections/{lichess.json → gaming/lichess.json} +7 -1
  23. package/dist/connections/messaging/agentmail.json +21 -0
  24. package/dist/connections/messaging/discord-bot.json +114 -0
  25. package/dist/connections/{discord-oauth.json → messaging/discord-oauth.json} +7 -1
  26. package/dist/connections/messaging/slack.json +75 -0
  27. package/dist/connections/messaging/telegram.json +66 -0
  28. package/dist/connections/{google.json → productivity/google.json} +7 -1
  29. package/dist/connections/productivity/notion.json +75 -0
  30. package/dist/connections/productivity/stripe.json +74 -0
  31. package/dist/connections/productivity/trello.json +113 -0
  32. package/dist/connections/{bluesky.json → social-media/bluesky.json} +41 -0
  33. package/dist/connections/social-media/mastodon.json +65 -0
  34. package/dist/connections/social-media/reddit.json +80 -0
  35. package/dist/connections/{twitch.json → social-media/twitch.json} +40 -0
  36. package/dist/connections/social-media/x.json +67 -0
  37. package/dist/mcp/server.js +544 -31
  38. package/dist/remote/admin-mutations.d.ts +37 -0
  39. package/dist/remote/admin-mutations.js +251 -0
  40. package/dist/remote/admin-types.d.ts +149 -0
  41. package/dist/remote/admin-types.js +11 -0
  42. package/dist/remote/admin.d.ts +37 -0
  43. package/dist/remote/admin.js +316 -0
  44. package/dist/remote/caller-bootstrap.d.ts +71 -0
  45. package/dist/remote/caller-bootstrap.js +141 -0
  46. package/dist/remote/ingestors/base-ingestor.d.ts +19 -3
  47. package/dist/remote/ingestors/base-ingestor.js +27 -6
  48. package/dist/remote/ingestors/discord/discord-gateway.d.ts +2 -2
  49. package/dist/remote/ingestors/discord/discord-gateway.js +29 -6
  50. package/dist/remote/ingestors/e2e/setup.d.ts +69 -0
  51. package/dist/remote/ingestors/e2e/setup.js +147 -0
  52. package/dist/remote/ingestors/manager.d.ts +110 -10
  53. package/dist/remote/ingestors/manager.js +449 -42
  54. package/dist/remote/ingestors/poll/poll-ingestor.d.ts +4 -2
  55. package/dist/remote/ingestors/poll/poll-ingestor.js +26 -5
  56. package/dist/remote/ingestors/registry.d.ts +2 -2
  57. package/dist/remote/ingestors/registry.js +2 -2
  58. package/dist/remote/ingestors/slack/socket-mode.d.ts +2 -2
  59. package/dist/remote/ingestors/slack/socket-mode.js +28 -6
  60. package/dist/remote/ingestors/types.d.ts +25 -0
  61. package/dist/remote/ingestors/webhook/base-webhook-ingestor.d.ts +46 -7
  62. package/dist/remote/ingestors/webhook/base-webhook-ingestor.js +115 -10
  63. package/dist/remote/ingestors/webhook/github-webhook-ingestor.d.ts +30 -0
  64. package/dist/remote/ingestors/webhook/github-webhook-ingestor.js +73 -2
  65. package/dist/remote/ingestors/webhook/lifecycle-types.d.ts +63 -0
  66. package/dist/remote/ingestors/webhook/lifecycle-types.js +12 -0
  67. package/dist/remote/ingestors/webhook/stripe-webhook-ingestor.js +2 -2
  68. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.d.ts +17 -5
  69. package/dist/remote/ingestors/webhook/trello-webhook-ingestor.js +32 -26
  70. package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.d.ts +46 -0
  71. package/dist/remote/ingestors/webhook/webhook-lifecycle-manager.js +261 -0
  72. package/dist/remote/server.d.ts +25 -33
  73. package/dist/remote/server.js +681 -195
  74. package/dist/remote/tool-dispatch.d.ts +84 -0
  75. package/dist/remote/tool-dispatch.js +910 -0
  76. package/dist/remote/triggers/rule-engine.d.ts +40 -0
  77. package/dist/remote/triggers/rule-engine.js +228 -0
  78. package/dist/remote/triggers/types.d.ts +69 -0
  79. package/dist/remote/triggers/types.js +10 -0
  80. package/dist/remote/tunnel-state.d.ts +14 -0
  81. package/dist/remote/tunnel-state.js +20 -0
  82. package/dist/remote/tunnel.d.ts +53 -0
  83. package/dist/remote/tunnel.js +149 -0
  84. package/dist/shared/config.d.ts +97 -25
  85. package/dist/shared/config.js +48 -35
  86. package/dist/shared/connections.d.ts +21 -5
  87. package/dist/shared/connections.js +56 -14
  88. package/dist/shared/crypto/index.d.ts +1 -0
  89. package/dist/shared/crypto/index.js +1 -0
  90. package/dist/shared/crypto/key-manager.d.ts +67 -0
  91. package/dist/shared/crypto/key-manager.js +152 -0
  92. package/dist/shared/env-utils.d.ts +42 -0
  93. package/dist/shared/env-utils.js +150 -0
  94. package/dist/shared/listener-config.d.ts +157 -0
  95. package/dist/shared/listener-config.js +10 -0
  96. package/dist/shared/migrations.d.ts +40 -0
  97. package/dist/shared/migrations.js +122 -0
  98. package/dist/shared/protocol/handshake.js +14 -1
  99. package/dist/shared/protocol/index.d.ts +2 -0
  100. package/dist/shared/protocol/index.js +2 -0
  101. package/dist/shared/protocol/sync-client.d.ts +52 -0
  102. package/dist/shared/protocol/sync-client.js +99 -0
  103. package/dist/shared/protocol/sync.d.ts +71 -0
  104. package/dist/shared/protocol/sync.js +176 -0
  105. package/frontend/dist/assets/index-D4k5QKBP.css +1 -0
  106. package/frontend/dist/assets/index-IzwnSOmu.js +267 -0
  107. package/frontend/dist/index.html +15 -0
  108. package/package.json +66 -13
  109. package/dist/connections/discord-bot.json +0 -24
  110. package/dist/connections/github.json +0 -25
  111. package/dist/connections/linear.json +0 -29
  112. package/dist/connections/mastodon.json +0 -25
  113. package/dist/connections/notion.json +0 -33
  114. package/dist/connections/reddit.json +0 -28
  115. package/dist/connections/slack.json +0 -23
  116. package/dist/connections/stripe.json +0 -25
  117. package/dist/connections/telegram.json +0 -26
  118. package/dist/connections/trello.json +0 -25
  119. package/dist/connections/x.json +0 -27
@@ -17,12 +17,24 @@ import { WebhookIngestor } from './base-webhook-ingestor.js';
17
17
  import type { WebhookIngestorConfig } from '../types.js';
18
18
  export declare class TrelloWebhookIngestor extends WebhookIngestor {
19
19
  /**
20
- * The callback URL used when the webhook was registered with Trello.
21
- * Needed for signature verification (Trello signs `body + callbackURL`).
22
- * Resolved from ${VAR} placeholders in the webhook config.
20
+ * Board ID filter for multi-instance support.
21
+ * When set, only webhooks from this specific board are accepted.
22
+ * Set via `_boardId` on the webhook config (injected by IngestorManager).
23
23
  */
24
- private readonly callbackUrl;
25
- constructor(connectionAlias: string, secrets: Record<string, string>, webhookConfig: WebhookIngestorConfig, bufferSize?: number);
24
+ private readonly boardId;
25
+ constructor(connectionAlias: string, secrets: Record<string, string>, webhookConfig: WebhookIngestorConfig, bufferSize?: number, instanceId?: string);
26
+ /**
27
+ * Return the board ID for lifecycle webhook registration.
28
+ * Used by the lifecycle manager to match existing webhooks by model ID
29
+ * and clean up stale registrations.
30
+ */
31
+ protected getModelId(): string | undefined;
32
+ /**
33
+ * Filter webhooks by board ID for multi-instance support.
34
+ * When boardId is set, only events from that specific board are accepted.
35
+ * The board ID is found in `body.model.id` of Trello webhook payloads.
36
+ */
37
+ protected shouldAcceptPayload(body: unknown): boolean;
26
38
  /**
27
39
  * Verify the Trello webhook signature (HMAC-SHA1, base64-encoded).
28
40
  *
@@ -18,32 +18,38 @@ import { WebhookIngestor } from './base-webhook-ingestor.js';
18
18
  import { verifyTrelloSignature, extractTrelloActionType, extractTrelloActionId, TRELLO_SIGNATURE_HEADER, } from './trello-types.js';
19
19
  import { createLogger } from '../../../shared/logger.js';
20
20
  const log = createLogger('webhook');
21
- // ── Placeholder resolution ──────────────────────────────────────────────
22
- /**
23
- * Resolve ${VAR} placeholders in a string using a secrets map.
24
- * Returns the original string if no placeholders are found.
25
- */
26
- function resolvePlaceholder(value, secrets) {
27
- return value.replace(/\$\{(\w+)\}/g, (match, name) => {
28
- if (name in secrets)
29
- return secrets[name];
30
- return match;
31
- });
32
- }
33
21
  // ── Trello Webhook Ingestor ─────────────────────────────────────────────
34
22
  export class TrelloWebhookIngestor extends WebhookIngestor {
35
23
  /**
36
- * The callback URL used when the webhook was registered with Trello.
37
- * Needed for signature verification (Trello signs `body + callbackURL`).
38
- * Resolved from ${VAR} placeholders in the webhook config.
24
+ * Board ID filter for multi-instance support.
25
+ * When set, only webhooks from this specific board are accepted.
26
+ * Set via `_boardId` on the webhook config (injected by IngestorManager).
39
27
  */
40
- callbackUrl;
41
- constructor(connectionAlias, secrets, webhookConfig, bufferSize) {
42
- super(connectionAlias, secrets, webhookConfig, bufferSize);
43
- // Resolve callbackUrl from secrets if it contains ${VAR} placeholders
44
- if (webhookConfig.callbackUrl) {
45
- this.callbackUrl = resolvePlaceholder(webhookConfig.callbackUrl, secrets);
46
- }
28
+ boardId;
29
+ constructor(connectionAlias, secrets, webhookConfig, bufferSize, instanceId) {
30
+ super(connectionAlias, secrets, webhookConfig, bufferSize, instanceId);
31
+ // Board ID filter for multi-instance discrimination
32
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any -- injected by IngestorManager for multi-instance support
33
+ this.boardId = webhookConfig._boardId;
34
+ }
35
+ /**
36
+ * Return the board ID for lifecycle webhook registration.
37
+ * Used by the lifecycle manager to match existing webhooks by model ID
38
+ * and clean up stale registrations.
39
+ */
40
+ getModelId() {
41
+ return this.boardId;
42
+ }
43
+ /**
44
+ * Filter webhooks by board ID for multi-instance support.
45
+ * When boardId is set, only events from that specific board are accepted.
46
+ * The board ID is found in `body.model.id` of Trello webhook payloads.
47
+ */
48
+ shouldAcceptPayload(body) {
49
+ if (!this.boardId)
50
+ return true;
51
+ const payload = body;
52
+ return payload.model?.id === this.boardId;
47
53
  }
48
54
  /**
49
55
  * Verify the Trello webhook signature (HMAC-SHA1, base64-encoded).
@@ -64,7 +70,7 @@ export class TrelloWebhookIngestor extends WebhookIngestor {
64
70
  `in resolved secrets for ${this.connectionAlias}`);
65
71
  return { valid: false, reason: 'Signature secret not configured' };
66
72
  }
67
- if (!this.callbackUrl) {
73
+ if (!this.resolvedCallbackUrl) {
68
74
  log.error(`Callback URL not configured for ${this.connectionAlias}. ` +
69
75
  `Trello signature verification requires the callbackUrl.`);
70
76
  return { valid: false, reason: 'Callback URL not configured' };
@@ -75,7 +81,7 @@ export class TrelloWebhookIngestor extends WebhookIngestor {
75
81
  if (!signatureValue) {
76
82
  return { valid: false, reason: 'Missing signature header' };
77
83
  }
78
- if (!verifyTrelloSignature(rawBody, signatureValue, secret, this.callbackUrl)) {
84
+ if (!verifyTrelloSignature(rawBody, signatureValue, secret, this.resolvedCallbackUrl)) {
79
85
  log.warn(`Signature verification failed for ${this.connectionAlias}`);
80
86
  return { valid: false, reason: 'Signature verification failed' };
81
87
  }
@@ -116,11 +122,11 @@ export class TrelloWebhookIngestor extends WebhookIngestor {
116
122
  }
117
123
  }
118
124
  // ── Self-registration ───────────────────────────────────────────────────
119
- registerIngestorFactory('webhook:trello', (connectionAlias, config, secrets, bufferSize) => {
125
+ registerIngestorFactory('webhook:trello', (connectionAlias, config, secrets, bufferSize, instanceId) => {
120
126
  if (!config.webhook) {
121
127
  log.error(`Missing webhook config for ${connectionAlias}`);
122
128
  return null;
123
129
  }
124
- return new TrelloWebhookIngestor(connectionAlias, secrets, config.webhook, bufferSize);
130
+ return new TrelloWebhookIngestor(connectionAlias, secrets, config.webhook, bufferSize, instanceId);
125
131
  });
126
132
  //# sourceMappingURL=trello-webhook-ingestor.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Webhook Lifecycle Manager.
3
+ *
4
+ * Stateless utility that executes declarative lifecycle HTTP requests to
5
+ * list, register, and unregister webhooks with external services.
6
+ *
7
+ * Used by webhook ingestors with a `lifecycle` config block to auto-register
8
+ * webhooks on start, clean up stale registrations (e.g., after tunnel URL changes),
9
+ * and unregister on permanent shutdown or instance deletion.
10
+ *
11
+ * All HTTP requests use native `fetch()` directly (not proxy routes) since
12
+ * secrets are already resolved and lifecycle URLs may not match the
13
+ * connection's `allowedEndpoints` patterns.
14
+ */
15
+ import type { WebhookLifecycleConfig, WebhookRegistrationState } from './lifecycle-types.js';
16
+ export declare class WebhookLifecycleManager {
17
+ private readonly config;
18
+ private readonly secrets;
19
+ constructor(config: WebhookLifecycleConfig, secrets: Record<string, string>);
20
+ /**
21
+ * Ensure a webhook is registered with the external service (idempotent).
22
+ *
23
+ * Flow:
24
+ * 1. If `list` config exists → fetch existing webhooks, find one matching
25
+ * callbackUrl (+ modelId if provided) → reuse if found
26
+ * 2. Find stale webhooks (matching modelId but wrong callbackUrl) → unregister each
27
+ * 3. If no match → call `register` to create a new webhook
28
+ *
29
+ * Graceful degradation: if `list` fails, attempts direct `register`.
30
+ * All errors are caught and returned in the state object.
31
+ */
32
+ ensureRegistered(callbackUrl: string, modelId?: string): Promise<WebhookRegistrationState>;
33
+ /**
34
+ * Unregister a webhook by ID.
35
+ * Replaces `${_webhookId}` in the unregister URL with the given ID.
36
+ * Errors are logged but never thrown.
37
+ */
38
+ unregister(webhookId: string): Promise<void>;
39
+ /** Fetch the list of existing webhooks from the external service. */
40
+ private listWebhooks;
41
+ /** Register a new webhook with the external service. */
42
+ private register;
43
+ /** Resolve headers with placeholder substitution. */
44
+ private resolveHeaders;
45
+ }
46
+ //# sourceMappingURL=webhook-lifecycle-manager.d.ts.map
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Webhook Lifecycle Manager.
3
+ *
4
+ * Stateless utility that executes declarative lifecycle HTTP requests to
5
+ * list, register, and unregister webhooks with external services.
6
+ *
7
+ * Used by webhook ingestors with a `lifecycle` config block to auto-register
8
+ * webhooks on start, clean up stale registrations (e.g., after tunnel URL changes),
9
+ * and unregister on permanent shutdown or instance deletion.
10
+ *
11
+ * All HTTP requests use native `fetch()` directly (not proxy routes) since
12
+ * secrets are already resolved and lifecycle URLs may not match the
13
+ * connection's `allowedEndpoints` patterns.
14
+ */
15
+ import { resolvePlaceholders } from '../../../shared/config.js';
16
+ import { createLogger } from '../../../shared/logger.js';
17
+ const log = createLogger('webhook-lifecycle');
18
+ // ── Helpers ──────────────────────────────────────────────────────────────
19
+ /**
20
+ * Recursively resolve `${VAR}` placeholders in an object tree.
21
+ * Walks objects and arrays, resolving placeholders in all string values.
22
+ */
23
+ function resolveDeep(value, secrets) {
24
+ if (typeof value === 'string') {
25
+ return resolvePlaceholders(value, secrets);
26
+ }
27
+ if (Array.isArray(value)) {
28
+ return value.map((item) => resolveDeep(item, secrets));
29
+ }
30
+ if (value !== null && typeof value === 'object') {
31
+ const result = {};
32
+ for (const [k, v] of Object.entries(value)) {
33
+ result[k] = resolveDeep(v, secrets);
34
+ }
35
+ return result;
36
+ }
37
+ return value;
38
+ }
39
+ /**
40
+ * Extract a value from a nested object using a dot-separated path.
41
+ * E.g., `getByPath(obj, 'data.webhooks')` → `obj.data.webhooks`.
42
+ */
43
+ function getByPath(obj, path) {
44
+ let current = obj;
45
+ for (const segment of path.split('.')) {
46
+ if (current === null || current === undefined || typeof current !== 'object') {
47
+ return undefined;
48
+ }
49
+ current = current[segment];
50
+ }
51
+ return current;
52
+ }
53
+ // ── Lifecycle Manager ────────────────────────────────────────────────────
54
+ export class WebhookLifecycleManager {
55
+ config;
56
+ secrets;
57
+ constructor(config, secrets) {
58
+ this.config = config;
59
+ this.secrets = secrets;
60
+ }
61
+ /**
62
+ * Ensure a webhook is registered with the external service (idempotent).
63
+ *
64
+ * Flow:
65
+ * 1. If `list` config exists → fetch existing webhooks, find one matching
66
+ * callbackUrl (+ modelId if provided) → reuse if found
67
+ * 2. Find stale webhooks (matching modelId but wrong callbackUrl) → unregister each
68
+ * 3. If no match → call `register` to create a new webhook
69
+ *
70
+ * Graceful degradation: if `list` fails, attempts direct `register`.
71
+ * All errors are caught and returned in the state object.
72
+ */
73
+ async ensureRegistered(callbackUrl, modelId) {
74
+ const now = new Date().toISOString();
75
+ try {
76
+ // Try to discover existing webhooks
77
+ let existingWebhooks = null;
78
+ if (this.config.list) {
79
+ try {
80
+ existingWebhooks = await this.listWebhooks();
81
+ }
82
+ catch (err) {
83
+ log.warn('Failed to list existing webhooks, attempting direct registration:', err);
84
+ // Fall through to register
85
+ }
86
+ }
87
+ if (existingWebhooks !== null && this.config.list) {
88
+ const listConfig = this.config.list;
89
+ // Find a webhook matching our callback URL (and model ID if applicable)
90
+ const matching = existingWebhooks.find((wh) => {
91
+ const rawUrl = getByPath(wh, listConfig.callbackUrlField);
92
+ const whCallbackUrl = typeof rawUrl === 'string' ? rawUrl : '';
93
+ const urlMatch = whCallbackUrl === callbackUrl;
94
+ if (!urlMatch)
95
+ return false;
96
+ if (modelId && listConfig.modelIdField) {
97
+ const rawModelId = getByPath(wh, listConfig.modelIdField);
98
+ return (typeof rawModelId === 'string' ? rawModelId : '') === modelId;
99
+ }
100
+ return true;
101
+ });
102
+ if (matching) {
103
+ const webhookId = String(getByPath(matching, listConfig.idField));
104
+ log.info(`Found existing webhook (ID: ${webhookId}), reusing`);
105
+ return { registered: true, webhookId, lastAttempt: now };
106
+ }
107
+ // Clean up stale webhooks (matching model but wrong callback URL)
108
+ if (modelId && listConfig.modelIdField && this.config.unregister) {
109
+ const stale = existingWebhooks.filter((wh) => {
110
+ const rawModelId = getByPath(wh, listConfig.modelIdField);
111
+ const whModelId = typeof rawModelId === 'string' ? rawModelId : '';
112
+ const rawUrl = getByPath(wh, listConfig.callbackUrlField);
113
+ const whCallbackUrl = typeof rawUrl === 'string' ? rawUrl : '';
114
+ return whModelId === modelId && whCallbackUrl !== callbackUrl;
115
+ });
116
+ for (const staleWh of stale) {
117
+ const staleId = String(getByPath(staleWh, listConfig.idField));
118
+ log.info(`Cleaning up stale webhook (ID: ${staleId})`);
119
+ try {
120
+ await this.unregister(staleId);
121
+ }
122
+ catch (err) {
123
+ log.warn(`Failed to clean up stale webhook ${staleId}:`, err);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ // No existing match — register a new webhook
129
+ if (this.config.register) {
130
+ return await this.register(callbackUrl, modelId);
131
+ }
132
+ // No register config — can't auto-register
133
+ return { registered: false, error: 'No register config defined', lastAttempt: now };
134
+ }
135
+ catch (err) {
136
+ const error = err instanceof Error ? err.message : String(err);
137
+ log.error('Webhook lifecycle error:', error);
138
+ return { registered: false, error, lastAttempt: now };
139
+ }
140
+ }
141
+ /**
142
+ * Unregister a webhook by ID.
143
+ * Replaces `${_webhookId}` in the unregister URL with the given ID.
144
+ * Errors are logged but never thrown.
145
+ */
146
+ async unregister(webhookId) {
147
+ if (!this.config.unregister) {
148
+ log.warn('No unregister config defined, skipping');
149
+ return;
150
+ }
151
+ const mergedSecrets = { ...this.secrets, _webhookId: webhookId };
152
+ const url = resolvePlaceholders(this.config.unregister.url, mergedSecrets);
153
+ const headers = this.resolveHeaders(this.config.unregister.headers, mergedSecrets);
154
+ try {
155
+ const resp = await fetch(url, {
156
+ method: this.config.unregister.method,
157
+ headers,
158
+ });
159
+ if (!resp.ok) {
160
+ const body = await resp.text().catch(() => '');
161
+ log.warn(`Webhook unregister failed (${resp.status}): ${body}`);
162
+ }
163
+ else {
164
+ log.info(`Webhook unregistered (ID: ${webhookId})`);
165
+ }
166
+ }
167
+ catch (err) {
168
+ log.warn(`Webhook unregister request failed for ${webhookId}:`, err);
169
+ }
170
+ }
171
+ // ── Private helpers ────────────────────────────────────────────────────
172
+ /** Fetch the list of existing webhooks from the external service. */
173
+ async listWebhooks() {
174
+ const listConfig = this.config.list;
175
+ const url = resolvePlaceholders(listConfig.url, this.secrets);
176
+ const headers = this.resolveHeaders(listConfig.headers);
177
+ const resp = await fetch(url, {
178
+ method: listConfig.method,
179
+ headers,
180
+ });
181
+ if (!resp.ok) {
182
+ const body = await resp.text().catch(() => '');
183
+ throw new Error(`List webhooks failed (${resp.status}): ${body}`);
184
+ }
185
+ const json = await resp.json();
186
+ // Extract array from response (may be nested under a dot-path)
187
+ let webhooks;
188
+ if (listConfig.responsePath) {
189
+ webhooks = getByPath(json, listConfig.responsePath);
190
+ }
191
+ else {
192
+ webhooks = json;
193
+ }
194
+ if (!Array.isArray(webhooks)) {
195
+ throw new Error(`Expected array from list endpoint${listConfig.responsePath ? ` at path "${listConfig.responsePath}"` : ''}, got ${typeof webhooks}`);
196
+ }
197
+ return webhooks;
198
+ }
199
+ /** Register a new webhook with the external service. */
200
+ async register(callbackUrl, modelId) {
201
+ const now = new Date().toISOString();
202
+ const registerConfig = this.config.register;
203
+ // Merge callbackUrl and modelId into resolution context
204
+ const mergedSecrets = { ...this.secrets };
205
+ if (callbackUrl)
206
+ mergedSecrets.CALLBACK_URL = callbackUrl;
207
+ if (modelId)
208
+ mergedSecrets.MODEL_ID = modelId;
209
+ const url = resolvePlaceholders(registerConfig.url, mergedSecrets);
210
+ const headers = this.resolveHeaders(registerConfig.headers, mergedSecrets);
211
+ const fetchOptions = {
212
+ method: registerConfig.method,
213
+ headers,
214
+ };
215
+ if (registerConfig.body) {
216
+ const resolvedBody = resolveDeep(registerConfig.body, mergedSecrets);
217
+ fetchOptions.body = JSON.stringify(resolvedBody);
218
+ // Ensure Content-Type is set if there's a body
219
+ if (!headers['Content-Type'] && !headers['content-type']) {
220
+ headers['Content-Type'] = 'application/json';
221
+ }
222
+ }
223
+ const resp = await fetch(url, fetchOptions);
224
+ if (!resp.ok) {
225
+ const body = await resp.text().catch(() => '');
226
+ return {
227
+ registered: false,
228
+ error: `Register failed (${resp.status}): ${body}`,
229
+ lastAttempt: now,
230
+ };
231
+ }
232
+ const json = await resp.json();
233
+ const rawWebhookId = json[registerConfig.idField];
234
+ const webhookId = typeof rawWebhookId === 'string'
235
+ ? rawWebhookId
236
+ : typeof rawWebhookId === 'number'
237
+ ? String(rawWebhookId)
238
+ : '';
239
+ if (!webhookId) {
240
+ return {
241
+ registered: false,
242
+ error: `Register response missing "${registerConfig.idField}" field`,
243
+ lastAttempt: now,
244
+ };
245
+ }
246
+ log.info(`Webhook registered (ID: ${webhookId})`);
247
+ return { registered: true, webhookId, lastAttempt: now };
248
+ }
249
+ /** Resolve headers with placeholder substitution. */
250
+ resolveHeaders(headers, secretsOverride) {
251
+ if (!headers)
252
+ return {};
253
+ const secrets = secretsOverride ?? this.secrets;
254
+ const resolved = {};
255
+ for (const [key, value] of Object.entries(headers)) {
256
+ resolved[key] = resolvePlaceholders(value, secrets);
257
+ }
258
+ return resolved;
259
+ }
260
+ }
261
+ //# sourceMappingURL=webhook-lifecycle-manager.js.map
@@ -12,6 +12,7 @@
12
12
  * - Maintains an audit log of all operations
13
13
  * - Rate-limits requests per session
14
14
  */
15
+ import express from 'express';
15
16
  import { type RemoteServerConfig, type ResolvedRoute } from '../shared/config.js';
16
17
  import { EncryptedChannel, type PublicKeyBundle } from '../shared/crypto/index.js';
17
18
  import { HandshakeResponder, type HandshakeInit } from '../shared/protocol/index.js';
@@ -43,13 +44,25 @@ export interface PendingHandshake {
43
44
  init: HandshakeInit;
44
45
  createdAt: number;
45
46
  }
46
- export declare function isEndpointAllowed(url: string, patterns: string[]): boolean;
47
+ /** Sanitized session projection never includes channel keys or resolved
48
+ * routes (which carry decrypted secrets). Returned by getSessionsSnapshot()
49
+ * for the read-only /admin API. */
50
+ export interface SessionSnapshot {
51
+ /** First 12 chars of the session ID — enough to disambiguate, doesn't
52
+ * expose enough material to forge or replay encrypted requests. */
53
+ sessionIdShort: string;
54
+ callerAlias: string;
55
+ createdAt: number;
56
+ lastActivity: number;
57
+ requestCount: number;
58
+ windowRequests: number;
59
+ windowStart: number;
60
+ }
61
+ /** Loopback guard — used by /sync/listen, /sync/status, /events/stream, /admin.
62
+ * Hoisted to module scope so the admin router and its tests can reuse it. */
63
+ export declare function requireLoopback(req: express.Request, res: express.Response, next: express.NextFunction): void;
47
64
  export { resolvePlaceholders } from '../shared/config.js';
48
- /**
49
- * Find the first route whose allowedEndpoints match the given URL.
50
- * Routes with empty allowedEndpoints match nothing.
51
- */
52
- export declare function matchRoute(url: string, routes: ResolvedRoute[]): ResolvedRoute | null;
65
+ export { toolHandlers, executeProxyRequest, isEndpointAllowed, matchRoute, type ToolContext, type ProxyRequestInput, type ProxyRequestResult, type FileAttachment, } from './tool-dispatch.js';
53
66
  export declare function checkRateLimit(session: Pick<Session, 'windowRequests' | 'windowStart'>, limit: number): boolean;
54
67
  export declare const SESSION_TTL: number;
55
68
  export declare const HANDSHAKE_TTL: number;
@@ -57,36 +70,13 @@ export declare function cleanupSessions(sessionsMap: Map<string, Pick<Session, '
57
70
  expiredSessions: string[];
58
71
  expiredHandshakes: string[];
59
72
  };
60
- export interface ProxyRequestInput {
61
- method: string;
62
- url: string;
63
- headers?: Record<string, string>;
64
- body?: unknown;
65
- }
66
- export interface ProxyRequestResult {
67
- status: number;
68
- statusText: string;
69
- headers: Record<string, string>;
70
- body: unknown;
71
- }
72
73
  /**
73
- * Core proxy request execution route matching, secret injection, and fetch.
74
+ * Project the active sessions into a sanitized snapshot for read-only use.
74
75
  *
75
- * Used by:
76
- * - The remote server's `http_request` tool handler (this file)
77
- * - callboard's `LocalProxy` class (in-process, no encryption)
78
- *
79
- * Pure in the sense that it takes routes as input rather than reading global state.
80
- * The only side effect is the outbound fetch().
76
+ * Drops `channel` (holds AES keys) and `resolvedRoutes` (carry decrypted
77
+ * secrets). Used by the loopback /admin API; never call res.json(session).
81
78
  */
82
- export declare function executeProxyRequest(input: ProxyRequestInput, routes: ResolvedRoute[]): Promise<ProxyRequestResult>;
83
- /** Context passed to every tool handler, providing caller identity and shared services. */
84
- export interface ToolContext {
85
- /** The caller alias for the session making this request. */
86
- callerAlias: string;
87
- /** The shared ingestor manager (for poll_events / ingestor_status). */
88
- ingestorManager: IngestorManager;
89
- }
79
+ export declare function getSessionsSnapshot(): SessionSnapshot[];
90
80
  /** Options for creating the app — allows dependency injection for tests */
91
81
  export interface CreateAppOptions {
92
82
  /** Override config instead of loading from disk */
@@ -97,6 +87,8 @@ export interface CreateAppOptions {
97
87
  authorizedPeers?: AuthorizedPeer[];
98
88
  /** Override the ingestor manager instead of creating one from config */
99
89
  ingestorManager?: IngestorManager;
90
+ /** Disable per-IP rate limiting (for tests that make many requests from localhost) */
91
+ disableRateLimiting?: boolean;
100
92
  }
101
93
  export declare function createApp(options?: CreateAppOptions): import("express-serve-static-core").Express;
102
94
  export declare function main(): void;