@vellumai/assistant 0.5.14 → 0.5.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.
Files changed (175) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/integrations.md +15 -14
  3. package/knip.json +3 -1
  4. package/openapi.yaml +11 -43
  5. package/package.json +1 -1
  6. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
  7. package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
  8. package/src/__tests__/checker.test.ts +59 -0
  9. package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
  10. package/src/__tests__/cli-memory.test.ts +372 -0
  11. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
  12. package/src/__tests__/config-schema.test.ts +0 -2
  13. package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
  15. package/src/__tests__/conversation-slash-commands.test.ts +2 -6
  16. package/src/__tests__/conversation-usage.test.ts +1 -0
  17. package/src/__tests__/credential-security-e2e.test.ts +4 -1
  18. package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
  20. package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
  21. package/src/__tests__/heartbeat-service.test.ts +1 -3
  22. package/src/__tests__/intent-routing.test.ts +6 -18
  23. package/src/__tests__/log-export-workspace.test.ts +2 -28
  24. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
  25. package/src/__tests__/managed-store.test.ts +2 -10
  26. package/src/__tests__/messaging-send-tool.test.ts +6 -6
  27. package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
  28. package/src/__tests__/migration-export-http.test.ts +3 -34
  29. package/src/__tests__/migration-import-commit-http.test.ts +1 -29
  30. package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
  31. package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
  32. package/src/__tests__/oauth-apps-routes.test.ts +120 -10
  33. package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
  34. package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
  35. package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
  36. package/src/__tests__/oauth-providers-routes.test.ts +5 -2
  37. package/src/__tests__/oauth-store.test.ts +0 -5
  38. package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
  39. package/src/__tests__/path-policy.test.ts +2 -17
  40. package/src/__tests__/permission-types.test.ts +0 -1
  41. package/src/__tests__/platform-callback-registration.test.ts +3 -7
  42. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  43. package/src/__tests__/provider-error-scenarios.test.ts +0 -2
  44. package/src/__tests__/qdrant-manager.test.ts +68 -21
  45. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  46. package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
  47. package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
  48. package/src/__tests__/secret-allowlist.test.ts +20 -35
  49. package/src/__tests__/shell-credential-ref.test.ts +0 -5
  50. package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
  51. package/src/__tests__/skill-load-inline-command.test.ts +3 -65
  52. package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
  53. package/src/__tests__/skill-load-tool.test.ts +3 -67
  54. package/src/__tests__/skill-memory.test.ts +362 -119
  55. package/src/__tests__/skills.test.ts +22 -49
  56. package/src/__tests__/slack-channel-config.test.ts +2 -21
  57. package/src/__tests__/starter-bundle.test.ts +2 -8
  58. package/src/__tests__/stt-hints.test.ts +7 -2
  59. package/src/__tests__/system-prompt.test.ts +25 -45
  60. package/src/__tests__/task-compiler.test.ts +0 -21
  61. package/src/__tests__/task-management-tools.test.ts +0 -21
  62. package/src/__tests__/task-memory-cleanup.test.ts +0 -21
  63. package/src/__tests__/task-runner.test.ts +0 -21
  64. package/src/__tests__/task-scheduler.test.ts +0 -21
  65. package/src/__tests__/terminal-tools.test.ts +1 -17
  66. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
  67. package/src/__tests__/tool-approval-handler.test.ts +1 -20
  68. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
  69. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
  70. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  71. package/src/__tests__/tool-executor.test.ts +0 -1
  72. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
  73. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
  74. package/src/__tests__/trust-store.test.ts +9 -41
  75. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
  76. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
  77. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
  78. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
  79. package/src/__tests__/trusted-contact-verification.test.ts +0 -22
  80. package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
  81. package/src/__tests__/twilio-provider.test.ts +0 -16
  82. package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
  83. package/src/__tests__/twilio-routes.test.ts +0 -24
  84. package/src/__tests__/update-bulletin.test.ts +17 -89
  85. package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
  86. package/src/__tests__/usage-routes.test.ts +0 -21
  87. package/src/__tests__/user-reference.test.ts +1 -5
  88. package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
  89. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
  90. package/src/__tests__/voice-invite-redemption.test.ts +0 -21
  91. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
  92. package/src/__tests__/voice-session-bridge.test.ts +0 -21
  93. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
  94. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
  96. package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
  97. package/src/acp/client-handler.ts +1 -2
  98. package/src/cli/__tests__/notifications.test.ts +0 -22
  99. package/src/cli/cli-memory.ts +176 -0
  100. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  101. package/src/cli/commands/oauth/connect.ts +15 -0
  102. package/src/cli/commands/oauth/providers.ts +49 -42
  103. package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
  104. package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
  105. package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
  106. package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
  107. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  108. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  109. package/src/config/feature-flag-registry.json +16 -0
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/security.ts +0 -6
  112. package/src/config/schemas/services.ts +8 -0
  113. package/src/context/window-manager.ts +28 -9
  114. package/src/credential-execution/approval-bridge.ts +0 -1
  115. package/src/daemon/config-watcher.ts +51 -0
  116. package/src/daemon/conversation-agent-loop.ts +3 -2
  117. package/src/daemon/conversation-process.ts +1 -0
  118. package/src/daemon/conversation-usage.ts +1 -0
  119. package/src/daemon/handlers/skills.ts +9 -1
  120. package/src/daemon/lifecycle.ts +13 -4
  121. package/src/daemon/message-types/conversations.ts +1 -0
  122. package/src/daemon/providers-setup.ts +2 -0
  123. package/src/daemon/server.ts +26 -22
  124. package/src/events/domain-events.ts +1 -2
  125. package/src/memory/db-init.ts +9 -0
  126. package/src/memory/job-handlers/batch-extraction.ts +16 -4
  127. package/src/memory/job-handlers/embedding.test.ts +3 -27
  128. package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
  129. package/src/memory/llm-usage-store.ts +35 -2
  130. package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
  131. package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
  132. package/src/memory/migrations/index.ts +2 -0
  133. package/src/memory/qdrant-manager.ts +26 -5
  134. package/src/memory/query-expansion.ts +1 -1
  135. package/src/memory/retriever.test.ts +22 -20
  136. package/src/memory/retriever.ts +10 -2
  137. package/src/memory/schema/oauth.ts +1 -1
  138. package/src/memory/search/mmr.ts +8 -5
  139. package/src/memory/slack-thread-store.ts +17 -0
  140. package/src/messaging/providers/outlook/adapter.ts +193 -0
  141. package/src/messaging/providers/outlook/client.ts +311 -0
  142. package/src/messaging/providers/outlook/types.ts +83 -0
  143. package/src/notifications/adapters/slack.ts +1 -1
  144. package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
  145. package/src/oauth/connect-orchestrator.ts +10 -3
  146. package/src/oauth/oauth-store.ts +10 -11
  147. package/src/oauth/provider-serializer.ts +3 -0
  148. package/src/oauth/provider-visibility.ts +16 -0
  149. package/src/oauth/seed-providers.ts +49 -17
  150. package/src/permissions/checker.ts +39 -7
  151. package/src/permissions/types.ts +2 -4
  152. package/src/prompts/journal-context.ts +9 -11
  153. package/src/prompts/system-prompt.ts +3 -64
  154. package/src/prompts/templates/UPDATES.md +6 -0
  155. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
  156. package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
  157. package/src/runtime/auth/route-policy.ts +0 -4
  158. package/src/runtime/guardian-reply-router.ts +6 -2
  159. package/src/runtime/routes/conversation-query-routes.ts +2 -58
  160. package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
  161. package/src/runtime/routes/memory-item-routes.test.ts +0 -17
  162. package/src/runtime/routes/memory-item-routes.ts +103 -12
  163. package/src/runtime/routes/oauth-apps.ts +18 -1
  164. package/src/runtime/routes/oauth-providers.ts +13 -1
  165. package/src/runtime/routes/settings-routes.ts +1 -0
  166. package/src/runtime/routes/usage-routes.ts +19 -2
  167. package/src/runtime/routes/work-items-routes.test.ts +0 -21
  168. package/src/runtime/routes/workspace-routes.test.ts +3 -27
  169. package/src/security/secret-allowlist.ts +4 -4
  170. package/src/skills/skill-memory.ts +62 -23
  171. package/src/tools/memory/handlers.test.ts +1 -29
  172. package/src/tools/permission-checker.ts +0 -18
  173. package/src/tools/skills/skill-script-runner.ts +1 -1
  174. package/src/util/device-id.ts +3 -65
  175. package/src/workspace/git-service.ts +27 -6
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Outlook messaging provider adapter.
3
+ *
4
+ * Maps Microsoft Graph API responses to the platform-agnostic messaging types
5
+ * and implements the MessagingProvider interface.
6
+ */
7
+
8
+ import type { OAuthConnection } from "../../../oauth/connection.js";
9
+ import type { MessagingProvider } from "../../provider.js";
10
+ import type {
11
+ ConnectionInfo,
12
+ Conversation,
13
+ HistoryOptions,
14
+ ListOptions,
15
+ Message,
16
+ SearchOptions,
17
+ SearchResult,
18
+ SendOptions,
19
+ SendResult,
20
+ } from "../../provider-types.js";
21
+ import * as outlook from "./client.js";
22
+ import type { OutlookMessage } from "./types.js";
23
+
24
+ function requireConnection(
25
+ connection: OAuthConnection | undefined,
26
+ ): OAuthConnection {
27
+ if (!connection) {
28
+ throw new Error(
29
+ "Outlook requires an OAuth connection — is the account connected?",
30
+ );
31
+ }
32
+ return connection;
33
+ }
34
+
35
+ function mapOutlookMessage(msg: OutlookMessage): Message {
36
+ const senderEmail = msg.from?.emailAddress?.address ?? "";
37
+ const senderName = msg.from?.emailAddress?.name || senderEmail || "Unknown";
38
+
39
+ return {
40
+ id: msg.id,
41
+ conversationId: msg.conversationId,
42
+ sender: {
43
+ id: senderEmail,
44
+ name: senderName,
45
+ email: senderEmail,
46
+ },
47
+ text: msg.body.contentType === "text" ? msg.body.content : msg.bodyPreview,
48
+ timestamp: new Date(msg.receivedDateTime).getTime(),
49
+ threadId: msg.conversationId,
50
+ platform: "outlook",
51
+ hasAttachments: msg.hasAttachments ?? false,
52
+ metadata: {
53
+ subject: msg.subject,
54
+ categories: msg.categories,
55
+ isRead: msg.isRead,
56
+ parentFolderId: msg.parentFolderId,
57
+ },
58
+ };
59
+ }
60
+
61
+ const MESSAGE_SELECT_FIELDS =
62
+ "id,conversationId,subject,bodyPreview,body,from,toRecipients,receivedDateTime,isRead,hasAttachments,parentFolderId,categories,flag";
63
+
64
+ export const outlookMessagingProvider: MessagingProvider = {
65
+ id: "outlook",
66
+ displayName: "Outlook",
67
+ credentialService: "outlook",
68
+ capabilities: new Set(["threads", "folders"]),
69
+
70
+ async testConnection(connection?: OAuthConnection): Promise<ConnectionInfo> {
71
+ const conn = requireConnection(connection);
72
+ const profile = await outlook.getProfile(conn);
73
+ return {
74
+ connected: true,
75
+ user: profile.mail || profile.userPrincipalName,
76
+ platform: "outlook",
77
+ };
78
+ },
79
+
80
+ async listConversations(
81
+ connection: OAuthConnection | undefined,
82
+ _options?: ListOptions,
83
+ ): Promise<Conversation[]> {
84
+ const conn = requireConnection(connection);
85
+ const folders = await outlook.listMailFolders(conn);
86
+ return folders.map((folder) => ({
87
+ id: folder.id,
88
+ name: folder.displayName,
89
+ type: "inbox" as const,
90
+ platform: "outlook",
91
+ unreadCount: folder.unreadItemCount ?? 0,
92
+ lastActivityAt: Date.now(),
93
+ metadata: {
94
+ totalItemCount: folder.totalItemCount,
95
+ childFolderCount: folder.childFolderCount,
96
+ },
97
+ }));
98
+ },
99
+
100
+ async getHistory(
101
+ connection: OAuthConnection | undefined,
102
+ conversationId: string,
103
+ options?: HistoryOptions,
104
+ ): Promise<Message[]> {
105
+ const conn = requireConnection(connection);
106
+ const result = await outlook.listMessages(conn, {
107
+ folderId: conversationId,
108
+ top: options?.limit ?? 50,
109
+ orderby: "receivedDateTime desc",
110
+ select: MESSAGE_SELECT_FIELDS,
111
+ });
112
+ return (result.value ?? []).map(mapOutlookMessage);
113
+ },
114
+
115
+ async search(
116
+ connection: OAuthConnection | undefined,
117
+ query: string,
118
+ options?: SearchOptions,
119
+ ): Promise<SearchResult> {
120
+ const conn = requireConnection(connection);
121
+ const result = await outlook.searchMessages(conn, query, {
122
+ top: options?.count ?? 20,
123
+ });
124
+ const messages = result.value ?? [];
125
+ return {
126
+ total: result["@odata.count"] ?? messages.length,
127
+ messages: messages.map(mapOutlookMessage),
128
+ hasMore: !!result["@odata.nextLink"],
129
+ };
130
+ },
131
+
132
+ async sendMessage(
133
+ connection: OAuthConnection | undefined,
134
+ conversationId: string,
135
+ text: string,
136
+ options?: SendOptions,
137
+ ): Promise<SendResult> {
138
+ const conn = requireConnection(connection);
139
+
140
+ if (options?.inReplyTo) {
141
+ await outlook.replyToMessage(conn, options.inReplyTo, text);
142
+ return {
143
+ id: "",
144
+ timestamp: Date.now(),
145
+ conversationId,
146
+ threadId: options?.threadId,
147
+ };
148
+ }
149
+
150
+ await outlook.sendMessage(conn, {
151
+ message: {
152
+ subject: options?.subject ?? "",
153
+ body: { contentType: "text", content: text },
154
+ toRecipients: [{ emailAddress: { address: conversationId } }],
155
+ },
156
+ });
157
+
158
+ // Microsoft Graph's sendMail returns 202 with no body
159
+ return {
160
+ id: "",
161
+ timestamp: Date.now(),
162
+ conversationId,
163
+ threadId: options?.threadId,
164
+ };
165
+ },
166
+
167
+ async getThreadReplies(
168
+ connection: OAuthConnection | undefined,
169
+ _conversationId: string,
170
+ threadId: string,
171
+ options?: HistoryOptions,
172
+ ): Promise<Message[]> {
173
+ const conn = requireConnection(connection);
174
+ const result = await outlook.listMessages(conn, {
175
+ filter: `conversationId eq '${threadId.replace(/'/g, "''")}'`,
176
+ top: options?.limit ?? 50,
177
+ orderby: "receivedDateTime asc",
178
+ select: MESSAGE_SELECT_FIELDS,
179
+ });
180
+ return (result.value ?? []).map(mapOutlookMessage);
181
+ },
182
+
183
+ async markRead(
184
+ connection: OAuthConnection | undefined,
185
+ _conversationId: string,
186
+ messageId?: string,
187
+ ): Promise<void> {
188
+ const conn = requireConnection(connection);
189
+ if (messageId) {
190
+ await outlook.markMessageRead(conn, messageId);
191
+ }
192
+ },
193
+ };
@@ -0,0 +1,311 @@
1
+ import type {
2
+ OAuthConnection,
3
+ OAuthConnectionResponse,
4
+ } from "../../../oauth/connection.js";
5
+ import type {
6
+ OutlookMailFolder,
7
+ OutlookMailFolderListResponse,
8
+ OutlookMessage,
9
+ OutlookMessageListResponse,
10
+ OutlookSendMessagePayload,
11
+ OutlookUserProfile,
12
+ } from "./types.js";
13
+
14
+ export class OutlookApiError extends Error {
15
+ constructor(
16
+ public readonly status: number,
17
+ public readonly statusText: string,
18
+ message: string,
19
+ ) {
20
+ super(message);
21
+ this.name = "OutlookApiError";
22
+ }
23
+ }
24
+
25
+ const MAX_RETRIES = 3;
26
+ const INITIAL_BACKOFF_MS = 1000;
27
+
28
+ function isRetryable(status: number): boolean {
29
+ return status === 429 || (status >= 500 && status < 600);
30
+ }
31
+
32
+ const IDEMPOTENT_METHODS = new Set([
33
+ "GET",
34
+ "HEAD",
35
+ "PUT",
36
+ "DELETE",
37
+ "OPTIONS",
38
+ "PATCH",
39
+ ]);
40
+
41
+ function isIdempotent(method: string): boolean {
42
+ return IDEMPOTENT_METHODS.has(method.toUpperCase());
43
+ }
44
+
45
+ /**
46
+ * Make an authenticated request to the Microsoft Graph API with retry logic.
47
+ *
48
+ * The OAuth provider's baseUrl is `https://graph.microsoft.com`, so all paths
49
+ * must include the full API version and resource prefix (e.g. `/v1.0/me/messages`).
50
+ */
51
+ async function request<T>(
52
+ connection: OAuthConnection,
53
+ path: string,
54
+ options?: RequestInit,
55
+ query?: Record<string, string | string[]>,
56
+ ): Promise<T> {
57
+ const method = (options?.method ?? "GET").toUpperCase();
58
+ const canRetry = isIdempotent(method);
59
+
60
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
61
+ let resp: OAuthConnectionResponse;
62
+ try {
63
+ const extraHeaders =
64
+ options?.headers &&
65
+ typeof options.headers === "object" &&
66
+ !Array.isArray(options.headers)
67
+ ? (options.headers as Record<string, string>)
68
+ : {};
69
+ resp = await connection.request({
70
+ method,
71
+ path,
72
+ query,
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ ...extraHeaders,
76
+ },
77
+ body: options?.body ? JSON.parse(options.body as string) : undefined,
78
+ });
79
+ } catch (err) {
80
+ // Network-level errors from connection.request() are not retryable
81
+ throw err;
82
+ }
83
+
84
+ if (resp.status < 200 || resp.status >= 300) {
85
+ if (canRetry && isRetryable(resp.status) && attempt < MAX_RETRIES) {
86
+ const retryAfter =
87
+ resp.headers["retry-after"] ?? resp.headers["Retry-After"];
88
+ const delayMs = retryAfter
89
+ ? parseInt(retryAfter, 10) * 1000
90
+ : INITIAL_BACKOFF_MS * Math.pow(2, attempt);
91
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
92
+ continue;
93
+ }
94
+ const bodyStr =
95
+ typeof resp.body === "string"
96
+ ? resp.body
97
+ : JSON.stringify(resp.body ?? "");
98
+ throw new OutlookApiError(
99
+ resp.status,
100
+ "",
101
+ `Microsoft Graph API ${resp.status}: ${bodyStr}`,
102
+ );
103
+ }
104
+
105
+ // Success
106
+ if (resp.status === 204 || resp.body === undefined) {
107
+ return undefined as T;
108
+ }
109
+ return resp.body as T;
110
+ }
111
+
112
+ throw new Error(
113
+ "Unreachable: retry loop exited without returning or throwing",
114
+ );
115
+ }
116
+
117
+ /** Get the authenticated user's profile. */
118
+ export async function getProfile(
119
+ connection: OAuthConnection,
120
+ ): Promise<OutlookUserProfile> {
121
+ return request<OutlookUserProfile>(connection, "/v1.0/me");
122
+ }
123
+
124
+ /** List messages, optionally within a specific folder. */
125
+ export async function listMessages(
126
+ connection: OAuthConnection,
127
+ options?: {
128
+ folderId?: string;
129
+ top?: number;
130
+ skip?: number;
131
+ filter?: string;
132
+ orderby?: string;
133
+ select?: string;
134
+ },
135
+ ): Promise<OutlookMessageListResponse> {
136
+ const path = options?.folderId
137
+ ? `/v1.0/me/mailFolders/${encodeURIComponent(options.folderId)}/messages`
138
+ : "/v1.0/me/messages";
139
+
140
+ const query: Record<string, string> = {};
141
+ if (options?.top !== undefined) query["$top"] = String(options.top);
142
+ if (options?.skip !== undefined) query["$skip"] = String(options.skip);
143
+ if (options?.filter) query["$filter"] = options.filter;
144
+ if (options?.orderby) query["$orderby"] = options.orderby;
145
+ if (options?.select) query["$select"] = options.select;
146
+
147
+ return request<OutlookMessageListResponse>(
148
+ connection,
149
+ path,
150
+ undefined,
151
+ Object.keys(query).length > 0 ? query : undefined,
152
+ );
153
+ }
154
+
155
+ /** Get a single message by ID. */
156
+ export async function getMessage(
157
+ connection: OAuthConnection,
158
+ messageId: string,
159
+ select?: string,
160
+ ): Promise<OutlookMessage> {
161
+ const query: Record<string, string> = {};
162
+ if (select) query["$select"] = select;
163
+
164
+ return request<OutlookMessage>(
165
+ connection,
166
+ `/v1.0/me/messages/${encodeURIComponent(messageId)}`,
167
+ undefined,
168
+ Object.keys(query).length > 0 ? query : undefined,
169
+ );
170
+ }
171
+
172
+ /** Search messages using Microsoft Graph KQL syntax. */
173
+ export async function searchMessages(
174
+ connection: OAuthConnection,
175
+ searchQuery: string,
176
+ options?: {
177
+ top?: number;
178
+ skip?: number;
179
+ },
180
+ ): Promise<OutlookMessageListResponse> {
181
+ const query: Record<string, string> = {
182
+ $search: `"${searchQuery.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`,
183
+ $count: "true",
184
+ };
185
+ if (options?.top !== undefined) query["$top"] = String(options.top);
186
+ if (options?.skip !== undefined) query["$skip"] = String(options.skip);
187
+
188
+ return request<OutlookMessageListResponse>(
189
+ connection,
190
+ "/v1.0/me/messages",
191
+ {
192
+ headers: {
193
+ ConsistencyLevel: "eventual",
194
+ },
195
+ },
196
+ query,
197
+ );
198
+ }
199
+
200
+ /** Send a new message. */
201
+ export async function sendMessage(
202
+ connection: OAuthConnection,
203
+ message: OutlookSendMessagePayload,
204
+ ): Promise<void> {
205
+ await request<void>(connection, "/v1.0/me/sendMail", {
206
+ method: "POST",
207
+ body: JSON.stringify(message),
208
+ });
209
+ }
210
+
211
+ /** Reply to an existing message. */
212
+ export async function replyToMessage(
213
+ connection: OAuthConnection,
214
+ messageId: string,
215
+ comment: string,
216
+ ): Promise<void> {
217
+ await request<void>(
218
+ connection,
219
+ `/v1.0/me/messages/${encodeURIComponent(messageId)}/reply`,
220
+ {
221
+ method: "POST",
222
+ body: JSON.stringify({ comment }),
223
+ },
224
+ );
225
+ }
226
+
227
+ /** List mail folders. */
228
+ export async function listMailFolders(
229
+ connection: OAuthConnection,
230
+ ): Promise<OutlookMailFolder[]> {
231
+ const allFolders: OutlookMailFolder[] = [];
232
+ let nextQuery: Record<string, string> | undefined = { $top: "100" };
233
+
234
+ while (nextQuery) {
235
+ const resp = await request<OutlookMailFolderListResponse>(
236
+ connection,
237
+ "/v1.0/me/mailFolders",
238
+ undefined,
239
+ nextQuery,
240
+ );
241
+ if (resp.value) allFolders.push(...resp.value);
242
+ if (resp["@odata.nextLink"]) {
243
+ const nextUrl = new URL(resp["@odata.nextLink"]);
244
+ nextQuery = {};
245
+ nextUrl.searchParams.forEach((v, k) => {
246
+ nextQuery![k] = v;
247
+ });
248
+ } else {
249
+ nextQuery = undefined;
250
+ }
251
+ }
252
+
253
+ return allFolders;
254
+ }
255
+
256
+ /** Mark a message as read. */
257
+ export async function markMessageRead(
258
+ connection: OAuthConnection,
259
+ messageId: string,
260
+ ): Promise<void> {
261
+ await request<void>(
262
+ connection,
263
+ `/v1.0/me/messages/${encodeURIComponent(messageId)}`,
264
+ {
265
+ method: "PATCH",
266
+ body: JSON.stringify({ isRead: true }),
267
+ },
268
+ );
269
+ }
270
+
271
+ /** Move a message to a different folder (e.g. for archiving). */
272
+ export async function moveMessage(
273
+ connection: OAuthConnection,
274
+ messageId: string,
275
+ destinationFolderId: string,
276
+ ): Promise<OutlookMessage> {
277
+ return request<OutlookMessage>(
278
+ connection,
279
+ `/v1.0/me/messages/${encodeURIComponent(messageId)}/move`,
280
+ {
281
+ method: "POST",
282
+ body: JSON.stringify({ destinationId: destinationFolderId }),
283
+ },
284
+ );
285
+ }
286
+
287
+ /** Max concurrent individual getMessage requests for batch fetching. */
288
+ const BATCH_CONCURRENCY = 5;
289
+
290
+ /** Fetch multiple messages with concurrency limiting. */
291
+ export async function batchGetMessages(
292
+ connection: OAuthConnection,
293
+ messageIds: string[],
294
+ select?: string,
295
+ ): Promise<OutlookMessage[]> {
296
+ if (messageIds.length === 0) return [];
297
+
298
+ if (messageIds.length === 1) {
299
+ return [await getMessage(connection, messageIds[0], select)];
300
+ }
301
+
302
+ const results: OutlookMessage[] = [];
303
+ for (let i = 0; i < messageIds.length; i += BATCH_CONCURRENCY) {
304
+ const wave = messageIds.slice(i, i + BATCH_CONCURRENCY);
305
+ const waveResults = await Promise.all(
306
+ wave.map((id) => getMessage(connection, id, select)),
307
+ );
308
+ results.push(...waveResults);
309
+ }
310
+ return results;
311
+ }
@@ -0,0 +1,83 @@
1
+ /** Minimal Outlook message reference from list endpoint */
2
+ export interface OutlookMessageRef {
3
+ id: string;
4
+ conversationId: string;
5
+ }
6
+
7
+ /** Outlook message list/search response (paginated) */
8
+ export interface OutlookMessageListResponse {
9
+ value?: OutlookMessage[];
10
+ "@odata.nextLink"?: string;
11
+ "@odata.count"?: number;
12
+ }
13
+
14
+ /** Email address in Microsoft Graph format */
15
+ export interface OutlookEmailAddress {
16
+ name?: string;
17
+ address: string;
18
+ }
19
+
20
+ /** Recipient wrapper containing an email address */
21
+ export interface OutlookRecipient {
22
+ emailAddress: OutlookEmailAddress;
23
+ }
24
+
25
+ /** Message body with content type */
26
+ export interface OutlookItemBody {
27
+ contentType: "text" | "html";
28
+ content: string;
29
+ }
30
+
31
+ /** Full Outlook message from Microsoft Graph */
32
+ export interface OutlookMessage {
33
+ id: string;
34
+ conversationId: string;
35
+ subject: string;
36
+ bodyPreview: string;
37
+ body: OutlookItemBody;
38
+ from?: OutlookRecipient;
39
+ toRecipients: OutlookRecipient[];
40
+ ccRecipients: OutlookRecipient[];
41
+ receivedDateTime: string; // ISO 8601
42
+ isRead: boolean;
43
+ hasAttachments: boolean;
44
+ parentFolderId: string;
45
+ categories: string[];
46
+ flag: {
47
+ flagStatus: "notFlagged" | "flagged" | "complete";
48
+ };
49
+ }
50
+
51
+ /** Outlook mail folder */
52
+ export interface OutlookMailFolder {
53
+ id: string;
54
+ displayName: string;
55
+ totalItemCount?: number;
56
+ unreadItemCount?: number;
57
+ parentFolderId?: string;
58
+ childFolderCount?: number;
59
+ }
60
+
61
+ /** Outlook mail folder list response */
62
+ export interface OutlookMailFolderListResponse {
63
+ value?: OutlookMailFolder[];
64
+ "@odata.nextLink"?: string;
65
+ }
66
+
67
+ /** Payload for sending a message via Microsoft Graph */
68
+ export interface OutlookSendMessagePayload {
69
+ message: {
70
+ subject: string;
71
+ body: OutlookItemBody;
72
+ toRecipients: OutlookRecipient[];
73
+ ccRecipients?: OutlookRecipient[];
74
+ };
75
+ saveToSentItems?: boolean;
76
+ }
77
+
78
+ /** Microsoft Graph user profile */
79
+ export interface OutlookUserProfile {
80
+ displayName: string;
81
+ mail: string;
82
+ userPrincipalName: string;
83
+ }
@@ -153,7 +153,7 @@ export function buildAccessRequestBlocks(
153
153
  type: "section",
154
154
  text: {
155
155
  type: "mrkdwn",
156
- text: `Reply *\`${code} approve\`* to grant access or *\`${code} reject\`* to deny.`,
156
+ text: `Reply *${code} approve* to grant access or *${code} reject* to deny.`,
157
157
  },
158
158
  });
159
159
  }
@@ -38,7 +38,6 @@ function makeProviderRow(
38
38
  defaultScopes: "[]",
39
39
  scopePolicy: "{}",
40
40
  extraParams: null,
41
- callbackTransport: null,
42
41
  pingUrl: null,
43
42
  pingMethod: null,
44
43
  pingHeaders: null,
@@ -60,6 +59,7 @@ function makeProviderRow(
60
59
  identityResponsePaths: null,
61
60
  identityFormat: null,
62
61
  identityOkField: null,
62
+ featureFlag: null,
63
63
  createdAt: now,
64
64
  updatedAt: now,
65
65
  ...rest,
@@ -68,6 +68,14 @@ export interface OAuthConnectOptions {
68
68
  /** Send a message to the client (e.g. open_url). */
69
69
  sendToClient?: (msg: { type: string; [key: string]: unknown }) => void;
70
70
 
71
+ /**
72
+ * Callback transport to use for the OAuth redirect.
73
+ * - `"loopback"` — start a local HTTP server (desktop clients).
74
+ * - `"gateway"` — use the public gateway ingress (web clients).
75
+ * Defaults to `"loopback"` when omitted.
76
+ */
77
+ callbackTransport?: "loopback" | "gateway";
78
+
71
79
  /**
72
80
  * Called when the deferred (non-interactive) flow completes — either
73
81
  * successfully after tokens are stored, or on failure. Lets callers
@@ -142,9 +150,8 @@ export async function orchestrateOAuthConnect(
142
150
  const tokenEndpointAuthMethod = providerRow.tokenEndpointAuthMethod as
143
151
  | TokenEndpointAuthMethod
144
152
  | undefined;
145
- const callbackTransport =
146
- (providerRow.callbackTransport as "loopback" | "gateway" | null) ??
147
- "loopback";
153
+ const callbackTransport: "loopback" | "gateway" =
154
+ options.callbackTransport ?? "loopback";
148
155
  const loopbackPort = providerRow.loopbackPort ?? undefined;
149
156
 
150
157
  // Resolve scopes via the scope policy engine