@vellumai/assistant 0.3.14 → 0.3.16
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/ARCHITECTURE.md +142 -0
- package/Dockerfile +2 -2
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +8 -8
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-executor.ts +1 -1
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +6 -7
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +4 -7
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +5 -4
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +82 -20
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +2 -2
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared business logic for ingress member and invite management.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the IPC handlers in daemon/handlers/config-inbox.ts so that
|
|
5
|
+
* both the HTTP routes and the IPC handlers call the same logic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createInvite,
|
|
10
|
+
type IngressInvite,
|
|
11
|
+
type InviteStatus,
|
|
12
|
+
listInvites,
|
|
13
|
+
redeemInvite,
|
|
14
|
+
revokeInvite,
|
|
15
|
+
} from '../memory/ingress-invite-store.js';
|
|
16
|
+
import {
|
|
17
|
+
blockMember,
|
|
18
|
+
type IngressMember,
|
|
19
|
+
listMembers,
|
|
20
|
+
type MemberPolicy,
|
|
21
|
+
type MemberStatus,
|
|
22
|
+
revokeMember,
|
|
23
|
+
upsertMember,
|
|
24
|
+
} from '../memory/ingress-member-store.js';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Response shapes — used by both HTTP routes and IPC handlers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export interface InviteResponseData {
|
|
31
|
+
id: string;
|
|
32
|
+
sourceChannel: string;
|
|
33
|
+
token?: string;
|
|
34
|
+
tokenHash: string;
|
|
35
|
+
maxUses: number;
|
|
36
|
+
useCount: number;
|
|
37
|
+
expiresAt: number | null;
|
|
38
|
+
status: string;
|
|
39
|
+
note?: string;
|
|
40
|
+
createdAt: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MemberResponseData {
|
|
44
|
+
id: string;
|
|
45
|
+
sourceChannel: string;
|
|
46
|
+
externalUserId?: string;
|
|
47
|
+
externalChatId?: string;
|
|
48
|
+
displayName?: string;
|
|
49
|
+
username?: string;
|
|
50
|
+
status: string;
|
|
51
|
+
policy: string;
|
|
52
|
+
lastSeenAt?: number;
|
|
53
|
+
createdAt: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Mappers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function inviteToResponse(inv: IngressInvite, rawToken?: string): InviteResponseData {
|
|
61
|
+
return {
|
|
62
|
+
id: inv.id,
|
|
63
|
+
sourceChannel: inv.sourceChannel,
|
|
64
|
+
...(rawToken ? { token: rawToken } : {}),
|
|
65
|
+
tokenHash: inv.tokenHash,
|
|
66
|
+
maxUses: inv.maxUses,
|
|
67
|
+
useCount: inv.useCount,
|
|
68
|
+
expiresAt: inv.expiresAt,
|
|
69
|
+
status: inv.status,
|
|
70
|
+
note: inv.note ?? undefined,
|
|
71
|
+
createdAt: inv.createdAt,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function memberToResponse(m: IngressMember): MemberResponseData {
|
|
76
|
+
return {
|
|
77
|
+
id: m.id,
|
|
78
|
+
sourceChannel: m.sourceChannel,
|
|
79
|
+
externalUserId: m.externalUserId ?? undefined,
|
|
80
|
+
externalChatId: m.externalChatId ?? undefined,
|
|
81
|
+
displayName: m.displayName ?? undefined,
|
|
82
|
+
username: m.username ?? undefined,
|
|
83
|
+
status: m.status,
|
|
84
|
+
policy: m.policy,
|
|
85
|
+
lastSeenAt: m.lastSeenAt ?? undefined,
|
|
86
|
+
createdAt: m.createdAt,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Result types
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export type IngressResult<T> =
|
|
95
|
+
| { ok: true; data: T }
|
|
96
|
+
| { ok: false; error: string };
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Invite operations
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export function createIngressInvite(params: {
|
|
103
|
+
sourceChannel?: string;
|
|
104
|
+
note?: string;
|
|
105
|
+
maxUses?: number;
|
|
106
|
+
expiresInMs?: number;
|
|
107
|
+
}): IngressResult<InviteResponseData> {
|
|
108
|
+
if (!params.sourceChannel) {
|
|
109
|
+
return { ok: false, error: 'sourceChannel is required for create' };
|
|
110
|
+
}
|
|
111
|
+
const { invite, rawToken } = createInvite({
|
|
112
|
+
sourceChannel: params.sourceChannel,
|
|
113
|
+
note: params.note,
|
|
114
|
+
maxUses: params.maxUses,
|
|
115
|
+
expiresInMs: params.expiresInMs,
|
|
116
|
+
});
|
|
117
|
+
return { ok: true, data: inviteToResponse(invite, rawToken) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function listIngressInvites(params: {
|
|
121
|
+
sourceChannel?: string;
|
|
122
|
+
status?: string;
|
|
123
|
+
}): IngressResult<InviteResponseData[]> {
|
|
124
|
+
const invites = listInvites({
|
|
125
|
+
sourceChannel: params.sourceChannel,
|
|
126
|
+
status: params.status as InviteStatus | undefined,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
data: invites.map((inv) => inviteToResponse(inv)),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function revokeIngressInvite(inviteId?: string): IngressResult<InviteResponseData> {
|
|
135
|
+
if (!inviteId) {
|
|
136
|
+
return { ok: false, error: 'inviteId is required for revoke' };
|
|
137
|
+
}
|
|
138
|
+
const revoked = revokeInvite(inviteId);
|
|
139
|
+
if (!revoked) {
|
|
140
|
+
return { ok: false, error: 'Invite not found or already revoked' };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true, data: inviteToResponse(revoked) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function redeemIngressInvite(params: {
|
|
146
|
+
token?: string;
|
|
147
|
+
externalUserId?: string;
|
|
148
|
+
externalChatId?: string;
|
|
149
|
+
sourceChannel?: string;
|
|
150
|
+
}): IngressResult<InviteResponseData> {
|
|
151
|
+
if (!params.token) {
|
|
152
|
+
return { ok: false, error: 'token is required for redeem' };
|
|
153
|
+
}
|
|
154
|
+
const result = redeemInvite({
|
|
155
|
+
rawToken: params.token,
|
|
156
|
+
externalUserId: params.externalUserId,
|
|
157
|
+
externalChatId: params.externalChatId,
|
|
158
|
+
sourceChannel: params.sourceChannel,
|
|
159
|
+
});
|
|
160
|
+
if ('error' in result) {
|
|
161
|
+
return { ok: false, error: result.error };
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, data: inviteToResponse(result.invite) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Member operations
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export function listIngressMembers(params: {
|
|
171
|
+
assistantId?: string;
|
|
172
|
+
sourceChannel?: string;
|
|
173
|
+
status?: string;
|
|
174
|
+
policy?: string;
|
|
175
|
+
}): IngressResult<MemberResponseData[]> {
|
|
176
|
+
const members = listMembers({
|
|
177
|
+
assistantId: params.assistantId,
|
|
178
|
+
sourceChannel: params.sourceChannel,
|
|
179
|
+
status: params.status as MemberStatus | undefined,
|
|
180
|
+
policy: params.policy as MemberPolicy | undefined,
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
data: members.map(memberToResponse),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function upsertIngressMember(params: {
|
|
189
|
+
sourceChannel?: string;
|
|
190
|
+
externalUserId?: string;
|
|
191
|
+
externalChatId?: string;
|
|
192
|
+
displayName?: string;
|
|
193
|
+
username?: string;
|
|
194
|
+
policy?: string;
|
|
195
|
+
status?: string;
|
|
196
|
+
assistantId?: string;
|
|
197
|
+
}): IngressResult<MemberResponseData> {
|
|
198
|
+
if (!params.sourceChannel) {
|
|
199
|
+
return { ok: false, error: 'sourceChannel is required for upsert' };
|
|
200
|
+
}
|
|
201
|
+
if (!params.externalUserId && !params.externalChatId) {
|
|
202
|
+
return { ok: false, error: 'At least one of externalUserId or externalChatId is required for upsert' };
|
|
203
|
+
}
|
|
204
|
+
const member = upsertMember({
|
|
205
|
+
assistantId: params.assistantId,
|
|
206
|
+
sourceChannel: params.sourceChannel,
|
|
207
|
+
externalUserId: params.externalUserId,
|
|
208
|
+
externalChatId: params.externalChatId,
|
|
209
|
+
displayName: params.displayName,
|
|
210
|
+
username: params.username,
|
|
211
|
+
policy: params.policy as MemberPolicy | undefined,
|
|
212
|
+
status: params.status as MemberStatus | undefined,
|
|
213
|
+
});
|
|
214
|
+
return { ok: true, data: memberToResponse(member) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function revokeIngressMember(memberId?: string, reason?: string): IngressResult<MemberResponseData> {
|
|
218
|
+
if (!memberId) {
|
|
219
|
+
return { ok: false, error: 'memberId is required for revoke' };
|
|
220
|
+
}
|
|
221
|
+
const revoked = revokeMember(memberId, reason);
|
|
222
|
+
if (!revoked) {
|
|
223
|
+
return { ok: false, error: 'Member not found or cannot be revoked' };
|
|
224
|
+
}
|
|
225
|
+
return { ok: true, data: memberToResponse(revoked) };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function blockIngressMember(memberId?: string, reason?: string): IngressResult<MemberResponseData> {
|
|
229
|
+
if (!memberId) {
|
|
230
|
+
return { ok: false, error: 'memberId is required for block' };
|
|
231
|
+
}
|
|
232
|
+
const blocked = blockMember(memberId, reason);
|
|
233
|
+
if (!blocked) {
|
|
234
|
+
return { ok: false, error: 'Member not found or already blocked' };
|
|
235
|
+
}
|
|
236
|
+
return { ok: true, data: memberToResponse(blocked) };
|
|
237
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { ConfigError, IngressBlockedError } from '../../util/errors.js';
|
|
6
6
|
import { getLogger } from '../../util/logger.js';
|
|
7
|
+
import { httpError } from '../http-errors.js';
|
|
7
8
|
|
|
8
9
|
const log = getLogger('runtime-http');
|
|
9
10
|
|
|
@@ -20,14 +21,14 @@ export async function withErrorHandling(
|
|
|
20
21
|
} catch (err) {
|
|
21
22
|
if (err instanceof IngressBlockedError) {
|
|
22
23
|
log.warn({ endpoint, detectedTypes: err.detectedTypes }, 'Blocked HTTP request containing secrets');
|
|
23
|
-
return
|
|
24
|
+
return httpError('UNPROCESSABLE_ENTITY', err.message, 422);
|
|
24
25
|
}
|
|
25
26
|
if (err instanceof ConfigError) {
|
|
26
27
|
log.warn({ err, endpoint }, 'Runtime HTTP config error');
|
|
27
|
-
return
|
|
28
|
+
return httpError('UNPROCESSABLE_ENTITY', err.message, 422);
|
|
28
29
|
}
|
|
29
30
|
log.error({ err, endpoint }, 'Runtime HTTP handler error');
|
|
30
31
|
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
31
|
-
return
|
|
32
|
+
return httpError('INTERNAL_ERROR', message, 500);
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Per-client-IP sliding-window rate limiter for /v1/* API endpoints.
|
|
2
|
+
// Tracks request counts per key and returns 429 when the limit is exceeded.
|
|
3
|
+
// Follows the same sliding-window pattern as gateway/src/auth-rate-limiter.ts.
|
|
4
|
+
|
|
5
|
+
import type { HttpErrorResponse } from '../http-errors.js';
|
|
6
|
+
import { isPrivateAddress } from './auth.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_REQUESTS = 60;
|
|
9
|
+
const DEFAULT_WINDOW_MS = 60_000; // 60 seconds
|
|
10
|
+
const MAX_TRACKED_TOKENS = 10_000;
|
|
11
|
+
|
|
12
|
+
// Lower limit for unauthenticated (IP-based) requests to reduce abuse surface.
|
|
13
|
+
const DEFAULT_IP_MAX_REQUESTS = 20;
|
|
14
|
+
const DEFAULT_IP_WINDOW_MS = 60_000;
|
|
15
|
+
const MAX_TRACKED_IPS = 50_000;
|
|
16
|
+
|
|
17
|
+
export class TokenRateLimiter {
|
|
18
|
+
private requests = new Map<string, number[]>();
|
|
19
|
+
private readonly maxRequests: number;
|
|
20
|
+
private readonly windowMs: number;
|
|
21
|
+
private readonly maxTrackedKeys: number;
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
maxRequests = DEFAULT_MAX_REQUESTS,
|
|
25
|
+
windowMs = DEFAULT_WINDOW_MS,
|
|
26
|
+
maxTrackedKeys = MAX_TRACKED_TOKENS,
|
|
27
|
+
) {
|
|
28
|
+
this.maxRequests = maxRequests;
|
|
29
|
+
this.windowMs = windowMs;
|
|
30
|
+
this.maxTrackedKeys = maxTrackedKeys;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check whether the request should be allowed and record it.
|
|
35
|
+
* Returns rate limit metadata for response headers.
|
|
36
|
+
*/
|
|
37
|
+
check(key: string): RateLimitResult {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
let timestamps = this.requests.get(key);
|
|
40
|
+
|
|
41
|
+
if (!timestamps) {
|
|
42
|
+
if (this.requests.size >= this.maxTrackedKeys) {
|
|
43
|
+
this.evictStale(now);
|
|
44
|
+
if (this.requests.size >= this.maxTrackedKeys) {
|
|
45
|
+
const oldest = this.requests.keys().next().value;
|
|
46
|
+
if (oldest !== undefined) this.requests.delete(oldest);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
timestamps = [];
|
|
50
|
+
this.requests.set(key, timestamps);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const cutoff = now - this.windowMs;
|
|
54
|
+
|
|
55
|
+
// Remove expired timestamps from the front
|
|
56
|
+
while (timestamps.length > 0 && timestamps[0] <= cutoff) {
|
|
57
|
+
timestamps.shift();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const remaining = Math.max(0, this.maxRequests - timestamps.length);
|
|
61
|
+
const resetAt = timestamps.length > 0
|
|
62
|
+
? Math.ceil((timestamps[0] + this.windowMs) / 1000)
|
|
63
|
+
: Math.ceil((now + this.windowMs) / 1000);
|
|
64
|
+
|
|
65
|
+
if (timestamps.length >= this.maxRequests) {
|
|
66
|
+
return {
|
|
67
|
+
allowed: false,
|
|
68
|
+
limit: this.maxRequests,
|
|
69
|
+
remaining: 0,
|
|
70
|
+
resetAt,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
timestamps.push(now);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
allowed: true,
|
|
78
|
+
limit: this.maxRequests,
|
|
79
|
+
remaining: remaining - 1,
|
|
80
|
+
resetAt,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private evictStale(now: number): void {
|
|
85
|
+
const cutoff = now - this.windowMs;
|
|
86
|
+
for (const [key, timestamps] of this.requests) {
|
|
87
|
+
while (timestamps.length > 0 && timestamps[0] <= cutoff) {
|
|
88
|
+
timestamps.shift();
|
|
89
|
+
}
|
|
90
|
+
if (timestamps.length === 0) {
|
|
91
|
+
this.requests.delete(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface RateLimitResult {
|
|
98
|
+
allowed: boolean;
|
|
99
|
+
limit: number;
|
|
100
|
+
remaining: number;
|
|
101
|
+
/** Unix timestamp (seconds) when the window resets. */
|
|
102
|
+
resetAt: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Build standard rate limit headers from a check result. */
|
|
106
|
+
export function rateLimitHeaders(result: RateLimitResult): Record<string, string> {
|
|
107
|
+
return {
|
|
108
|
+
'X-RateLimit-Limit': String(result.limit),
|
|
109
|
+
'X-RateLimit-Remaining': String(result.remaining),
|
|
110
|
+
'X-RateLimit-Reset': String(result.resetAt),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Return a 429 response with rate limit headers and a Retry-After hint. */
|
|
115
|
+
export function rateLimitResponse(result: RateLimitResult): Response {
|
|
116
|
+
const retryAfter = Math.max(1, result.resetAt - Math.ceil(Date.now() / 1000));
|
|
117
|
+
const body: HttpErrorResponse = {
|
|
118
|
+
error: { code: 'RATE_LIMITED', message: 'Too Many Requests' },
|
|
119
|
+
};
|
|
120
|
+
return Response.json(body, {
|
|
121
|
+
status: 429,
|
|
122
|
+
headers: {
|
|
123
|
+
...rateLimitHeaders(result),
|
|
124
|
+
'Retry-After': String(retryAfter),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Singleton rate limiter for authenticated /v1/* requests (per-client-IP). */
|
|
130
|
+
export const apiRateLimiter = new TokenRateLimiter();
|
|
131
|
+
|
|
132
|
+
/** Singleton rate limiter for unauthenticated requests (per-IP, lower limits). */
|
|
133
|
+
export const ipRateLimiter = new TokenRateLimiter(DEFAULT_IP_MAX_REQUESTS, DEFAULT_IP_WINDOW_MS, MAX_TRACKED_IPS);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extract the client IP from a request. Only trusts proxy headers
|
|
137
|
+
* (X-Forwarded-For, X-Real-IP) when the peer IP is loopback or private,
|
|
138
|
+
* meaning the request arrived via the gateway. Direct connections from
|
|
139
|
+
* external clients use the peer IP, preventing header spoofing.
|
|
140
|
+
*/
|
|
141
|
+
export function extractClientIp(
|
|
142
|
+
req: Request,
|
|
143
|
+
server: { requestIP(req: Request): { address: string } | null },
|
|
144
|
+
): string {
|
|
145
|
+
const peerIp = server.requestIP(req)?.address ?? '0.0.0.0';
|
|
146
|
+
|
|
147
|
+
if (isPrivateAddress(peerIp)) {
|
|
148
|
+
const forwarded = req.headers.get('x-forwarded-for');
|
|
149
|
+
if (forwarded) {
|
|
150
|
+
const first = forwarded.split(',')[0].trim();
|
|
151
|
+
if (first) return first;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const realIp = req.headers.get('x-real-ip');
|
|
155
|
+
if (realIp) return realIp.trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return peerIp;
|
|
159
|
+
}
|
|
160
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP request/response logging middleware.
|
|
3
|
+
*
|
|
4
|
+
* Logs method, path, status, and latency for every request to aid
|
|
5
|
+
* debugging client issues. Uses structured Pino logging.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLogger } from '../../util/logger.js';
|
|
9
|
+
|
|
10
|
+
const log = getLogger('http-request');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a request handler to log request metadata and response timing.
|
|
14
|
+
*
|
|
15
|
+
* The handler may return `undefined` for WebSocket upgrades (Bun consumes
|
|
16
|
+
* the request and there is no HTTP response to send).
|
|
17
|
+
*/
|
|
18
|
+
export async function withRequestLogging(
|
|
19
|
+
req: Request,
|
|
20
|
+
handler: () => Promise<Response>,
|
|
21
|
+
): Promise<Response> {
|
|
22
|
+
const start = performance.now();
|
|
23
|
+
const url = new URL(req.url);
|
|
24
|
+
const method = req.method;
|
|
25
|
+
const path = url.pathname;
|
|
26
|
+
|
|
27
|
+
let response: Response;
|
|
28
|
+
try {
|
|
29
|
+
response = await handler();
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
32
|
+
log.error(
|
|
33
|
+
{ method, path, latencyMs, err },
|
|
34
|
+
`${method} ${path} -> error (${latencyMs}ms)`,
|
|
35
|
+
);
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const latencyMs = Math.round(performance.now() - start);
|
|
40
|
+
|
|
41
|
+
// WebSocket upgrades return undefined — log and pass through without
|
|
42
|
+
// dereferencing response properties.
|
|
43
|
+
if (!response) {
|
|
44
|
+
log.info(
|
|
45
|
+
{ method, path, latencyMs },
|
|
46
|
+
`${method} ${path} -> ws-upgrade (${latencyMs}ms)`,
|
|
47
|
+
);
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const status = response.status;
|
|
52
|
+
|
|
53
|
+
const logData = {
|
|
54
|
+
method,
|
|
55
|
+
path,
|
|
56
|
+
status,
|
|
57
|
+
latencyMs,
|
|
58
|
+
contentType: req.headers.get('content-type') ?? undefined,
|
|
59
|
+
userAgent: req.headers.get('user-agent') ?? undefined,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (status >= 500) {
|
|
63
|
+
log.error(logData, `${method} ${path} -> ${status} (${latencyMs}ms)`);
|
|
64
|
+
} else if (status >= 400) {
|
|
65
|
+
log.warn(logData, `${method} ${path} -> ${status} (${latencyMs}ms)`);
|
|
66
|
+
} else {
|
|
67
|
+
log.info(logData, `${method} ${path} -> ${status} (${latencyMs}ms)`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return response;
|
|
71
|
+
}
|
|
@@ -7,6 +7,7 @@ import { isTwilioWebhookValidationDisabled } from '../../config/env.js';
|
|
|
7
7
|
import { loadConfig } from '../../config/loader.js';
|
|
8
8
|
import { getPublicBaseUrl } from '../../inbound/public-ingress-urls.js';
|
|
9
9
|
import { getLogger } from '../../util/logger.js';
|
|
10
|
+
import { httpError } from '../http-errors.js';
|
|
10
11
|
|
|
11
12
|
const log = getLogger('runtime-http');
|
|
12
13
|
|
|
@@ -68,13 +69,13 @@ export async function validateTwilioWebhook(
|
|
|
68
69
|
// Fail-closed: reject if no auth token is configured
|
|
69
70
|
if (!authToken) {
|
|
70
71
|
log.error('Twilio auth token not configured — rejecting webhook request (fail-closed)');
|
|
71
|
-
return
|
|
72
|
+
return httpError('FORBIDDEN', 'Forbidden', 403);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
const signature = req.headers.get('x-twilio-signature');
|
|
75
76
|
if (!signature) {
|
|
76
77
|
log.warn('Twilio webhook request missing X-Twilio-Signature header');
|
|
77
|
-
return
|
|
78
|
+
return httpError('FORBIDDEN', 'Forbidden', 403);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// Parse form-urlencoded body into key-value params for signature computation
|
|
@@ -85,9 +86,9 @@ export async function validateTwilioWebhook(
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// Reconstruct the public-facing URL that Twilio signed against.
|
|
88
|
-
// Behind proxies/gateways, req.url is the local
|
|
89
|
-
//
|
|
90
|
-
//
|
|
89
|
+
// Behind proxies/gateways, req.url is the local runtime URL which
|
|
90
|
+
// differs from the public URL Twilio used to compute the HMAC-SHA1
|
|
91
|
+
// signature.
|
|
91
92
|
let publicBaseUrl: string | undefined;
|
|
92
93
|
try {
|
|
93
94
|
publicBaseUrl = getPublicBaseUrl(loadConfig());
|
|
@@ -108,7 +109,7 @@ export async function validateTwilioWebhook(
|
|
|
108
109
|
|
|
109
110
|
if (!isValid) {
|
|
110
111
|
log.warn('Twilio webhook signature validation failed');
|
|
111
|
-
return
|
|
112
|
+
return httpError('FORBIDDEN', 'Forbidden', 403);
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
return { body: rawBody };
|
|
@@ -67,6 +67,18 @@ export function getByConversation(conversationId: string): Array<{ requestId: st
|
|
|
67
67
|
return results;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Remove all pending interactions for a given session.
|
|
72
|
+
* Used when auto-denying all pending confirmations (e.g. new user message).
|
|
73
|
+
*/
|
|
74
|
+
export function removeBySession(session: Session): void {
|
|
75
|
+
for (const [requestId, interaction] of pending) {
|
|
76
|
+
if (interaction.session === session) {
|
|
77
|
+
pending.delete(requestId);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
/** Clear all pending interactions. Useful for testing. */
|
|
71
83
|
export function clear(): void {
|
|
72
84
|
pending.clear();
|