@vellumai/assistant 0.10.3-staging.2 → 0.10.4-staging.1
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/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -37,6 +37,12 @@ const gatewayIpc = {
|
|
|
37
37
|
mirrored: boolean;
|
|
38
38
|
},
|
|
39
39
|
claimThrows: false,
|
|
40
|
+
// When set, contacts_get_rich throws (gateway read unreachable) so the
|
|
41
|
+
// gate-status fallback must fail open.
|
|
42
|
+
richThrows: false,
|
|
43
|
+
// When set, overrides the contacts_get_rich response (e.g. a gateway row
|
|
44
|
+
// under a divergent UUID for the same (type,address)).
|
|
45
|
+
richOverride: null as ((contactId: string | undefined) => unknown) | null,
|
|
40
46
|
// Drives the upsert_verified_channel relay verdict. When false the gateway
|
|
41
47
|
// refuses the actor (blocked/revoked) and the activation is refused.
|
|
42
48
|
activationVerified: true,
|
|
@@ -52,6 +58,13 @@ mock.module("../ipc/gateway-client.js", () => ({
|
|
|
52
58
|
params?: Record<string, unknown>,
|
|
53
59
|
) => {
|
|
54
60
|
gatewayIpc.calls.push({ method, params });
|
|
61
|
+
if (method === "contacts_get_rich") {
|
|
62
|
+
if (gatewayIpc.richThrows) throw new Error("gateway read unreachable");
|
|
63
|
+
if (gatewayIpc.richOverride) {
|
|
64
|
+
return gatewayIpc.richOverride(params?.contactId as string);
|
|
65
|
+
}
|
|
66
|
+
return richContactForId(params?.contactId as string);
|
|
67
|
+
}
|
|
55
68
|
if (method === "record_invite_redemption") {
|
|
56
69
|
if (gatewayIpc.claimThrows) throw new Error("gateway unreachable");
|
|
57
70
|
onGatewayClaim?.();
|
|
@@ -81,6 +94,76 @@ mock.module("../ipc/gateway-client.js", () => ({
|
|
|
81
94
|
},
|
|
82
95
|
}));
|
|
83
96
|
|
|
97
|
+
// Serves contacts_get_rich (the gateway ACL read backing the gate-status
|
|
98
|
+
// fallback) from the seeded local contact, so gate resolution sources status
|
|
99
|
+
// from the gateway path rather than the local channel column.
|
|
100
|
+
function richContactForId(contactId: string | undefined) {
|
|
101
|
+
if (!contactId) return undefined;
|
|
102
|
+
const contact = getContact(contactId);
|
|
103
|
+
if (!contact) return undefined;
|
|
104
|
+
// ACL columns live on the still-present DB rows, not the slimmed interfaces;
|
|
105
|
+
// read them raw to build the gateway-rich response the production read parses.
|
|
106
|
+
const contactRole = (
|
|
107
|
+
getSqlite()
|
|
108
|
+
.query("SELECT role FROM contacts WHERE id = ?")
|
|
109
|
+
.get(contact.id) as { role: string } | undefined
|
|
110
|
+
)?.role;
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
contact: {
|
|
114
|
+
id: contact.id,
|
|
115
|
+
displayName: contact.displayName,
|
|
116
|
+
role: contactRole ?? "contact",
|
|
117
|
+
interactionCount: contact.interactionCount,
|
|
118
|
+
createdAt: contact.createdAt,
|
|
119
|
+
updatedAt: contact.updatedAt,
|
|
120
|
+
channels: contact.channels.map((c) => {
|
|
121
|
+
const acl = rawChannelAcl(c.id);
|
|
122
|
+
return {
|
|
123
|
+
id: c.id,
|
|
124
|
+
contactId: c.contactId,
|
|
125
|
+
type: c.type,
|
|
126
|
+
address: c.address,
|
|
127
|
+
isPrimary: c.isPrimary,
|
|
128
|
+
externalUserId: c.externalChatId,
|
|
129
|
+
status: acl.status,
|
|
130
|
+
policy: acl.policy,
|
|
131
|
+
verifiedAt: acl.verifiedAt,
|
|
132
|
+
verifiedVia: acl.verifiedVia,
|
|
133
|
+
lastSeenAt: acl.lastSeenAt,
|
|
134
|
+
interactionCount: acl.interactionCount,
|
|
135
|
+
lastInteraction: acl.lastInteraction,
|
|
136
|
+
revokedReason: acl.revokedReason,
|
|
137
|
+
blockedReason: acl.blockedReason,
|
|
138
|
+
};
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Read a channel's ACL columns straight off the still-present DB row. */
|
|
145
|
+
function rawChannelAcl(channelId: string) {
|
|
146
|
+
return getSqlite()
|
|
147
|
+
.query(
|
|
148
|
+
`SELECT status, policy, verified_at AS verifiedAt, verified_via AS verifiedVia,
|
|
149
|
+
last_seen_at AS lastSeenAt, interaction_count AS interactionCount,
|
|
150
|
+
last_interaction AS lastInteraction, revoked_reason AS revokedReason,
|
|
151
|
+
blocked_reason AS blockedReason
|
|
152
|
+
FROM contact_channels WHERE id = ?`,
|
|
153
|
+
)
|
|
154
|
+
.get(channelId) as {
|
|
155
|
+
status: string;
|
|
156
|
+
policy: string;
|
|
157
|
+
verifiedAt: number | null;
|
|
158
|
+
verifiedVia: string | null;
|
|
159
|
+
lastSeenAt: number | null;
|
|
160
|
+
interactionCount: number;
|
|
161
|
+
lastInteraction: number | null;
|
|
162
|
+
revokedReason: string | null;
|
|
163
|
+
blockedReason: string | null;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
84
167
|
// Lets a test inject a side-effect into the gateway claim — runs after the
|
|
85
168
|
// service's pre-validation but before the assistant use-bump, so it can race a
|
|
86
169
|
// revoke into the window that makes `recordInviteUse` return false.
|
|
@@ -89,6 +172,8 @@ let onGatewayClaim: (() => void) | null = null;
|
|
|
89
172
|
function resetGatewayIpc() {
|
|
90
173
|
gatewayIpc.claim = { ok: true, updated: true, mirrored: true };
|
|
91
174
|
gatewayIpc.claimThrows = false;
|
|
175
|
+
gatewayIpc.richThrows = false;
|
|
176
|
+
gatewayIpc.richOverride = null;
|
|
92
177
|
gatewayIpc.activationVerified = true;
|
|
93
178
|
gatewayIpc.activationChannelId = "gw-channel-id";
|
|
94
179
|
gatewayIpc.calls = [];
|
|
@@ -102,7 +187,6 @@ import {
|
|
|
102
187
|
getContact,
|
|
103
188
|
upsertContact,
|
|
104
189
|
} from "../contacts/contact-store.js";
|
|
105
|
-
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
106
190
|
import { getSqlite } from "../memory/db-connection.js";
|
|
107
191
|
import { initializeDb } from "../memory/db-init.js";
|
|
108
192
|
import {
|
|
@@ -116,6 +200,7 @@ import {
|
|
|
116
200
|
resolveMemberGateStatus,
|
|
117
201
|
} from "../runtime/invite-redemption-service.js";
|
|
118
202
|
import { hashVoiceCode } from "../util/voice-code.js";
|
|
203
|
+
import { seedContactChannel } from "./helpers/seed-contact-channel.js";
|
|
119
204
|
|
|
120
205
|
await initializeDb();
|
|
121
206
|
|
|
@@ -130,6 +215,15 @@ function createTargetContact(displayName = "Target Contact"): string {
|
|
|
130
215
|
return upsertContact({ displayName, role: "contact" }).id;
|
|
131
216
|
}
|
|
132
217
|
|
|
218
|
+
/** Read a contact's local role column (dropped from the Contact interface). */
|
|
219
|
+
function localContactRole(contactId: string): string | undefined {
|
|
220
|
+
return (
|
|
221
|
+
getSqlite()
|
|
222
|
+
.query("SELECT role FROM contacts WHERE id = ?")
|
|
223
|
+
.get(contactId) as { role: string } | undefined
|
|
224
|
+
)?.role;
|
|
225
|
+
}
|
|
226
|
+
|
|
133
227
|
describe("invite-redemption-service", () => {
|
|
134
228
|
beforeEach(() => {
|
|
135
229
|
resetTables();
|
|
@@ -181,15 +275,16 @@ describe("invite-redemption-service", () => {
|
|
|
181
275
|
|
|
182
276
|
expect(outcome.ok).toBe(true);
|
|
183
277
|
|
|
278
|
+
// The gateway owns the verified ACL verdict (relayed via
|
|
279
|
+
// upsert_verified_channel); the local mirror persists identity only.
|
|
184
280
|
const result = findContactChannel({
|
|
185
281
|
channelType: "telegram",
|
|
186
282
|
address: "user-1",
|
|
187
283
|
});
|
|
188
|
-
|
|
189
284
|
expect(result).not.toBeNull();
|
|
190
|
-
expect(
|
|
191
|
-
|
|
192
|
-
|
|
285
|
+
expect(
|
|
286
|
+
gatewayIpc.calls.some((c) => c.method === "upsert_verified_channel"),
|
|
287
|
+
).toBe(true);
|
|
193
288
|
});
|
|
194
289
|
|
|
195
290
|
test("marks channel as verified via invite on 6-digit code redemption", async () => {
|
|
@@ -210,15 +305,15 @@ describe("invite-redemption-service", () => {
|
|
|
210
305
|
|
|
211
306
|
expect(outcome.ok).toBe(true);
|
|
212
307
|
|
|
308
|
+
// The gateway owns the verified ACL verdict; the local mirror is identity-only.
|
|
213
309
|
const result = findContactChannel({
|
|
214
310
|
channelType: "telegram",
|
|
215
311
|
address: "code-user-1",
|
|
216
312
|
});
|
|
217
|
-
|
|
218
313
|
expect(result).not.toBeNull();
|
|
219
|
-
expect(
|
|
220
|
-
|
|
221
|
-
|
|
314
|
+
expect(
|
|
315
|
+
gatewayIpc.calls.some((c) => c.method === "upsert_verified_channel"),
|
|
316
|
+
).toBe(true);
|
|
222
317
|
});
|
|
223
318
|
|
|
224
319
|
test("returns invalid_token for a bogus token", async () => {
|
|
@@ -329,7 +424,7 @@ describe("invite-redemption-service", () => {
|
|
|
329
424
|
|
|
330
425
|
test("returns already_member when user is already an active member", async () => {
|
|
331
426
|
// Pre-create an active member and find their contact
|
|
332
|
-
const member =
|
|
427
|
+
const member = seedContactChannel({
|
|
333
428
|
sourceChannel: "telegram",
|
|
334
429
|
externalUserId: "existing-user",
|
|
335
430
|
status: "active",
|
|
@@ -338,7 +433,7 @@ describe("invite-redemption-service", () => {
|
|
|
338
433
|
// Create an invite targeting the same contact that owns the channel
|
|
339
434
|
const { rawToken } = createInvite({
|
|
340
435
|
sourceChannel: "telegram",
|
|
341
|
-
contactId: member
|
|
436
|
+
contactId: member.contactId,
|
|
342
437
|
maxUses: 5,
|
|
343
438
|
});
|
|
344
439
|
|
|
@@ -361,7 +456,7 @@ describe("invite-redemption-service", () => {
|
|
|
361
456
|
|
|
362
457
|
test("returns invalid_token for a blocked member to avoid leaking membership status", async () => {
|
|
363
458
|
// Pre-create a blocked member and find their contact
|
|
364
|
-
const member =
|
|
459
|
+
const member = seedContactChannel({
|
|
365
460
|
sourceChannel: "telegram",
|
|
366
461
|
externalUserId: "blocked-user",
|
|
367
462
|
status: "blocked",
|
|
@@ -370,7 +465,7 @@ describe("invite-redemption-service", () => {
|
|
|
370
465
|
// Create an invite targeting the same contact that owns the channel
|
|
371
466
|
const { rawToken } = createInvite({
|
|
372
467
|
sourceChannel: "telegram",
|
|
373
|
-
contactId: member
|
|
468
|
+
contactId: member.contactId,
|
|
374
469
|
maxUses: 5,
|
|
375
470
|
});
|
|
376
471
|
|
|
@@ -383,18 +478,150 @@ describe("invite-redemption-service", () => {
|
|
|
383
478
|
expect(outcome).toEqual({ ok: false, reason: "invalid_token" });
|
|
384
479
|
});
|
|
385
480
|
|
|
481
|
+
test("matches an active member by (type,address) when the gateway row has a divergent uuid", async () => {
|
|
482
|
+
const member = seedContactChannel({
|
|
483
|
+
sourceChannel: "telegram",
|
|
484
|
+
externalUserId: "divergent-user",
|
|
485
|
+
status: "active",
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// The gateway row for the same (type,address) carries a DIFFERENT id, as a
|
|
489
|
+
// reconcile divergence would produce. Matching by id alone would miss it.
|
|
490
|
+
gatewayIpc.richOverride = () => ({
|
|
491
|
+
ok: true,
|
|
492
|
+
contact: {
|
|
493
|
+
id: member.contactId,
|
|
494
|
+
displayName: "divergent-user",
|
|
495
|
+
role: "contact",
|
|
496
|
+
interactionCount: 0,
|
|
497
|
+
createdAt: 1,
|
|
498
|
+
updatedAt: 1,
|
|
499
|
+
channels: [
|
|
500
|
+
{
|
|
501
|
+
id: "gateway-divergent-uuid",
|
|
502
|
+
contactId: member.contactId,
|
|
503
|
+
type: "telegram",
|
|
504
|
+
address: "divergent-user",
|
|
505
|
+
isPrimary: false,
|
|
506
|
+
externalUserId: null,
|
|
507
|
+
status: "active",
|
|
508
|
+
policy: "allow",
|
|
509
|
+
verifiedAt: 1,
|
|
510
|
+
verifiedVia: "invite",
|
|
511
|
+
lastSeenAt: null,
|
|
512
|
+
interactionCount: 0,
|
|
513
|
+
lastInteraction: null,
|
|
514
|
+
revokedReason: null,
|
|
515
|
+
blockedReason: null,
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const { rawToken } = createInvite({
|
|
522
|
+
sourceChannel: "telegram",
|
|
523
|
+
contactId: member.contactId,
|
|
524
|
+
maxUses: 5,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const outcome = await redeemInvite({
|
|
528
|
+
rawToken,
|
|
529
|
+
sourceChannel: "telegram",
|
|
530
|
+
externalUserId: "divergent-user",
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
expect(outcome.ok).toBe(true);
|
|
534
|
+
expect((outcome as { type: string }).type).toBe("already_member");
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("blocks via the (type,address) match when the gateway row has a divergent uuid", async () => {
|
|
538
|
+
const member = seedContactChannel({
|
|
539
|
+
sourceChannel: "telegram",
|
|
540
|
+
externalUserId: "divergent-blocked",
|
|
541
|
+
status: "blocked",
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
gatewayIpc.richOverride = () => ({
|
|
545
|
+
ok: true,
|
|
546
|
+
contact: {
|
|
547
|
+
id: member.contactId,
|
|
548
|
+
displayName: "divergent-blocked",
|
|
549
|
+
role: "contact",
|
|
550
|
+
interactionCount: 0,
|
|
551
|
+
createdAt: 1,
|
|
552
|
+
updatedAt: 1,
|
|
553
|
+
channels: [
|
|
554
|
+
{
|
|
555
|
+
id: "gateway-divergent-blocked-uuid",
|
|
556
|
+
contactId: member.contactId,
|
|
557
|
+
type: "telegram",
|
|
558
|
+
// Case-divergent address must still match (COLLATE NOCASE).
|
|
559
|
+
address: "DIVERGENT-BLOCKED",
|
|
560
|
+
isPrimary: false,
|
|
561
|
+
externalUserId: null,
|
|
562
|
+
status: "blocked",
|
|
563
|
+
policy: "deny",
|
|
564
|
+
verifiedAt: null,
|
|
565
|
+
verifiedVia: null,
|
|
566
|
+
lastSeenAt: null,
|
|
567
|
+
interactionCount: 0,
|
|
568
|
+
lastInteraction: null,
|
|
569
|
+
revokedReason: null,
|
|
570
|
+
blockedReason: "guardian blocked",
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
const { rawToken } = createInvite({
|
|
577
|
+
sourceChannel: "telegram",
|
|
578
|
+
contactId: member.contactId,
|
|
579
|
+
maxUses: 5,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const outcome = await redeemInvite({
|
|
583
|
+
rawToken,
|
|
584
|
+
sourceChannel: "telegram",
|
|
585
|
+
externalUserId: "divergent-blocked",
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
expect(outcome).toEqual({ ok: false, reason: "invalid_token" });
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("fails open (no throw) when the gateway gate-status read is unreachable", async () => {
|
|
592
|
+
// No verdict member and an unreachable gateway read must degrade to the
|
|
593
|
+
// fail-open path: redemption still resolves rather than throwing.
|
|
594
|
+
const member = seedContactChannel({
|
|
595
|
+
sourceChannel: "telegram",
|
|
596
|
+
externalUserId: "readfail-user",
|
|
597
|
+
status: "revoked",
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
gatewayIpc.richThrows = true;
|
|
601
|
+
|
|
602
|
+
const { rawToken } = createInvite({
|
|
603
|
+
sourceChannel: "telegram",
|
|
604
|
+
contactId: member.contactId,
|
|
605
|
+
maxUses: 1,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
const outcome = await redeemInvite({
|
|
609
|
+
rawToken,
|
|
610
|
+
sourceChannel: "telegram",
|
|
611
|
+
externalUserId: "readfail-user",
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
expect(outcome.ok).toBe(true);
|
|
615
|
+
});
|
|
616
|
+
|
|
386
617
|
test("binds redeemer to the invite's target contact, not the guardian", async () => {
|
|
387
618
|
// Pre-create a guardian contact with a revoked telegram channel
|
|
388
|
-
const
|
|
619
|
+
const guardianSeed = seedContactChannel({
|
|
620
|
+
sourceChannel: "telegram",
|
|
621
|
+
externalUserId: "guardian-tg-id",
|
|
389
622
|
displayName: "Guardian",
|
|
390
623
|
role: "guardian",
|
|
391
|
-
|
|
392
|
-
{
|
|
393
|
-
type: "telegram",
|
|
394
|
-
address: "guardian-tg-id",
|
|
395
|
-
status: "revoked",
|
|
396
|
-
},
|
|
397
|
-
],
|
|
624
|
+
status: "revoked",
|
|
398
625
|
});
|
|
399
626
|
|
|
400
627
|
// Create a separate target contact "Mom"
|
|
@@ -440,32 +667,27 @@ describe("invite-redemption-service", () => {
|
|
|
440
667
|
});
|
|
441
668
|
expect(result).not.toBeNull();
|
|
442
669
|
expect(result!.contact.id).toBe(momContact.id);
|
|
443
|
-
expect(result!.channel.status).toBe("active");
|
|
444
670
|
|
|
445
671
|
// Verify the original guardian contact was NOT modified
|
|
446
|
-
const guardian = getContact(
|
|
672
|
+
const guardian = getContact(guardianSeed.contactId);
|
|
447
673
|
expect(guardian).not.toBeNull();
|
|
448
|
-
expect(
|
|
674
|
+
expect(localContactRole(guardianSeed.contactId)).toBe("guardian");
|
|
449
675
|
});
|
|
450
676
|
|
|
451
677
|
test("downgrades guardian to contact when redeeming invite targeting own contact", async () => {
|
|
452
678
|
// Create a guardian contact with a revoked channel
|
|
453
|
-
const
|
|
679
|
+
const guardianSeed = seedContactChannel({
|
|
680
|
+
sourceChannel: "telegram",
|
|
681
|
+
externalUserId: "guardian-own-id",
|
|
454
682
|
displayName: "Guardian",
|
|
455
683
|
role: "guardian",
|
|
456
|
-
|
|
457
|
-
{
|
|
458
|
-
type: "telegram",
|
|
459
|
-
address: "guardian-own-id",
|
|
460
|
-
status: "revoked",
|
|
461
|
-
},
|
|
462
|
-
],
|
|
684
|
+
status: "revoked",
|
|
463
685
|
});
|
|
464
686
|
|
|
465
687
|
// Create invite targeting the guardian's own contact
|
|
466
688
|
const { rawToken } = createInvite({
|
|
467
689
|
sourceChannel: "telegram",
|
|
468
|
-
contactId:
|
|
690
|
+
contactId: guardianSeed.contactId,
|
|
469
691
|
maxUses: 5,
|
|
470
692
|
});
|
|
471
693
|
|
|
@@ -477,23 +699,23 @@ describe("invite-redemption-service", () => {
|
|
|
477
699
|
|
|
478
700
|
expect(outcome.ok).toBe(true);
|
|
479
701
|
|
|
480
|
-
// The
|
|
481
|
-
|
|
482
|
-
expect(
|
|
702
|
+
// The role downgrade is gateway-owned; redemption relays the activation for
|
|
703
|
+
// the guardian's own contact rather than mutating the local role.
|
|
704
|
+
expect(
|
|
705
|
+
gatewayIpc.calls.some((c) => c.method === "upsert_verified_channel"),
|
|
706
|
+
).toBe(true);
|
|
707
|
+
const updated = getContact(guardianSeed.contactId);
|
|
708
|
+
expect(updated).not.toBeNull();
|
|
483
709
|
});
|
|
484
710
|
|
|
485
711
|
test("binds redeemer to the invite's target contact via 6-digit code, not the guardian", async () => {
|
|
486
712
|
// Pre-create a guardian contact with a revoked telegram channel
|
|
487
|
-
const
|
|
713
|
+
const guardianSeed = seedContactChannel({
|
|
714
|
+
sourceChannel: "telegram",
|
|
715
|
+
externalUserId: "guardian-code-id",
|
|
488
716
|
displayName: "Guardian",
|
|
489
717
|
role: "guardian",
|
|
490
|
-
|
|
491
|
-
{
|
|
492
|
-
type: "telegram",
|
|
493
|
-
address: "guardian-code-id",
|
|
494
|
-
status: "revoked",
|
|
495
|
-
},
|
|
496
|
-
],
|
|
718
|
+
status: "revoked",
|
|
497
719
|
});
|
|
498
720
|
|
|
499
721
|
// Create a separate target contact "Mom"
|
|
@@ -530,12 +752,11 @@ describe("invite-redemption-service", () => {
|
|
|
530
752
|
});
|
|
531
753
|
expect(result).not.toBeNull();
|
|
532
754
|
expect(result!.contact.id).toBe(momContact.id);
|
|
533
|
-
expect(result!.channel.status).toBe("active");
|
|
534
755
|
|
|
535
756
|
// Verify the original guardian contact was NOT modified
|
|
536
|
-
const guardian = getContact(
|
|
757
|
+
const guardian = getContact(guardianSeed.contactId);
|
|
537
758
|
expect(guardian).not.toBeNull();
|
|
538
|
-
expect(
|
|
759
|
+
expect(localContactRole(guardianSeed.contactId)).toBe("guardian");
|
|
539
760
|
});
|
|
540
761
|
|
|
541
762
|
test("does not return already_member for a revoked member", async () => {
|
|
@@ -547,12 +768,11 @@ describe("invite-redemption-service", () => {
|
|
|
547
768
|
});
|
|
548
769
|
|
|
549
770
|
// Pre-create a revoked member
|
|
550
|
-
|
|
771
|
+
seedContactChannel({
|
|
551
772
|
sourceChannel: "telegram",
|
|
552
773
|
externalUserId: "revoked-user",
|
|
553
774
|
status: "revoked",
|
|
554
775
|
});
|
|
555
|
-
expect(member!.channel.status).toBe("revoked");
|
|
556
776
|
|
|
557
777
|
const outcome = await redeemInvite({
|
|
558
778
|
rawToken,
|
|
@@ -605,7 +825,7 @@ describe("invite-redemption-service", () => {
|
|
|
605
825
|
|
|
606
826
|
test("returns invalid_token for an active member with a bogus token (no membership probing)", async () => {
|
|
607
827
|
// Pre-create an active member
|
|
608
|
-
|
|
828
|
+
seedContactChannel({
|
|
609
829
|
sourceChannel: "telegram",
|
|
610
830
|
externalUserId: "probed-user",
|
|
611
831
|
status: "active",
|
|
@@ -632,7 +852,7 @@ describe("invite-redemption-service", () => {
|
|
|
632
852
|
});
|
|
633
853
|
|
|
634
854
|
// Pre-create an active member
|
|
635
|
-
|
|
855
|
+
seedContactChannel({
|
|
636
856
|
sourceChannel: "telegram",
|
|
637
857
|
externalUserId: "expired-token-user",
|
|
638
858
|
status: "active",
|
|
@@ -658,7 +878,7 @@ describe("invite-redemption-service", () => {
|
|
|
658
878
|
});
|
|
659
879
|
|
|
660
880
|
// Pre-create an active member on telegram
|
|
661
|
-
|
|
881
|
+
seedContactChannel({
|
|
662
882
|
sourceChannel: "telegram",
|
|
663
883
|
externalUserId: "cross-channel-user",
|
|
664
884
|
status: "active",
|
|
@@ -821,7 +1041,7 @@ describe("invite-redemption-service", () => {
|
|
|
821
1041
|
});
|
|
822
1042
|
|
|
823
1043
|
// Seed an already-active member bound to the invite's target contact.
|
|
824
|
-
|
|
1044
|
+
seedContactChannel({
|
|
825
1045
|
sourceChannel: "telegram",
|
|
826
1046
|
externalUserId: "already-member-user",
|
|
827
1047
|
role: "contact",
|
|
@@ -34,6 +34,40 @@ mock.module("../calls/call-domain.js", () => ({
|
|
|
34
34
|
startInviteCall: async () => mockStartInviteCallResult,
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
|
+
// Model the gateway: the redemption claim (record_invite_redemption) and the
|
|
38
|
+
// gateway-owned activation (upsert_verified_channel) are both relayed. The
|
|
39
|
+
// activation write fails closed in production, so the mock must serve a
|
|
40
|
+
// verified upsert for the legitimate-success redemption paths.
|
|
41
|
+
const gatewayIpc = {
|
|
42
|
+
claim: { ok: true, updated: true, mirrored: true },
|
|
43
|
+
activationVerified: true,
|
|
44
|
+
};
|
|
45
|
+
mock.module("../ipc/gateway-client.js", () => ({
|
|
46
|
+
ipcCallPersistent: async (
|
|
47
|
+
method: string,
|
|
48
|
+
params?: Record<string, unknown>,
|
|
49
|
+
) => {
|
|
50
|
+
if (method === "record_invite_redemption") return gatewayIpc.claim;
|
|
51
|
+
if (method === "upsert_verified_channel") {
|
|
52
|
+
if (!gatewayIpc.activationVerified) return { ok: true, verified: false };
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
verified: true,
|
|
56
|
+
channel: {
|
|
57
|
+
id: "gw-channel-id",
|
|
58
|
+
contactId: (params?.contactId as string) ?? "gw-contact",
|
|
59
|
+
type: (params?.type as string) ?? "telegram",
|
|
60
|
+
address: (params?.address as string) ?? "gw-addr",
|
|
61
|
+
status: "active",
|
|
62
|
+
verifiedAt: 1,
|
|
63
|
+
verifiedVia: (params?.verifiedVia as string) ?? "invite",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
70
|
+
|
|
37
71
|
import { upsertContact } from "../contacts/contact-store.js";
|
|
38
72
|
import { getSqlite } from "../memory/db-connection.js";
|
|
39
73
|
import { initializeDb } from "../memory/db-init.js";
|
|
@@ -19,15 +19,78 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
19
19
|
// mutating. Default the claim to consumed (updated:true) so these assistant-side
|
|
20
20
|
// handler tests exercise the happy redemption path.
|
|
21
21
|
mock.module("../ipc/gateway-client.js", () => ({
|
|
22
|
-
ipcCallPersistent: async (
|
|
22
|
+
ipcCallPersistent: async (
|
|
23
|
+
method: string,
|
|
24
|
+
params?: Record<string, unknown>,
|
|
25
|
+
) => {
|
|
26
|
+
if (method === "contacts_get_rich") {
|
|
27
|
+
return richContactForId(params?.contactId as string);
|
|
28
|
+
}
|
|
23
29
|
if (method === "record_invite_redemption") {
|
|
24
30
|
return { ok: true, updated: true, mirrored: true };
|
|
25
31
|
}
|
|
32
|
+
if (method === "upsert_verified_channel") {
|
|
33
|
+
// Gateway-as-SoT activation: return a verified channel so the gateway-first
|
|
34
|
+
// relay lands its write before mirroring identity locally.
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
verified: true,
|
|
38
|
+
channel: {
|
|
39
|
+
id: "gw-channel-1",
|
|
40
|
+
contactId: (params?.contactId as string) ?? "gw-contact-1",
|
|
41
|
+
type: (params?.type as string) ?? "telegram",
|
|
42
|
+
address: (params?.address as string) ?? "",
|
|
43
|
+
status: "active",
|
|
44
|
+
verifiedAt: Date.now(),
|
|
45
|
+
verifiedVia: (params?.verifiedVia as string) ?? "invite",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
26
49
|
return undefined;
|
|
27
50
|
},
|
|
28
51
|
}));
|
|
29
52
|
|
|
30
|
-
|
|
53
|
+
// Serves contacts_get_rich (the gateway ACL read backing the gate-status
|
|
54
|
+
// fallback) from the seeded local contact identity. Channel ACL state is
|
|
55
|
+
// gateway-owned, so a contact with a mirrored channel reports "active" here —
|
|
56
|
+
// the local status column is drained and never consulted.
|
|
57
|
+
function richContactForId(contactId: string | undefined) {
|
|
58
|
+
if (!contactId) return undefined;
|
|
59
|
+
const contact = getContact(contactId);
|
|
60
|
+
if (!contact) return undefined;
|
|
61
|
+
// ACL columns are gateway-owned; the projection reports "active" and no longer
|
|
62
|
+
// mirrors the drained local ACL fields off the typed contact/channel.
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
contact: {
|
|
66
|
+
id: contact.id,
|
|
67
|
+
displayName: contact.displayName,
|
|
68
|
+
role: "contact",
|
|
69
|
+
interactionCount: contact.interactionCount,
|
|
70
|
+
createdAt: contact.createdAt,
|
|
71
|
+
updatedAt: contact.updatedAt,
|
|
72
|
+
channels: contact.channels.map((c) => ({
|
|
73
|
+
id: c.id,
|
|
74
|
+
contactId: c.contactId,
|
|
75
|
+
type: c.type,
|
|
76
|
+
address: c.address,
|
|
77
|
+
isPrimary: c.isPrimary,
|
|
78
|
+
externalUserId: c.externalChatId,
|
|
79
|
+
status: "active",
|
|
80
|
+
policy: "allow",
|
|
81
|
+
verifiedAt: null,
|
|
82
|
+
verifiedVia: null,
|
|
83
|
+
lastSeenAt: null,
|
|
84
|
+
interactionCount: 0,
|
|
85
|
+
lastInteraction: null,
|
|
86
|
+
revokedReason: null,
|
|
87
|
+
blockedReason: null,
|
|
88
|
+
})),
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
import { getContact, upsertContact } from "../contacts/contact-store.js";
|
|
31
94
|
import { handleMintInvite } from "../ipc/routes/invite-ipc-routes.js";
|
|
32
95
|
import { getSqlite } from "../memory/db-connection.js";
|
|
33
96
|
import { initializeDb } from "../memory/db-init.js";
|