@vellumai/assistant 0.3.15 → 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 +1 -1
- 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-outbound-http.test.ts +194 -2
- 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 -1
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- 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 +2 -2
- 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/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- 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-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 +5 -6
- 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 +0 -3
- 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 +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- 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 +2 -1
- 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 +78 -16
- 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 +1 -1
- 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
|
@@ -1,1320 +1,69 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* and guardian approval requests.
|
|
2
|
+
* Re-export hub for channel guardian store modules.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import { and, count, desc, eq, gt, gte, inArray, lte, or } from 'drizzle-orm';
|
|
12
|
-
import { v4 as uuid } from 'uuid';
|
|
13
|
-
|
|
14
|
-
import { getDb } from './db.js';
|
|
15
|
-
import {
|
|
16
|
-
channelGuardianApprovalRequests,
|
|
17
|
-
channelGuardianBindings,
|
|
18
|
-
channelGuardianRateLimits,
|
|
19
|
-
channelGuardianVerificationChallenges,
|
|
20
|
-
} from './schema.js';
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Types
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
export type BindingStatus = 'active' | 'revoked';
|
|
27
|
-
export type ChallengeStatus = 'pending' | 'consumed' | 'expired' | 'revoked';
|
|
28
|
-
export type SessionStatus = 'pending' | 'consumed' | 'pending_bootstrap' | 'awaiting_response' | 'verified' | 'expired' | 'revoked' | 'locked';
|
|
29
|
-
export type IdentityBindingStatus = 'pending_bootstrap' | 'bound';
|
|
30
|
-
export type ApprovalRequestStatus = 'pending' | 'approved' | 'denied' | 'expired' | 'cancelled';
|
|
31
|
-
|
|
32
|
-
export interface GuardianBinding {
|
|
33
|
-
id: string;
|
|
34
|
-
assistantId: string;
|
|
35
|
-
channel: string;
|
|
36
|
-
guardianExternalUserId: string;
|
|
37
|
-
guardianDeliveryChatId: string;
|
|
38
|
-
status: BindingStatus;
|
|
39
|
-
verifiedAt: number;
|
|
40
|
-
verifiedVia: string;
|
|
41
|
-
metadataJson: string | null;
|
|
42
|
-
createdAt: number;
|
|
43
|
-
updatedAt: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface VerificationChallenge {
|
|
47
|
-
id: string;
|
|
48
|
-
assistantId: string;
|
|
49
|
-
channel: string;
|
|
50
|
-
challengeHash: string;
|
|
51
|
-
expiresAt: number;
|
|
52
|
-
status: SessionStatus;
|
|
53
|
-
createdBySessionId: string | null;
|
|
54
|
-
consumedByExternalUserId: string | null;
|
|
55
|
-
consumedByChatId: string | null;
|
|
56
|
-
// Outbound session: expected-identity binding
|
|
57
|
-
expectedExternalUserId: string | null;
|
|
58
|
-
expectedChatId: string | null;
|
|
59
|
-
expectedPhoneE164: string | null;
|
|
60
|
-
identityBindingStatus: IdentityBindingStatus | null;
|
|
61
|
-
// Outbound session: delivery tracking
|
|
62
|
-
destinationAddress: string | null;
|
|
63
|
-
lastSentAt: number | null;
|
|
64
|
-
sendCount: number;
|
|
65
|
-
nextResendAt: number | null;
|
|
66
|
-
// Session configuration
|
|
67
|
-
codeDigits: number;
|
|
68
|
-
maxAttempts: number;
|
|
69
|
-
// Telegram bootstrap deep-link token hash
|
|
70
|
-
bootstrapTokenHash: string | null;
|
|
71
|
-
createdAt: number;
|
|
72
|
-
updatedAt: number;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface GuardianApprovalRequest {
|
|
76
|
-
id: string;
|
|
77
|
-
runId: string;
|
|
78
|
-
requestId: string | null;
|
|
79
|
-
conversationId: string;
|
|
80
|
-
assistantId: string;
|
|
81
|
-
channel: string;
|
|
82
|
-
requesterExternalUserId: string;
|
|
83
|
-
requesterChatId: string;
|
|
84
|
-
guardianExternalUserId: string;
|
|
85
|
-
guardianChatId: string;
|
|
86
|
-
toolName: string;
|
|
87
|
-
riskLevel: string | null;
|
|
88
|
-
reason: string | null;
|
|
89
|
-
status: ApprovalRequestStatus;
|
|
90
|
-
decidedByExternalUserId: string | null;
|
|
91
|
-
expiresAt: number;
|
|
92
|
-
createdAt: number;
|
|
93
|
-
updatedAt: number;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Helpers
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
function rowToBinding(row: typeof channelGuardianBindings.$inferSelect): GuardianBinding {
|
|
101
|
-
return {
|
|
102
|
-
id: row.id,
|
|
103
|
-
assistantId: row.assistantId,
|
|
104
|
-
channel: row.channel,
|
|
105
|
-
guardianExternalUserId: row.guardianExternalUserId,
|
|
106
|
-
guardianDeliveryChatId: row.guardianDeliveryChatId,
|
|
107
|
-
status: row.status as BindingStatus,
|
|
108
|
-
verifiedAt: row.verifiedAt,
|
|
109
|
-
verifiedVia: row.verifiedVia,
|
|
110
|
-
metadataJson: row.metadataJson,
|
|
111
|
-
createdAt: row.createdAt,
|
|
112
|
-
updatedAt: row.updatedAt,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function rowToChallenge(row: typeof channelGuardianVerificationChallenges.$inferSelect): VerificationChallenge {
|
|
117
|
-
return {
|
|
118
|
-
id: row.id,
|
|
119
|
-
assistantId: row.assistantId,
|
|
120
|
-
channel: row.channel,
|
|
121
|
-
challengeHash: row.challengeHash,
|
|
122
|
-
expiresAt: row.expiresAt,
|
|
123
|
-
status: row.status as SessionStatus,
|
|
124
|
-
createdBySessionId: row.createdBySessionId,
|
|
125
|
-
consumedByExternalUserId: row.consumedByExternalUserId,
|
|
126
|
-
consumedByChatId: row.consumedByChatId,
|
|
127
|
-
expectedExternalUserId: row.expectedExternalUserId ?? null,
|
|
128
|
-
expectedChatId: row.expectedChatId ?? null,
|
|
129
|
-
expectedPhoneE164: row.expectedPhoneE164 ?? null,
|
|
130
|
-
identityBindingStatus: (row.identityBindingStatus as IdentityBindingStatus) ?? null,
|
|
131
|
-
destinationAddress: row.destinationAddress ?? null,
|
|
132
|
-
lastSentAt: row.lastSentAt ?? null,
|
|
133
|
-
sendCount: row.sendCount ?? 0,
|
|
134
|
-
nextResendAt: row.nextResendAt ?? null,
|
|
135
|
-
codeDigits: row.codeDigits ?? 6,
|
|
136
|
-
maxAttempts: row.maxAttempts ?? 3,
|
|
137
|
-
bootstrapTokenHash: row.bootstrapTokenHash ?? null,
|
|
138
|
-
createdAt: row.createdAt,
|
|
139
|
-
updatedAt: row.updatedAt,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function rowToApprovalRequest(row: typeof channelGuardianApprovalRequests.$inferSelect): GuardianApprovalRequest {
|
|
144
|
-
return {
|
|
145
|
-
id: row.id,
|
|
146
|
-
runId: row.runId,
|
|
147
|
-
requestId: row.requestId ?? null,
|
|
148
|
-
conversationId: row.conversationId,
|
|
149
|
-
assistantId: row.assistantId,
|
|
150
|
-
channel: row.channel,
|
|
151
|
-
requesterExternalUserId: row.requesterExternalUserId,
|
|
152
|
-
requesterChatId: row.requesterChatId,
|
|
153
|
-
guardianExternalUserId: row.guardianExternalUserId,
|
|
154
|
-
guardianChatId: row.guardianChatId,
|
|
155
|
-
toolName: row.toolName,
|
|
156
|
-
riskLevel: row.riskLevel,
|
|
157
|
-
reason: row.reason,
|
|
158
|
-
status: row.status as ApprovalRequestStatus,
|
|
159
|
-
decidedByExternalUserId: row.decidedByExternalUserId,
|
|
160
|
-
expiresAt: row.expiresAt,
|
|
161
|
-
createdAt: row.createdAt,
|
|
162
|
-
updatedAt: row.updatedAt,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ---------------------------------------------------------------------------
|
|
167
|
-
// Guardian Bindings
|
|
168
|
-
// ---------------------------------------------------------------------------
|
|
169
|
-
|
|
170
|
-
export function createBinding(params: {
|
|
171
|
-
assistantId: string;
|
|
172
|
-
channel: string;
|
|
173
|
-
guardianExternalUserId: string;
|
|
174
|
-
guardianDeliveryChatId: string;
|
|
175
|
-
verifiedVia?: string;
|
|
176
|
-
metadataJson?: string | null;
|
|
177
|
-
}): GuardianBinding {
|
|
178
|
-
const db = getDb();
|
|
179
|
-
const now = Date.now();
|
|
180
|
-
const id = uuid();
|
|
181
|
-
|
|
182
|
-
const row = {
|
|
183
|
-
id,
|
|
184
|
-
assistantId: params.assistantId,
|
|
185
|
-
channel: params.channel,
|
|
186
|
-
guardianExternalUserId: params.guardianExternalUserId,
|
|
187
|
-
guardianDeliveryChatId: params.guardianDeliveryChatId,
|
|
188
|
-
status: 'active' as const,
|
|
189
|
-
verifiedAt: now,
|
|
190
|
-
verifiedVia: params.verifiedVia ?? 'challenge',
|
|
191
|
-
metadataJson: params.metadataJson ?? null,
|
|
192
|
-
createdAt: now,
|
|
193
|
-
updatedAt: now,
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
db.insert(channelGuardianBindings).values(row).run();
|
|
197
|
-
|
|
198
|
-
return rowToBinding(row);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function getActiveBinding(assistantId: string, channel: string): GuardianBinding | null {
|
|
202
|
-
const db = getDb();
|
|
203
|
-
const row = db
|
|
204
|
-
.select()
|
|
205
|
-
.from(channelGuardianBindings)
|
|
206
|
-
.where(
|
|
207
|
-
and(
|
|
208
|
-
eq(channelGuardianBindings.assistantId, assistantId),
|
|
209
|
-
eq(channelGuardianBindings.channel, channel),
|
|
210
|
-
eq(channelGuardianBindings.status, 'active'),
|
|
211
|
-
),
|
|
212
|
-
)
|
|
213
|
-
.get();
|
|
214
|
-
|
|
215
|
-
return row ? rowToBinding(row) : null;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function revokeBinding(assistantId: string, channel: string): boolean {
|
|
219
|
-
const db = getDb();
|
|
220
|
-
const now = Date.now();
|
|
221
|
-
|
|
222
|
-
const existing = db
|
|
223
|
-
.select({ id: channelGuardianBindings.id })
|
|
224
|
-
.from(channelGuardianBindings)
|
|
225
|
-
.where(
|
|
226
|
-
and(
|
|
227
|
-
eq(channelGuardianBindings.assistantId, assistantId),
|
|
228
|
-
eq(channelGuardianBindings.channel, channel),
|
|
229
|
-
eq(channelGuardianBindings.status, 'active'),
|
|
230
|
-
),
|
|
231
|
-
)
|
|
232
|
-
.get();
|
|
233
|
-
|
|
234
|
-
if (!existing) return false;
|
|
235
|
-
|
|
236
|
-
db.update(channelGuardianBindings)
|
|
237
|
-
.set({ status: 'revoked', updatedAt: now })
|
|
238
|
-
.where(eq(channelGuardianBindings.id, existing.id))
|
|
239
|
-
.run();
|
|
240
|
-
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ---------------------------------------------------------------------------
|
|
245
|
-
// Verification Challenges
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
|
|
248
|
-
export function createChallenge(params: {
|
|
249
|
-
id: string;
|
|
250
|
-
assistantId: string;
|
|
251
|
-
channel: string;
|
|
252
|
-
challengeHash: string;
|
|
253
|
-
expiresAt: number;
|
|
254
|
-
createdBySessionId?: string;
|
|
255
|
-
}): VerificationChallenge {
|
|
256
|
-
const db = getDb();
|
|
257
|
-
const now = Date.now();
|
|
258
|
-
|
|
259
|
-
// Revoke any prior pending challenges for the same (assistantId, channel)
|
|
260
|
-
// to close the replay window — only the latest challenge should be valid.
|
|
261
|
-
db.update(channelGuardianVerificationChallenges)
|
|
262
|
-
.set({ status: 'revoked', updatedAt: now })
|
|
263
|
-
.where(
|
|
264
|
-
and(
|
|
265
|
-
eq(channelGuardianVerificationChallenges.assistantId, params.assistantId),
|
|
266
|
-
eq(channelGuardianVerificationChallenges.channel, params.channel),
|
|
267
|
-
eq(channelGuardianVerificationChallenges.status, 'pending'),
|
|
268
|
-
),
|
|
269
|
-
)
|
|
270
|
-
.run();
|
|
271
|
-
|
|
272
|
-
const row = {
|
|
273
|
-
id: params.id,
|
|
274
|
-
assistantId: params.assistantId,
|
|
275
|
-
channel: params.channel,
|
|
276
|
-
challengeHash: params.challengeHash,
|
|
277
|
-
expiresAt: params.expiresAt,
|
|
278
|
-
status: 'pending' as const,
|
|
279
|
-
createdBySessionId: params.createdBySessionId ?? null,
|
|
280
|
-
consumedByExternalUserId: null,
|
|
281
|
-
consumedByChatId: null,
|
|
282
|
-
expectedExternalUserId: null,
|
|
283
|
-
expectedChatId: null,
|
|
284
|
-
expectedPhoneE164: null,
|
|
285
|
-
identityBindingStatus: 'bound' as const,
|
|
286
|
-
destinationAddress: null,
|
|
287
|
-
lastSentAt: null,
|
|
288
|
-
sendCount: 0,
|
|
289
|
-
nextResendAt: null,
|
|
290
|
-
codeDigits: 6,
|
|
291
|
-
maxAttempts: 3,
|
|
292
|
-
bootstrapTokenHash: null,
|
|
293
|
-
createdAt: now,
|
|
294
|
-
updatedAt: now,
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
db.insert(channelGuardianVerificationChallenges).values(row).run();
|
|
298
|
-
|
|
299
|
-
return rowToChallenge(row);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function revokePendingChallenges(assistantId: string, channel: string): void {
|
|
303
|
-
const db = getDb();
|
|
304
|
-
db.update(channelGuardianVerificationChallenges)
|
|
305
|
-
.set({ status: 'revoked', updatedAt: Date.now() })
|
|
306
|
-
.where(
|
|
307
|
-
and(
|
|
308
|
-
eq(channelGuardianVerificationChallenges.assistantId, assistantId),
|
|
309
|
-
eq(channelGuardianVerificationChallenges.channel, channel),
|
|
310
|
-
eq(channelGuardianVerificationChallenges.status, 'pending'),
|
|
311
|
-
),
|
|
312
|
-
)
|
|
313
|
-
.run();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function findPendingChallengeByHash(
|
|
317
|
-
assistantId: string,
|
|
318
|
-
channel: string,
|
|
319
|
-
challengeHash: string,
|
|
320
|
-
): VerificationChallenge | null {
|
|
321
|
-
const db = getDb();
|
|
322
|
-
const now = Date.now();
|
|
323
|
-
|
|
324
|
-
// Match any consumable status: 'pending' (inbound), 'pending_bootstrap', 'awaiting_response' (outbound)
|
|
325
|
-
const row = db
|
|
326
|
-
.select()
|
|
327
|
-
.from(channelGuardianVerificationChallenges)
|
|
328
|
-
.where(
|
|
329
|
-
and(
|
|
330
|
-
eq(channelGuardianVerificationChallenges.assistantId, assistantId),
|
|
331
|
-
eq(channelGuardianVerificationChallenges.channel, channel),
|
|
332
|
-
eq(channelGuardianVerificationChallenges.challengeHash, challengeHash),
|
|
333
|
-
inArray(channelGuardianVerificationChallenges.status, ['pending', 'pending_bootstrap', 'awaiting_response']),
|
|
334
|
-
gt(channelGuardianVerificationChallenges.expiresAt, now),
|
|
335
|
-
),
|
|
336
|
-
)
|
|
337
|
-
.get();
|
|
338
|
-
|
|
339
|
-
return row ? rowToChallenge(row) : null;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Find any pending inbound (non-expired) challenge for a given (assistantId, channel).
|
|
344
|
-
* Scoped to 'pending' status only — this is the inbound verification path used by
|
|
345
|
-
* the relay-server to gate incoming voice calls. Outbound session states
|
|
346
|
-
* (pending_bootstrap, awaiting_response) are excluded so that an active outbound
|
|
347
|
-
* verification does not inadvertently force unrelated inbound callers into the
|
|
348
|
-
* guardian verification flow.
|
|
349
|
-
*/
|
|
350
|
-
export function findPendingChallengeForChannel(
|
|
351
|
-
assistantId: string,
|
|
352
|
-
channel: string,
|
|
353
|
-
): VerificationChallenge | null {
|
|
354
|
-
const db = getDb();
|
|
355
|
-
const now = Date.now();
|
|
356
|
-
|
|
357
|
-
const row = db
|
|
358
|
-
.select()
|
|
359
|
-
.from(channelGuardianVerificationChallenges)
|
|
360
|
-
.where(
|
|
361
|
-
and(
|
|
362
|
-
eq(channelGuardianVerificationChallenges.assistantId, assistantId),
|
|
363
|
-
eq(channelGuardianVerificationChallenges.channel, channel),
|
|
364
|
-
eq(channelGuardianVerificationChallenges.status, 'pending'),
|
|
365
|
-
gt(channelGuardianVerificationChallenges.expiresAt, now),
|
|
366
|
-
),
|
|
367
|
-
)
|
|
368
|
-
.get();
|
|
369
|
-
|
|
370
|
-
return row ? rowToChallenge(row) : null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export function consumeChallenge(
|
|
374
|
-
id: string,
|
|
375
|
-
consumedByExternalUserId: string,
|
|
376
|
-
consumedByChatId: string,
|
|
377
|
-
): void {
|
|
378
|
-
const db = getDb();
|
|
379
|
-
const now = Date.now();
|
|
380
|
-
|
|
381
|
-
db.update(channelGuardianVerificationChallenges)
|
|
382
|
-
.set({
|
|
383
|
-
status: 'consumed',
|
|
384
|
-
consumedByExternalUserId,
|
|
385
|
-
consumedByChatId,
|
|
386
|
-
updatedAt: now,
|
|
387
|
-
})
|
|
388
|
-
.where(eq(channelGuardianVerificationChallenges.id, id))
|
|
389
|
-
.run();
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// ---------------------------------------------------------------------------
|
|
393
|
-
// Verification Sessions (outbound identity-bound)
|
|
394
|
-
// ---------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Create an outbound verification session with expected-identity binding.
|
|
398
|
-
* Auto-revokes prior pending/awaiting_response sessions for the same
|
|
399
|
-
* (assistantId, channel) to close the replay window.
|
|
400
|
-
*/
|
|
401
|
-
export function createVerificationSession(params: {
|
|
402
|
-
id: string;
|
|
403
|
-
assistantId: string;
|
|
404
|
-
channel: string;
|
|
405
|
-
challengeHash: string;
|
|
406
|
-
expiresAt: number;
|
|
407
|
-
status: SessionStatus;
|
|
408
|
-
createdBySessionId?: string;
|
|
409
|
-
expectedExternalUserId?: string | null;
|
|
410
|
-
expectedChatId?: string | null;
|
|
411
|
-
expectedPhoneE164?: string | null;
|
|
412
|
-
identityBindingStatus?: IdentityBindingStatus;
|
|
413
|
-
destinationAddress?: string | null;
|
|
414
|
-
codeDigits?: number;
|
|
415
|
-
maxAttempts?: number;
|
|
416
|
-
bootstrapTokenHash?: string | null;
|
|
417
|
-
}): VerificationChallenge {
|
|
418
|
-
const db = getDb();
|
|
419
|
-
const now = Date.now();
|
|
420
|
-
|
|
421
|
-
// Revoke any prior pending/awaiting_response sessions for the same (assistantId, channel)
|
|
422
|
-
db.update(channelGuardianVerificationChallenges)
|
|
423
|
-
.set({ status: 'revoked', updatedAt: now })
|
|
424
|
-
.where(
|
|
425
|
-
and(
|
|
426
|
-
eq(channelGuardianVerificationChallenges.assistantId, params.assistantId),
|
|
427
|
-
eq(channelGuardianVerificationChallenges.channel, params.channel),
|
|
428
|
-
inArray(channelGuardianVerificationChallenges.status, ['pending', 'pending_bootstrap', 'awaiting_response']),
|
|
429
|
-
),
|
|
430
|
-
)
|
|
431
|
-
.run();
|
|
432
|
-
|
|
433
|
-
const row = {
|
|
434
|
-
id: params.id,
|
|
435
|
-
assistantId: params.assistantId,
|
|
436
|
-
channel: params.channel,
|
|
437
|
-
challengeHash: params.challengeHash,
|
|
438
|
-
expiresAt: params.expiresAt,
|
|
439
|
-
status: params.status as string,
|
|
440
|
-
createdBySessionId: params.createdBySessionId ?? null,
|
|
441
|
-
consumedByExternalUserId: null,
|
|
442
|
-
consumedByChatId: null,
|
|
443
|
-
expectedExternalUserId: params.expectedExternalUserId ?? null,
|
|
444
|
-
expectedChatId: params.expectedChatId ?? null,
|
|
445
|
-
expectedPhoneE164: params.expectedPhoneE164 ?? null,
|
|
446
|
-
identityBindingStatus: params.identityBindingStatus ?? 'bound',
|
|
447
|
-
destinationAddress: params.destinationAddress ?? null,
|
|
448
|
-
lastSentAt: null,
|
|
449
|
-
sendCount: 0,
|
|
450
|
-
nextResendAt: null,
|
|
451
|
-
codeDigits: params.codeDigits ?? 6,
|
|
452
|
-
maxAttempts: params.maxAttempts ?? 3,
|
|
453
|
-
bootstrapTokenHash: params.bootstrapTokenHash ?? null,
|
|
454
|
-
createdAt: now,
|
|
455
|
-
updatedAt: now,
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
db.insert(channelGuardianVerificationChallenges).values(row).run();
|
|
459
|
-
|
|
460
|
-
return rowToChallenge(row);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* Find the most recent pending_bootstrap or awaiting_response session
|
|
465
|
-
* for a given (assistantId, channel).
|
|
466
|
-
*/
|
|
467
|
-
export function findActiveSession(
|
|
468
|
-
assistantId: string,
|
|
469
|
-
channel: string,
|
|
470
|
-
): VerificationChallenge | null {
|
|
471
|
-
const db = getDb();
|
|
472
|
-
const now = Date.now();
|
|
473
|
-
|
|
474
|
-
const row = db
|
|
475
|
-
.select()
|
|
476
|
-
.from(channelGuardianVerificationChallenges)
|
|
477
|
-
.where(
|
|
478
|
-
and(
|
|
479
|
-
eq(channelGuardianVerificationChallenges.assistantId, assistantId),
|
|
480
|
-
eq(channelGuardianVerificationChallenges.channel, channel),
|
|
481
|
-
inArray(channelGuardianVerificationChallenges.status, ['pending_bootstrap', 'awaiting_response']),
|
|
482
|
-
gt(channelGuardianVerificationChallenges.expiresAt, now),
|
|
483
|
-
),
|
|
484
|
-
)
|
|
485
|
-
.orderBy(desc(channelGuardianVerificationChallenges.createdAt))
|
|
486
|
-
.get();
|
|
487
|
-
|
|
488
|
-
return row ? rowToChallenge(row) : null;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Look up a pending_bootstrap session by its bootstrap token hash.
|
|
493
|
-
* Used by the Telegram /start gv_<token> bootstrap flow.
|
|
494
|
-
*/
|
|
495
|
-
export function findSessionByBootstrapTokenHash(
|
|
496
|
-
assistantId: string,
|
|
497
|
-
channel: string,
|
|
498
|
-
tokenHash: string,
|
|
499
|
-
): VerificationChallenge | null {
|
|
500
|
-
const db = getDb();
|
|
501
|
-
const now = Date.now();
|
|
502
|
-
|
|
503
|
-
const row = db
|
|
504
|
-
.select()
|
|
505
|
-
.from(channelGuardianVerificationChallenges)
|
|
506
|
-
.where(
|
|
507
|
-
and(
|
|
508
|
-
eq(channelGuardianVerificationChallenges.assistantId, assistantId),
|
|
509
|
-
eq(channelGuardianVerificationChallenges.channel, channel),
|
|
510
|
-
eq(channelGuardianVerificationChallenges.bootstrapTokenHash, tokenHash),
|
|
511
|
-
eq(channelGuardianVerificationChallenges.status, 'pending_bootstrap'),
|
|
512
|
-
gt(channelGuardianVerificationChallenges.expiresAt, now),
|
|
513
|
-
),
|
|
514
|
-
)
|
|
515
|
-
.get();
|
|
516
|
-
|
|
517
|
-
return row ? rowToChallenge(row) : null;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Identity-bound lookup for the consume path. Finds a session matching the
|
|
522
|
-
* given identity fields with an active status.
|
|
523
|
-
*/
|
|
524
|
-
export function findSessionByIdentity(
|
|
525
|
-
assistantId: string,
|
|
526
|
-
channel: string,
|
|
527
|
-
externalUserId?: string,
|
|
528
|
-
chatId?: string,
|
|
529
|
-
phoneE164?: string,
|
|
530
|
-
): VerificationChallenge | null {
|
|
531
|
-
// Require at least one identity parameter to avoid accidentally matching
|
|
532
|
-
// an unrelated session when the caller has no parsed identity fields.
|
|
533
|
-
if (!externalUserId && !chatId && !phoneE164) {
|
|
534
|
-
return null;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const db = getDb();
|
|
538
|
-
const now = Date.now();
|
|
539
|
-
|
|
540
|
-
const conditions = [
|
|
541
|
-
eq(channelGuardianVerificationChallenges.assistantId, assistantId),
|
|
542
|
-
eq(channelGuardianVerificationChallenges.channel, channel),
|
|
543
|
-
inArray(channelGuardianVerificationChallenges.status, ['pending_bootstrap', 'awaiting_response']),
|
|
544
|
-
gt(channelGuardianVerificationChallenges.expiresAt, now),
|
|
545
|
-
];
|
|
546
|
-
|
|
547
|
-
// Build identity match conditions
|
|
548
|
-
const identityConditions = [];
|
|
549
|
-
if (externalUserId) {
|
|
550
|
-
identityConditions.push(eq(channelGuardianVerificationChallenges.expectedExternalUserId, externalUserId));
|
|
551
|
-
}
|
|
552
|
-
if (chatId) {
|
|
553
|
-
identityConditions.push(eq(channelGuardianVerificationChallenges.expectedChatId, chatId));
|
|
554
|
-
}
|
|
555
|
-
if (phoneE164) {
|
|
556
|
-
identityConditions.push(eq(channelGuardianVerificationChallenges.expectedPhoneE164, phoneE164));
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (identityConditions.length > 0) {
|
|
560
|
-
conditions.push(or(...identityConditions)!);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const row = db
|
|
564
|
-
.select()
|
|
565
|
-
.from(channelGuardianVerificationChallenges)
|
|
566
|
-
.where(and(...conditions))
|
|
567
|
-
.orderBy(desc(channelGuardianVerificationChallenges.createdAt))
|
|
568
|
-
.get();
|
|
569
|
-
|
|
570
|
-
return row ? rowToChallenge(row) : null;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
/**
|
|
574
|
-
* Transition a session's status with optional extra field updates.
|
|
575
|
-
*/
|
|
576
|
-
export function updateSessionStatus(
|
|
577
|
-
id: string,
|
|
578
|
-
status: SessionStatus,
|
|
579
|
-
extraFields?: Partial<{
|
|
580
|
-
consumedByExternalUserId: string;
|
|
581
|
-
consumedByChatId: string;
|
|
582
|
-
}>,
|
|
583
|
-
): void {
|
|
584
|
-
const db = getDb();
|
|
585
|
-
const now = Date.now();
|
|
586
|
-
|
|
587
|
-
db.update(channelGuardianVerificationChallenges)
|
|
588
|
-
.set({
|
|
589
|
-
status,
|
|
590
|
-
updatedAt: now,
|
|
591
|
-
...(extraFields?.consumedByExternalUserId !== undefined
|
|
592
|
-
? { consumedByExternalUserId: extraFields.consumedByExternalUserId }
|
|
593
|
-
: {}),
|
|
594
|
-
...(extraFields?.consumedByChatId !== undefined
|
|
595
|
-
? { consumedByChatId: extraFields.consumedByChatId }
|
|
596
|
-
: {}),
|
|
597
|
-
})
|
|
598
|
-
.where(eq(channelGuardianVerificationChallenges.id, id))
|
|
599
|
-
.run();
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Update outbound delivery tracking fields on a session.
|
|
604
|
-
*/
|
|
605
|
-
export function updateSessionDelivery(
|
|
606
|
-
id: string,
|
|
607
|
-
lastSentAt: number,
|
|
608
|
-
sendCount: number,
|
|
609
|
-
nextResendAt: number | null,
|
|
610
|
-
): void {
|
|
611
|
-
const db = getDb();
|
|
612
|
-
const now = Date.now();
|
|
613
|
-
|
|
614
|
-
db.update(channelGuardianVerificationChallenges)
|
|
615
|
-
.set({
|
|
616
|
-
lastSentAt,
|
|
617
|
-
sendCount,
|
|
618
|
-
nextResendAt,
|
|
619
|
-
updatedAt: now,
|
|
620
|
-
})
|
|
621
|
-
.where(eq(channelGuardianVerificationChallenges.id, id))
|
|
622
|
-
.run();
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Count actual sends to a specific destination across all sessions within a
|
|
627
|
-
* rolling time window. Uses COUNT of rows with a last_sent_at timestamp
|
|
628
|
-
* inside the window rather than SUM(send_count) to avoid double-counting
|
|
629
|
-
* cumulative session counters when resend creates new sessions that carry
|
|
630
|
-
* forward the cumulative count.
|
|
631
|
-
*/
|
|
632
|
-
export function countRecentSendsToDestination(
|
|
633
|
-
channel: string,
|
|
634
|
-
destinationAddress: string,
|
|
635
|
-
windowMs: number,
|
|
636
|
-
): number {
|
|
637
|
-
const db = getDb();
|
|
638
|
-
const cutoff = Date.now() - windowMs;
|
|
639
|
-
|
|
640
|
-
const result = db
|
|
641
|
-
.select({ total: count() })
|
|
642
|
-
.from(channelGuardianVerificationChallenges)
|
|
643
|
-
.where(
|
|
644
|
-
and(
|
|
645
|
-
eq(channelGuardianVerificationChallenges.channel, channel),
|
|
646
|
-
eq(channelGuardianVerificationChallenges.destinationAddress, destinationAddress),
|
|
647
|
-
gte(channelGuardianVerificationChallenges.lastSentAt, cutoff),
|
|
648
|
-
),
|
|
649
|
-
)
|
|
650
|
-
.get();
|
|
651
|
-
|
|
652
|
-
return result?.total ?? 0;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Telegram bootstrap completion: bind the expected identity fields and
|
|
657
|
-
* transition identity_binding_status from pending_bootstrap to bound.
|
|
658
|
-
*/
|
|
659
|
-
export function bindSessionIdentity(
|
|
660
|
-
id: string,
|
|
661
|
-
externalUserId: string,
|
|
662
|
-
chatId: string,
|
|
663
|
-
): void {
|
|
664
|
-
const db = getDb();
|
|
665
|
-
const now = Date.now();
|
|
666
|
-
|
|
667
|
-
db.update(channelGuardianVerificationChallenges)
|
|
668
|
-
.set({
|
|
669
|
-
expectedExternalUserId: externalUserId,
|
|
670
|
-
expectedChatId: chatId,
|
|
671
|
-
identityBindingStatus: 'bound',
|
|
672
|
-
updatedAt: now,
|
|
673
|
-
})
|
|
674
|
-
.where(eq(channelGuardianVerificationChallenges.id, id))
|
|
675
|
-
.run();
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// ---------------------------------------------------------------------------
|
|
679
|
-
// Guardian Approval Requests
|
|
680
|
-
// ---------------------------------------------------------------------------
|
|
681
|
-
|
|
682
|
-
export function createApprovalRequest(params: {
|
|
683
|
-
runId: string;
|
|
684
|
-
requestId?: string;
|
|
685
|
-
conversationId: string;
|
|
686
|
-
assistantId?: string;
|
|
687
|
-
channel: string;
|
|
688
|
-
requesterExternalUserId: string;
|
|
689
|
-
requesterChatId: string;
|
|
690
|
-
guardianExternalUserId: string;
|
|
691
|
-
guardianChatId: string;
|
|
692
|
-
toolName: string;
|
|
693
|
-
riskLevel?: string;
|
|
694
|
-
reason?: string;
|
|
695
|
-
expiresAt: number;
|
|
696
|
-
}): GuardianApprovalRequest {
|
|
697
|
-
const db = getDb();
|
|
698
|
-
const now = Date.now();
|
|
699
|
-
const id = uuid();
|
|
700
|
-
|
|
701
|
-
const row = {
|
|
702
|
-
id,
|
|
703
|
-
runId: params.runId,
|
|
704
|
-
requestId: params.requestId ?? null,
|
|
705
|
-
conversationId: params.conversationId,
|
|
706
|
-
assistantId: params.assistantId ?? 'self',
|
|
707
|
-
channel: params.channel,
|
|
708
|
-
requesterExternalUserId: params.requesterExternalUserId,
|
|
709
|
-
requesterChatId: params.requesterChatId,
|
|
710
|
-
guardianExternalUserId: params.guardianExternalUserId,
|
|
711
|
-
guardianChatId: params.guardianChatId,
|
|
712
|
-
toolName: params.toolName,
|
|
713
|
-
riskLevel: params.riskLevel ?? null,
|
|
714
|
-
reason: params.reason ?? null,
|
|
715
|
-
status: 'pending' as const,
|
|
716
|
-
decidedByExternalUserId: null,
|
|
717
|
-
expiresAt: params.expiresAt,
|
|
718
|
-
createdAt: now,
|
|
719
|
-
updatedAt: now,
|
|
720
|
-
};
|
|
721
|
-
|
|
722
|
-
db.insert(channelGuardianApprovalRequests).values(row).run();
|
|
723
|
-
|
|
724
|
-
return rowToApprovalRequest(row);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
export function getPendingApprovalForRun(runId: string): GuardianApprovalRequest | null {
|
|
728
|
-
const db = getDb();
|
|
729
|
-
const now = Date.now();
|
|
730
|
-
|
|
731
|
-
const row = db
|
|
732
|
-
.select()
|
|
733
|
-
.from(channelGuardianApprovalRequests)
|
|
734
|
-
.where(
|
|
735
|
-
and(
|
|
736
|
-
eq(channelGuardianApprovalRequests.runId, runId),
|
|
737
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
738
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
739
|
-
),
|
|
740
|
-
)
|
|
741
|
-
.get();
|
|
742
|
-
|
|
743
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
export function getPendingApprovalForRequest(requestId: string): GuardianApprovalRequest | null {
|
|
747
|
-
const db = getDb();
|
|
748
|
-
const now = Date.now();
|
|
749
|
-
|
|
750
|
-
const row = db
|
|
751
|
-
.select()
|
|
752
|
-
.from(channelGuardianApprovalRequests)
|
|
753
|
-
.where(
|
|
754
|
-
and(
|
|
755
|
-
eq(channelGuardianApprovalRequests.requestId, requestId),
|
|
756
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
757
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
758
|
-
),
|
|
759
|
-
)
|
|
760
|
-
.get();
|
|
761
|
-
|
|
762
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* Find a pending (status = 'pending') guardian approval request for a run
|
|
767
|
-
* regardless of whether it has expired. Used by the non-guardian gate to
|
|
768
|
-
* detect expired-but-unresolved approvals that should still block the
|
|
769
|
-
* requester from self-approving.
|
|
770
|
-
*/
|
|
771
|
-
export function getUnresolvedApprovalForRun(runId: string): GuardianApprovalRequest | null {
|
|
772
|
-
const db = getDb();
|
|
773
|
-
|
|
774
|
-
const row = db
|
|
775
|
-
.select()
|
|
776
|
-
.from(channelGuardianApprovalRequests)
|
|
777
|
-
.where(
|
|
778
|
-
and(
|
|
779
|
-
eq(channelGuardianApprovalRequests.runId, runId),
|
|
780
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
781
|
-
),
|
|
782
|
-
)
|
|
783
|
-
.get();
|
|
784
|
-
|
|
785
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
export function getUnresolvedApprovalForRequest(requestId: string): GuardianApprovalRequest | null {
|
|
789
|
-
const db = getDb();
|
|
790
|
-
|
|
791
|
-
const row = db
|
|
792
|
-
.select()
|
|
793
|
-
.from(channelGuardianApprovalRequests)
|
|
794
|
-
.where(
|
|
795
|
-
and(
|
|
796
|
-
eq(channelGuardianApprovalRequests.requestId, requestId),
|
|
797
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
798
|
-
),
|
|
799
|
-
)
|
|
800
|
-
.get();
|
|
801
|
-
|
|
802
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
/**
|
|
806
|
-
* Find a pending guardian approval request by the guardian's chat ID.
|
|
807
|
-
* Used when the guardian sends a decision from their chat.
|
|
808
|
-
*
|
|
809
|
-
* When `assistantId` is provided, the lookup is scoped to that assistant,
|
|
810
|
-
* preventing cross-assistant approval consumption in shared guardian chats.
|
|
811
|
-
*/
|
|
812
|
-
export function getPendingApprovalByGuardianChat(
|
|
813
|
-
channel: string,
|
|
814
|
-
guardianChatId: string,
|
|
815
|
-
assistantId?: string,
|
|
816
|
-
): GuardianApprovalRequest | null {
|
|
817
|
-
const db = getDb();
|
|
818
|
-
const now = Date.now();
|
|
819
|
-
|
|
820
|
-
const conditions = [
|
|
821
|
-
eq(channelGuardianApprovalRequests.channel, channel),
|
|
822
|
-
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
823
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
824
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
825
|
-
];
|
|
826
|
-
if (assistantId) {
|
|
827
|
-
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const row = db
|
|
831
|
-
.select()
|
|
832
|
-
.from(channelGuardianApprovalRequests)
|
|
833
|
-
.where(and(...conditions))
|
|
834
|
-
.orderBy(desc(channelGuardianApprovalRequests.createdAt))
|
|
835
|
-
.get();
|
|
836
|
-
|
|
837
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
/**
|
|
841
|
-
* Find a pending guardian approval request scoped to a specific run,
|
|
842
|
-
* guardian chat, and channel. Used when a callback button provides a run ID,
|
|
843
|
-
* so the decision is applied to exactly the right approval even when
|
|
844
|
-
* multiple approvals target the same guardian chat.
|
|
845
|
-
*
|
|
846
|
-
* When `assistantId` is provided, the lookup is further scoped to that
|
|
847
|
-
* assistant to prevent cross-assistant approval consumption.
|
|
848
|
-
*/
|
|
849
|
-
export function getPendingApprovalByRunAndGuardianChat(
|
|
850
|
-
runId: string,
|
|
851
|
-
channel: string,
|
|
852
|
-
guardianChatId: string,
|
|
853
|
-
assistantId?: string,
|
|
854
|
-
): GuardianApprovalRequest | null {
|
|
855
|
-
const db = getDb();
|
|
856
|
-
const now = Date.now();
|
|
857
|
-
|
|
858
|
-
const conditions = [
|
|
859
|
-
eq(channelGuardianApprovalRequests.runId, runId),
|
|
860
|
-
eq(channelGuardianApprovalRequests.channel, channel),
|
|
861
|
-
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
862
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
863
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
864
|
-
];
|
|
865
|
-
if (assistantId) {
|
|
866
|
-
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const row = db
|
|
870
|
-
.select()
|
|
871
|
-
.from(channelGuardianApprovalRequests)
|
|
872
|
-
.where(and(...conditions))
|
|
873
|
-
.get();
|
|
874
|
-
|
|
875
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Find a pending guardian approval request scoped to a specific requestId,
|
|
880
|
-
* guardian chat, and channel. Used when a callback button provides a requestId,
|
|
881
|
-
* so the decision is applied to exactly the right approval even when
|
|
882
|
-
* multiple approvals target the same guardian chat.
|
|
883
|
-
*/
|
|
884
|
-
export function getPendingApprovalByRequestAndGuardianChat(
|
|
885
|
-
requestId: string,
|
|
886
|
-
channel: string,
|
|
887
|
-
guardianChatId: string,
|
|
888
|
-
assistantId?: string,
|
|
889
|
-
): GuardianApprovalRequest | null {
|
|
890
|
-
const db = getDb();
|
|
891
|
-
const now = Date.now();
|
|
892
|
-
|
|
893
|
-
const conditions = [
|
|
894
|
-
eq(channelGuardianApprovalRequests.requestId, requestId),
|
|
895
|
-
eq(channelGuardianApprovalRequests.channel, channel),
|
|
896
|
-
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
897
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
898
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
899
|
-
];
|
|
900
|
-
if (assistantId) {
|
|
901
|
-
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const row = db
|
|
905
|
-
.select()
|
|
906
|
-
.from(channelGuardianApprovalRequests)
|
|
907
|
-
.where(and(...conditions))
|
|
908
|
-
.get();
|
|
909
|
-
|
|
910
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
/**
|
|
914
|
-
* Return all pending (non-expired) guardian approval requests for a given
|
|
915
|
-
* guardian chat and channel. Used to detect ambiguity when a guardian sends
|
|
916
|
-
* a plain-text decision while multiple approvals are pending.
|
|
917
|
-
*
|
|
918
|
-
* When `assistantId` is provided, the results are scoped to that assistant
|
|
919
|
-
* to prevent cross-assistant approval consumption.
|
|
920
|
-
*/
|
|
921
|
-
export function getAllPendingApprovalsByGuardianChat(
|
|
922
|
-
channel: string,
|
|
923
|
-
guardianChatId: string,
|
|
924
|
-
assistantId?: string,
|
|
925
|
-
): GuardianApprovalRequest[] {
|
|
926
|
-
const db = getDb();
|
|
927
|
-
const now = Date.now();
|
|
928
|
-
|
|
929
|
-
const conditions = [
|
|
930
|
-
eq(channelGuardianApprovalRequests.channel, channel),
|
|
931
|
-
eq(channelGuardianApprovalRequests.guardianChatId, guardianChatId),
|
|
932
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
933
|
-
gt(channelGuardianApprovalRequests.expiresAt, now),
|
|
934
|
-
];
|
|
935
|
-
if (assistantId) {
|
|
936
|
-
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
const rows = db
|
|
940
|
-
.select()
|
|
941
|
-
.from(channelGuardianApprovalRequests)
|
|
942
|
-
.where(and(...conditions))
|
|
943
|
-
.orderBy(desc(channelGuardianApprovalRequests.createdAt))
|
|
944
|
-
.all();
|
|
945
|
-
|
|
946
|
-
return rows.map(rowToApprovalRequest);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/**
|
|
950
|
-
* Return all pending approval requests whose expiresAt has passed.
|
|
951
|
-
* Used by the proactive expiry sweep to auto-deny expired approvals
|
|
952
|
-
* without waiting for requester follow-up traffic.
|
|
953
|
-
*/
|
|
954
|
-
export function getExpiredPendingApprovals(): GuardianApprovalRequest[] {
|
|
955
|
-
const db = getDb();
|
|
956
|
-
const now = Date.now();
|
|
957
|
-
|
|
958
|
-
const rows = db
|
|
959
|
-
.select()
|
|
960
|
-
.from(channelGuardianApprovalRequests)
|
|
961
|
-
.where(
|
|
962
|
-
and(
|
|
963
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
964
|
-
lte(channelGuardianApprovalRequests.expiresAt, now),
|
|
965
|
-
),
|
|
966
|
-
)
|
|
967
|
-
.all();
|
|
968
|
-
|
|
969
|
-
return rows.map(rowToApprovalRequest);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
export function updateApprovalDecision(
|
|
973
|
-
id: string,
|
|
974
|
-
decision: { status: ApprovalRequestStatus; decidedByExternalUserId?: string },
|
|
975
|
-
): void {
|
|
976
|
-
const db = getDb();
|
|
977
|
-
const now = Date.now();
|
|
978
|
-
|
|
979
|
-
db.update(channelGuardianApprovalRequests)
|
|
980
|
-
.set({
|
|
981
|
-
status: decision.status,
|
|
982
|
-
decidedByExternalUserId: decision.decidedByExternalUserId ?? null,
|
|
983
|
-
updatedAt: now,
|
|
984
|
-
})
|
|
985
|
-
.where(eq(channelGuardianApprovalRequests.id, id))
|
|
986
|
-
.run();
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// ---------------------------------------------------------------------------
|
|
990
|
-
// Escalation Query Helpers
|
|
991
|
-
// ---------------------------------------------------------------------------
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* List approval requests filtered by assistant, and optionally by channel,
|
|
995
|
-
* conversation, and status. Returns a paginated list of escalations.
|
|
996
|
-
*/
|
|
997
|
-
export function listPendingApprovalRequests(params: {
|
|
998
|
-
assistantId?: string;
|
|
999
|
-
channel?: string;
|
|
1000
|
-
conversationId?: string;
|
|
1001
|
-
status?: ApprovalRequestStatus;
|
|
1002
|
-
limit?: number;
|
|
1003
|
-
offset?: number;
|
|
1004
|
-
}): GuardianApprovalRequest[] {
|
|
1005
|
-
const db = getDb();
|
|
1006
|
-
|
|
1007
|
-
const conditions = [
|
|
1008
|
-
eq(channelGuardianApprovalRequests.assistantId, params.assistantId ?? 'self'),
|
|
1009
|
-
];
|
|
1010
|
-
if (params.channel) {
|
|
1011
|
-
conditions.push(eq(channelGuardianApprovalRequests.channel, params.channel));
|
|
1012
|
-
}
|
|
1013
|
-
if (params.conversationId) {
|
|
1014
|
-
conditions.push(eq(channelGuardianApprovalRequests.conversationId, params.conversationId));
|
|
1015
|
-
}
|
|
1016
|
-
conditions.push(
|
|
1017
|
-
eq(channelGuardianApprovalRequests.status, params.status ?? 'pending'),
|
|
1018
|
-
);
|
|
1019
|
-
|
|
1020
|
-
let query = db
|
|
1021
|
-
.select()
|
|
1022
|
-
.from(channelGuardianApprovalRequests)
|
|
1023
|
-
.where(and(...conditions))
|
|
1024
|
-
.orderBy(desc(channelGuardianApprovalRequests.createdAt));
|
|
1025
|
-
|
|
1026
|
-
if (params.limit !== undefined) {
|
|
1027
|
-
query = query.limit(params.limit) as typeof query;
|
|
1028
|
-
}
|
|
1029
|
-
if (params.offset !== undefined) {
|
|
1030
|
-
query = query.offset(params.offset) as typeof query;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
return query.all().map(rowToApprovalRequest);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
/**
|
|
1037
|
-
* Fetch a single approval request by its primary key.
|
|
1038
|
-
*/
|
|
1039
|
-
export function getApprovalRequestById(id: string): GuardianApprovalRequest | null {
|
|
1040
|
-
const db = getDb();
|
|
1041
|
-
|
|
1042
|
-
const row = db
|
|
1043
|
-
.select()
|
|
1044
|
-
.from(channelGuardianApprovalRequests)
|
|
1045
|
-
.where(eq(channelGuardianApprovalRequests.id, id))
|
|
1046
|
-
.get();
|
|
1047
|
-
|
|
1048
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
/**
|
|
1052
|
-
* Fetch a single approval request by run ID (any status).
|
|
1053
|
-
* Useful for checking whether a run has an associated approval request.
|
|
1054
|
-
*/
|
|
1055
|
-
export function getApprovalRequestByRunId(runId: string): GuardianApprovalRequest | null {
|
|
1056
|
-
const db = getDb();
|
|
1057
|
-
|
|
1058
|
-
const row = db
|
|
1059
|
-
.select()
|
|
1060
|
-
.from(channelGuardianApprovalRequests)
|
|
1061
|
-
.where(eq(channelGuardianApprovalRequests.runId, runId))
|
|
1062
|
-
.orderBy(desc(channelGuardianApprovalRequests.createdAt))
|
|
1063
|
-
.get();
|
|
1064
|
-
|
|
1065
|
-
return row ? rowToApprovalRequest(row) : null;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
/**
|
|
1069
|
-
* Resolve a pending approval request with a decision.
|
|
4
|
+
* The implementation has been split into focused modules:
|
|
5
|
+
* - guardian-bindings.ts — channel binding CRUD
|
|
6
|
+
* - guardian-verification.ts — verification challenge/session management
|
|
7
|
+
* - guardian-approvals.ts — approval request tracking
|
|
8
|
+
* - guardian-rate-limits.ts — verification rate limiting
|
|
1070
9
|
*
|
|
1071
|
-
*
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
eq(channelGuardianApprovalRequests.status, 'pending'),
|
|
1132
|
-
];
|
|
1133
|
-
if (assistantId) {
|
|
1134
|
-
conditions.push(eq(channelGuardianApprovalRequests.assistantId, assistantId));
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
const result = db
|
|
1138
|
-
.select({ count: count() })
|
|
1139
|
-
.from(channelGuardianApprovalRequests)
|
|
1140
|
-
.where(and(...conditions))
|
|
1141
|
-
.get();
|
|
1142
|
-
|
|
1143
|
-
return result?.count ?? 0;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// ---------------------------------------------------------------------------
|
|
1147
|
-
// Verification Rate Limits
|
|
1148
|
-
// ---------------------------------------------------------------------------
|
|
1149
|
-
|
|
1150
|
-
export interface VerificationRateLimit {
|
|
1151
|
-
id: string;
|
|
1152
|
-
assistantId: string;
|
|
1153
|
-
channel: string;
|
|
1154
|
-
actorExternalUserId: string;
|
|
1155
|
-
actorChatId: string;
|
|
1156
|
-
/** Individual attempt timestamps (epoch-ms) within the sliding window. */
|
|
1157
|
-
attemptTimestamps: number[];
|
|
1158
|
-
/** Total stored attempt count (may include expired timestamps; use lockedUntil for enforcement decisions). */
|
|
1159
|
-
invalidAttempts: number;
|
|
1160
|
-
lockedUntil: number | null;
|
|
1161
|
-
createdAt: number;
|
|
1162
|
-
updatedAt: number;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
function parseTimestamps(json: string): number[] {
|
|
1166
|
-
try {
|
|
1167
|
-
const arr = JSON.parse(json);
|
|
1168
|
-
return Array.isArray(arr) ? arr : [];
|
|
1169
|
-
} catch {
|
|
1170
|
-
return [];
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function rowToRateLimit(row: typeof channelGuardianRateLimits.$inferSelect): VerificationRateLimit {
|
|
1175
|
-
const timestamps = parseTimestamps(row.attemptTimestampsJson);
|
|
1176
|
-
return {
|
|
1177
|
-
id: row.id,
|
|
1178
|
-
assistantId: row.assistantId,
|
|
1179
|
-
channel: row.channel,
|
|
1180
|
-
actorExternalUserId: row.actorExternalUserId,
|
|
1181
|
-
actorChatId: row.actorChatId,
|
|
1182
|
-
attemptTimestamps: timestamps,
|
|
1183
|
-
invalidAttempts: timestamps.length,
|
|
1184
|
-
lockedUntil: row.lockedUntil,
|
|
1185
|
-
createdAt: row.createdAt,
|
|
1186
|
-
updatedAt: row.updatedAt,
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
/**
|
|
1191
|
-
* Get the rate-limit record for a given actor on a specific channel.
|
|
1192
|
-
*/
|
|
1193
|
-
export function getRateLimit(
|
|
1194
|
-
assistantId: string,
|
|
1195
|
-
channel: string,
|
|
1196
|
-
actorExternalUserId: string,
|
|
1197
|
-
actorChatId: string,
|
|
1198
|
-
): VerificationRateLimit | null {
|
|
1199
|
-
const db = getDb();
|
|
1200
|
-
const row = db
|
|
1201
|
-
.select()
|
|
1202
|
-
.from(channelGuardianRateLimits)
|
|
1203
|
-
.where(
|
|
1204
|
-
and(
|
|
1205
|
-
eq(channelGuardianRateLimits.assistantId, assistantId),
|
|
1206
|
-
eq(channelGuardianRateLimits.channel, channel),
|
|
1207
|
-
eq(channelGuardianRateLimits.actorExternalUserId, actorExternalUserId),
|
|
1208
|
-
eq(channelGuardianRateLimits.actorChatId, actorChatId),
|
|
1209
|
-
),
|
|
1210
|
-
)
|
|
1211
|
-
.get();
|
|
1212
|
-
|
|
1213
|
-
return row ? rowToRateLimit(row) : null;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
/**
|
|
1217
|
-
* Record an invalid verification attempt using a true sliding window.
|
|
1218
|
-
*
|
|
1219
|
-
* Each individual attempt timestamp is stored; on every new attempt we
|
|
1220
|
-
* discard timestamps older than `windowMs`, append the current one, and
|
|
1221
|
-
* check whether the count exceeds `maxAttempts`. This avoids the
|
|
1222
|
-
* inactivity-timeout pitfall where attempts spaced just under the window
|
|
1223
|
-
* accumulate indefinitely.
|
|
1224
|
-
*/
|
|
1225
|
-
export function recordInvalidAttempt(
|
|
1226
|
-
assistantId: string,
|
|
1227
|
-
channel: string,
|
|
1228
|
-
actorExternalUserId: string,
|
|
1229
|
-
actorChatId: string,
|
|
1230
|
-
windowMs: number,
|
|
1231
|
-
maxAttempts: number,
|
|
1232
|
-
lockoutMs: number,
|
|
1233
|
-
): VerificationRateLimit {
|
|
1234
|
-
const db = getDb();
|
|
1235
|
-
const now = Date.now();
|
|
1236
|
-
const cutoff = now - windowMs;
|
|
1237
|
-
|
|
1238
|
-
const existing = getRateLimit(assistantId, channel, actorExternalUserId, actorChatId);
|
|
1239
|
-
|
|
1240
|
-
if (existing) {
|
|
1241
|
-
// Keep only timestamps within the sliding window, then add the new one
|
|
1242
|
-
const recentTimestamps = existing.attemptTimestamps.filter((ts) => ts > cutoff);
|
|
1243
|
-
recentTimestamps.push(now);
|
|
1244
|
-
|
|
1245
|
-
const newLockedUntil =
|
|
1246
|
-
recentTimestamps.length >= maxAttempts ? now + lockoutMs : existing.lockedUntil;
|
|
1247
|
-
|
|
1248
|
-
const timestampsJson = JSON.stringify(recentTimestamps);
|
|
1249
|
-
|
|
1250
|
-
db.update(channelGuardianRateLimits)
|
|
1251
|
-
.set({
|
|
1252
|
-
attemptTimestampsJson: timestampsJson,
|
|
1253
|
-
lockedUntil: newLockedUntil,
|
|
1254
|
-
updatedAt: now,
|
|
1255
|
-
})
|
|
1256
|
-
.where(eq(channelGuardianRateLimits.id, existing.id))
|
|
1257
|
-
.run();
|
|
1258
|
-
|
|
1259
|
-
return {
|
|
1260
|
-
...existing,
|
|
1261
|
-
attemptTimestamps: recentTimestamps,
|
|
1262
|
-
invalidAttempts: recentTimestamps.length,
|
|
1263
|
-
lockedUntil: newLockedUntil,
|
|
1264
|
-
updatedAt: now,
|
|
1265
|
-
};
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// First attempt — create the row
|
|
1269
|
-
const id = uuid();
|
|
1270
|
-
const timestamps = [now];
|
|
1271
|
-
const lockedUntil = 1 >= maxAttempts ? now + lockoutMs : null;
|
|
1272
|
-
const row = {
|
|
1273
|
-
id,
|
|
1274
|
-
assistantId,
|
|
1275
|
-
channel,
|
|
1276
|
-
actorExternalUserId,
|
|
1277
|
-
actorChatId,
|
|
1278
|
-
// Legacy columns kept for backward compatibility with upgraded databases
|
|
1279
|
-
invalidAttempts: 0,
|
|
1280
|
-
windowStartedAt: 0,
|
|
1281
|
-
attemptTimestampsJson: JSON.stringify(timestamps),
|
|
1282
|
-
lockedUntil,
|
|
1283
|
-
createdAt: now,
|
|
1284
|
-
updatedAt: now,
|
|
1285
|
-
};
|
|
1286
|
-
|
|
1287
|
-
db.insert(channelGuardianRateLimits).values(row).run();
|
|
1288
|
-
|
|
1289
|
-
return rowToRateLimit(row);
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
/**
|
|
1293
|
-
* Reset the rate-limit counter for a given actor (e.g. after a
|
|
1294
|
-
* successful verification).
|
|
1295
|
-
*/
|
|
1296
|
-
export function resetRateLimit(
|
|
1297
|
-
assistantId: string,
|
|
1298
|
-
channel: string,
|
|
1299
|
-
actorExternalUserId: string,
|
|
1300
|
-
actorChatId: string,
|
|
1301
|
-
): void {
|
|
1302
|
-
const db = getDb();
|
|
1303
|
-
const now = Date.now();
|
|
1304
|
-
|
|
1305
|
-
db.update(channelGuardianRateLimits)
|
|
1306
|
-
.set({
|
|
1307
|
-
attemptTimestampsJson: '[]',
|
|
1308
|
-
lockedUntil: null,
|
|
1309
|
-
updatedAt: now,
|
|
1310
|
-
})
|
|
1311
|
-
.where(
|
|
1312
|
-
and(
|
|
1313
|
-
eq(channelGuardianRateLimits.assistantId, assistantId),
|
|
1314
|
-
eq(channelGuardianRateLimits.channel, channel),
|
|
1315
|
-
eq(channelGuardianRateLimits.actorExternalUserId, actorExternalUserId),
|
|
1316
|
-
eq(channelGuardianRateLimits.actorChatId, actorChatId),
|
|
1317
|
-
),
|
|
1318
|
-
)
|
|
1319
|
-
.run();
|
|
1320
|
-
}
|
|
10
|
+
* This file re-exports everything for backward compatibility.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
type BindingStatus,
|
|
15
|
+
type GuardianBinding,
|
|
16
|
+
createBinding,
|
|
17
|
+
getActiveBinding,
|
|
18
|
+
revokeBinding,
|
|
19
|
+
} from './guardian-bindings.js';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
type ChallengeStatus,
|
|
23
|
+
type SessionStatus,
|
|
24
|
+
type IdentityBindingStatus,
|
|
25
|
+
type VerificationPurpose,
|
|
26
|
+
type VerificationChallenge,
|
|
27
|
+
createChallenge,
|
|
28
|
+
revokePendingChallenges,
|
|
29
|
+
findPendingChallengeByHash,
|
|
30
|
+
findPendingChallengeForChannel,
|
|
31
|
+
consumeChallenge,
|
|
32
|
+
createVerificationSession,
|
|
33
|
+
findActiveSession,
|
|
34
|
+
findSessionByBootstrapTokenHash,
|
|
35
|
+
findSessionByIdentity,
|
|
36
|
+
updateSessionStatus,
|
|
37
|
+
updateSessionDelivery,
|
|
38
|
+
countRecentSendsToDestination,
|
|
39
|
+
bindSessionIdentity,
|
|
40
|
+
} from './guardian-verification.js';
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
type ApprovalRequestStatus,
|
|
44
|
+
type GuardianApprovalRequest,
|
|
45
|
+
createApprovalRequest,
|
|
46
|
+
getPendingApprovalForRun,
|
|
47
|
+
getPendingApprovalForRequest,
|
|
48
|
+
getUnresolvedApprovalForRun,
|
|
49
|
+
getUnresolvedApprovalForRequest,
|
|
50
|
+
getPendingApprovalByGuardianChat,
|
|
51
|
+
getPendingApprovalByRunAndGuardianChat,
|
|
52
|
+
getPendingApprovalByRequestAndGuardianChat,
|
|
53
|
+
getAllPendingApprovalsByGuardianChat,
|
|
54
|
+
getExpiredPendingApprovals,
|
|
55
|
+
updateApprovalDecision,
|
|
56
|
+
listPendingApprovalRequests,
|
|
57
|
+
getApprovalRequestById,
|
|
58
|
+
getApprovalRequestByRunId,
|
|
59
|
+
resolveApprovalRequest,
|
|
60
|
+
countPendingByConversation,
|
|
61
|
+
findPendingAccessRequestForRequester,
|
|
62
|
+
} from './guardian-approvals.js';
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
type VerificationRateLimit,
|
|
66
|
+
getRateLimit,
|
|
67
|
+
recordInvalidAttempt,
|
|
68
|
+
resetRateLimit,
|
|
69
|
+
} from './guardian-rate-limits.js';
|