@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.
- package/ARCHITECTURE.md +2 -2
- package/docs/architecture/integrations.md +15 -14
- package/knip.json +3 -1
- package/openapi.yaml +11 -43
- package/package.json +1 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
- package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
- package/src/__tests__/checker.test.ts +59 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
- package/src/__tests__/cli-memory.test.ts +372 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
- package/src/__tests__/config-schema.test.ts +0 -2
- package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +2 -6
- package/src/__tests__/conversation-usage.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +4 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
- package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -3
- package/src/__tests__/intent-routing.test.ts +6 -18
- package/src/__tests__/log-export-workspace.test.ts +2 -28
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
- package/src/__tests__/managed-store.test.ts +2 -10
- package/src/__tests__/messaging-send-tool.test.ts +6 -6
- package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
- package/src/__tests__/migration-export-http.test.ts +3 -34
- package/src/__tests__/migration-import-commit-http.test.ts +1 -29
- package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
- package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
- package/src/__tests__/oauth-apps-routes.test.ts +120 -10
- package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
- package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
- package/src/__tests__/oauth-providers-routes.test.ts +5 -2
- package/src/__tests__/oauth-store.test.ts +0 -5
- package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
- package/src/__tests__/path-policy.test.ts +2 -17
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/platform-callback-registration.test.ts +3 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -2
- package/src/__tests__/qdrant-manager.test.ts +68 -21
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
- package/src/__tests__/secret-allowlist.test.ts +20 -35
- package/src/__tests__/shell-credential-ref.test.ts +0 -5
- package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
- package/src/__tests__/skill-load-inline-command.test.ts +3 -65
- package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
- package/src/__tests__/skill-load-tool.test.ts +3 -67
- package/src/__tests__/skill-memory.test.ts +362 -119
- package/src/__tests__/skills.test.ts +22 -49
- package/src/__tests__/slack-channel-config.test.ts +2 -21
- package/src/__tests__/starter-bundle.test.ts +2 -8
- package/src/__tests__/stt-hints.test.ts +7 -2
- package/src/__tests__/system-prompt.test.ts +25 -45
- package/src/__tests__/task-compiler.test.ts +0 -21
- package/src/__tests__/task-management-tools.test.ts +0 -21
- package/src/__tests__/task-memory-cleanup.test.ts +0 -21
- package/src/__tests__/task-runner.test.ts +0 -21
- package/src/__tests__/task-scheduler.test.ts +0 -21
- package/src/__tests__/terminal-tools.test.ts +1 -17
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
- package/src/__tests__/tool-approval-handler.test.ts +1 -20
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
- package/src/__tests__/trust-store.test.ts +9 -41
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
- package/src/__tests__/trusted-contact-verification.test.ts +0 -22
- package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
- package/src/__tests__/twilio-provider.test.ts +0 -16
- package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
- package/src/__tests__/twilio-routes.test.ts +0 -24
- package/src/__tests__/update-bulletin.test.ts +17 -89
- package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
- package/src/__tests__/usage-routes.test.ts +0 -21
- package/src/__tests__/user-reference.test.ts +1 -5
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
- package/src/__tests__/voice-invite-redemption.test.ts +0 -21
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
- package/src/__tests__/voice-session-bridge.test.ts +0 -21
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
- package/src/acp/client-handler.ts +1 -2
- package/src/cli/__tests__/notifications.test.ts +0 -22
- package/src/cli/cli-memory.ts +176 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +15 -0
- package/src/cli/commands/oauth/providers.ts +49 -42
- package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
- package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/context/window-manager.ts +28 -9
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/daemon/config-watcher.ts +51 -0
- package/src/daemon/conversation-agent-loop.ts +3 -2
- package/src/daemon/conversation-process.ts +1 -0
- package/src/daemon/conversation-usage.ts +1 -0
- package/src/daemon/handlers/skills.ts +9 -1
- package/src/daemon/lifecycle.ts +13 -4
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/providers-setup.ts +2 -0
- package/src/daemon/server.ts +26 -22
- package/src/events/domain-events.ts +1 -2
- package/src/memory/db-init.ts +9 -0
- package/src/memory/job-handlers/batch-extraction.ts +16 -4
- package/src/memory/job-handlers/embedding.test.ts +3 -27
- package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
- package/src/memory/llm-usage-store.ts +35 -2
- package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
- package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/qdrant-manager.ts +26 -5
- package/src/memory/query-expansion.ts +1 -1
- package/src/memory/retriever.test.ts +22 -20
- package/src/memory/retriever.ts +10 -2
- package/src/memory/schema/oauth.ts +1 -1
- package/src/memory/search/mmr.ts +8 -5
- package/src/memory/slack-thread-store.ts +17 -0
- package/src/messaging/providers/outlook/adapter.ts +193 -0
- package/src/messaging/providers/outlook/client.ts +311 -0
- package/src/messaging/providers/outlook/types.ts +83 -0
- package/src/notifications/adapters/slack.ts +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +10 -3
- package/src/oauth/oauth-store.ts +10 -11
- package/src/oauth/provider-serializer.ts +3 -0
- package/src/oauth/provider-visibility.ts +16 -0
- package/src/oauth/seed-providers.ts +49 -17
- package/src/permissions/checker.ts +39 -7
- package/src/permissions/types.ts +2 -4
- package/src/prompts/journal-context.ts +9 -11
- package/src/prompts/system-prompt.ts +3 -64
- package/src/prompts/templates/UPDATES.md +6 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
- package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/routes/conversation-query-routes.ts +2 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
- package/src/runtime/routes/memory-item-routes.test.ts +0 -17
- package/src/runtime/routes/memory-item-routes.ts +103 -12
- package/src/runtime/routes/oauth-apps.ts +18 -1
- package/src/runtime/routes/oauth-providers.ts +13 -1
- package/src/runtime/routes/settings-routes.ts +1 -0
- package/src/runtime/routes/usage-routes.ts +19 -2
- package/src/runtime/routes/work-items-routes.test.ts +0 -21
- package/src/runtime/routes/workspace-routes.test.ts +3 -27
- package/src/security/secret-allowlist.ts +4 -4
- package/src/skills/skill-memory.ts +62 -23
- package/src/tools/memory/handlers.test.ts +1 -29
- package/src/tools/permission-checker.ts +0 -18
- package/src/tools/skills/skill-script-runner.ts +1 -1
- package/src/util/device-id.ts +3 -65
- 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
|
|
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
|
-
|
|
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
|