@khester/create-dynamics-app 2.6.6 → 2.8.0

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 (66) hide show
  1. package/README.md +3 -0
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +87 -6
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/artifacts/types.d.ts +1 -1
  6. package/dist/artifacts/types.d.ts.map +1 -1
  7. package/dist/index.js +8 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/scaffold.d.ts +2 -0
  10. package/dist/scaffold.d.ts.map +1 -1
  11. package/dist/scaffold.js +4 -0
  12. package/dist/scaffold.js.map +1 -1
  13. package/dist/utils/copyTemplate.d.ts.map +1 -1
  14. package/dist/utils/copyTemplate.js +26 -7
  15. package/dist/utils/copyTemplate.js.map +1 -1
  16. package/package.json +2 -2
  17. package/templates/teams-app/README.md +32 -9
  18. package/templates/teams-app/{appPackage/manifest.json → _variants/tab.basic/appPackage/manifest.template.json} +6 -5
  19. package/templates/teams-app/_variants/tab.dashboard/appPackage/manifest.template.json +38 -0
  20. package/templates/teams-app/_variants/tab.dashboard/auth.html +15 -0
  21. package/templates/teams-app/_variants/tab.dashboard/env.local.template +3 -0
  22. package/templates/teams-app/_variants/tab.dashboard/src/App.tsx +49 -0
  23. package/templates/teams-app/_variants/tab.dashboard/src/auth-bridge.ts +52 -0
  24. package/templates/teams-app/_variants/tab.dashboard/src/components/Dashboard.tsx +33 -0
  25. package/templates/teams-app/_variants/tab.dashboard/src/components/widgets/StatsWidget.tsx +25 -0
  26. package/templates/teams-app/_variants/tab.dashboard/src/components/widgets/TasksWidget.tsx +20 -0
  27. package/templates/teams-app/_variants/tab.dashboard/src/hooks/useHashRoute.ts +16 -0
  28. package/templates/teams-app/_variants/tab.dashboard/src/hooks/useTabConfig.ts +70 -0
  29. package/templates/teams-app/_variants/tab.dashboard/src/hooks/useTeamsAuth.ts +121 -0
  30. package/templates/teams-app/_variants/tab.dashboard/src/pages/Config.tsx +118 -0
  31. package/templates/teams-app/_variants/tab.dashboard/src/pages/Remove.tsx +12 -0
  32. package/templates/teams-app/_variants/tab.dashboard/src/vite-env.d.ts +10 -0
  33. package/templates/teams-app/_variants/tab.dashboard/vite.config.ts +19 -0
  34. package/templates/teams-app/_variants/tab.sso/appPackage/manifest.template.json +31 -0
  35. package/templates/teams-app/_variants/tab.sso/auth.html +15 -0
  36. package/templates/teams-app/_variants/tab.sso/env.local.template +3 -0
  37. package/templates/teams-app/_variants/tab.sso/src/App.tsx +57 -0
  38. package/templates/teams-app/_variants/tab.sso/src/auth-bridge.ts +52 -0
  39. package/templates/teams-app/_variants/tab.sso/src/hooks/useTeamsAuth.ts +121 -0
  40. package/templates/teams-app/_variants/tab.sso/src/vite-env.d.ts +10 -0
  41. package/templates/teams-app/_variants/tab.sso/vite.config.ts +19 -0
  42. package/templates/teams-app/appPackage/color.png +0 -0
  43. package/templates/teams-app/appPackage/outline.png +0 -0
  44. package/templates/teams-app/gitignore +2 -0
  45. package/templates/teams-app/package.json +1 -1
  46. package/templates/teams-bot/.env.example +18 -0
  47. package/templates/teams-bot/README.md +52 -0
  48. package/templates/teams-bot/_variants/bot.echo/appPackage/manifest.template.json +25 -0
  49. package/templates/teams-bot/_variants/bot.echo/src/bot/TeamsBot.ts +142 -0
  50. package/templates/teams-bot/_variants/bot.echo/src/bot/welcomeCard.ts +134 -0
  51. package/templates/teams-bot/_variants/bot.echo/src/index.ts +59 -0
  52. package/templates/teams-bot/_variants/bot.echo/src/mock/mockMode.ts +104 -0
  53. package/templates/teams-bot/_variants/bot.notification/appPackage/manifest.template.json +25 -0
  54. package/templates/teams-bot/_variants/bot.notification/src/bot/NotificationBot.ts +54 -0
  55. package/templates/teams-bot/_variants/bot.notification/src/index.ts +136 -0
  56. package/templates/teams-bot/_variants/bot.notification/src/mock/mockMode.ts +55 -0
  57. package/templates/teams-bot/_variants/bot.notification/src/storage/conversationStore.ts +84 -0
  58. package/templates/teams-bot/appPackage/color.png +0 -0
  59. package/templates/teams-bot/appPackage/outline.png +0 -0
  60. package/templates/teams-bot/gitignore +8 -0
  61. package/templates/teams-bot/nodemon.json +6 -0
  62. package/templates/teams-bot/package.json +24 -0
  63. package/templates/teams-bot/src/adapters/botAdapter.ts +45 -0
  64. package/templates/teams-bot/src/config.ts +34 -0
  65. package/templates/teams-bot/tsconfig.json +17 -0
  66. /package/templates/teams-app/{src → _variants/tab.basic/src}/App.tsx +0 -0
@@ -0,0 +1,104 @@
1
+ import { TestAdapter, Activity, ConversationReference, TurnContext } from 'botbuilder';
2
+ import { TeamsBot } from '../bot/TeamsBot';
3
+
4
+ // Create a test adapter for mock mode testing
5
+ export function createMockAdapter(): TestAdapter {
6
+ const adapter = new TestAdapter(async (context: TurnContext) => {
7
+ const bot = new TeamsBot();
8
+ await bot.run(context);
9
+ });
10
+
11
+ return adapter;
12
+ }
13
+
14
+ // Simulate a Teams conversation for testing
15
+ export async function runMockConversation(
16
+ userMessage: string,
17
+ callback?: (response: Partial<Activity>[]) => void
18
+ ): Promise<Partial<Activity>[]> {
19
+ const adapter = createMockAdapter();
20
+ const responses: Partial<Activity>[] = [];
21
+
22
+ await adapter.send(userMessage).assertReply((activity) => {
23
+ responses.push(activity);
24
+ callback?.([activity]);
25
+ });
26
+
27
+ return responses;
28
+ }
29
+
30
+ // Create a mock conversation reference for proactive messaging tests
31
+ export function createMockConversationReference(
32
+ userId: string = 'user-1'
33
+ ): Partial<ConversationReference> {
34
+ return {
35
+ activityId: 'mock-activity-id',
36
+ user: {
37
+ id: userId,
38
+ name: 'Mock User',
39
+ },
40
+ bot: {
41
+ id: 'mock-bot-id',
42
+ name: 'Mock Bot',
43
+ },
44
+ conversation: {
45
+ id: 'mock-conversation-id',
46
+ isGroup: false,
47
+ conversationType: 'personal',
48
+ tenantId: 'mock-tenant-id',
49
+ name: '',
50
+ },
51
+ channelId: 'msteams',
52
+ serviceUrl: 'https://smba.trafficmanager.net/teams/',
53
+ };
54
+ }
55
+
56
+ // Interactive console testing
57
+ export async function startInteractiveTest(): Promise<void> {
58
+ const readline = await import('readline');
59
+ const rl = readline.createInterface({
60
+ input: process.stdin,
61
+ output: process.stdout,
62
+ });
63
+
64
+ console.log('\n=== Bot Mock Mode Interactive Test ===');
65
+ console.log('Type messages to test the bot. Type "exit" to quit.\n');
66
+
67
+ const adapter = createMockAdapter();
68
+
69
+ const askQuestion = (): void => {
70
+ rl.question('You: ', async (input) => {
71
+ if (input.toLowerCase() === 'exit') {
72
+ console.log('Goodbye!');
73
+ rl.close();
74
+ process.exit(0);
75
+ return;
76
+ }
77
+
78
+ try {
79
+ await adapter.send(input).assertReply((activity) => {
80
+ if (activity.text) {
81
+ console.log(`Bot: ${activity.text}`);
82
+ }
83
+ if (activity.attachments && activity.attachments.length > 0) {
84
+ console.log('Bot: [Adaptive Card]');
85
+ const card = activity.attachments[0].content;
86
+ if (card.body) {
87
+ for (const block of card.body) {
88
+ if (block.type === 'TextBlock' && block.text) {
89
+ console.log(` ${block.text}`);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ });
95
+ } catch {
96
+ console.log('Bot: [No response]');
97
+ }
98
+
99
+ askQuestion();
100
+ });
101
+ };
102
+
103
+ askQuestion();
104
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json",
3
+ "manifestVersion": "1.19",
4
+ "id": "{{TEAMS_APP_ID}}",
5
+ "version": "1.0.0",
6
+ "developer": {
7
+ "name": "Contoso",
8
+ "websiteUrl": "https://example.com",
9
+ "privacyUrl": "https://example.com/privacy",
10
+ "termsOfUseUrl": "https://example.com/terms"
11
+ },
12
+ "name": { "short": "{{projectName}}", "full": "{{projectName}}" },
13
+ "description": { "short": "{{projectName}} notifications", "full": "{{projectName}} — a Microsoft Teams bot that delivers proactive notifications; message it 'subscribe' to opt in" },
14
+ "icons": { "color": "color.png", "outline": "outline.png" },
15
+ "accentColor": "#6264A7",
16
+ "bots": [
17
+ {
18
+ "botId": "{{BOT_ID}}",
19
+ "scopes": ["personal", "team", "groupChat"],
20
+ "supportsFiles": false,
21
+ "isNotificationOnly": false
22
+ }
23
+ ],
24
+ "permissions": ["identity"]
25
+ }
@@ -0,0 +1,54 @@
1
+ import { TeamsActivityHandler, TurnContext, MessageFactory, CardFactory, Attachment } from 'botbuilder';
2
+ import { IConversationStore } from '../storage/conversationStore';
3
+
4
+ export class NotificationBot extends TeamsActivityHandler {
5
+ constructor(private conversationStore: IConversationStore) {
6
+ super();
7
+
8
+ // Capture the conversation reference whenever we hear from a user — this is
9
+ // what proactive messaging replays later via continueConversationAsync.
10
+ this.onConversationUpdate(async (context: TurnContext, next: () => Promise<void>) => {
11
+ await this.saveConversationReference(context);
12
+ await next();
13
+ });
14
+
15
+ this.onMessage(async (context: TurnContext, next: () => Promise<void>) => {
16
+ await this.saveConversationReference(context);
17
+ const text = context.activity.text?.toLowerCase().trim() || '';
18
+
19
+ if (text === 'subscribe') {
20
+ await context.sendActivity(MessageFactory.text('You are subscribed to notifications!'));
21
+ } else if (text === 'unsubscribe') {
22
+ const userId = context.activity.from.id;
23
+ await this.conversationStore.delete(userId);
24
+ await context.sendActivity(MessageFactory.text('You have been unsubscribed.'));
25
+ } else {
26
+ await context.sendActivity(
27
+ MessageFactory.text('Commands: **subscribe**, **unsubscribe**. Notifications arrive via POST /api/notify.')
28
+ );
29
+ }
30
+ await next();
31
+ });
32
+ }
33
+
34
+ private async saveConversationReference(context: TurnContext): Promise<void> {
35
+ const reference = TurnContext.getConversationReference(context.activity);
36
+ const userId = context.activity.from?.id;
37
+ if (userId) {
38
+ await this.conversationStore.save(userId, reference);
39
+ }
40
+ }
41
+
42
+ static createNotificationCard(title: string, message: string): Attachment {
43
+ return CardFactory.adaptiveCard({
44
+ type: 'AdaptiveCard',
45
+ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
46
+ version: '1.4',
47
+ body: [
48
+ { type: 'TextBlock', text: title, weight: 'Bolder', size: 'Medium', wrap: true },
49
+ { type: 'TextBlock', text: message, wrap: true },
50
+ { type: 'TextBlock', text: new Date().toLocaleString(), size: 'Small', isSubtle: true },
51
+ ],
52
+ });
53
+ }
54
+ }
@@ -0,0 +1,136 @@
1
+ import express, { Request, Response } from 'express';
2
+ import { timingSafeEqual } from 'node:crypto';
3
+ import { config, validateConfig } from './config';
4
+ import { createBotAdapter } from './adapters/botAdapter';
5
+ import { NotificationBot } from './bot/NotificationBot';
6
+ import { FileConversationStore, MemoryConversationStore } from './storage/conversationStore';
7
+ import { startInteractiveTest } from './mock/mockMode';
8
+
9
+ // Length-checked constant-time compare (timingSafeEqual throws on length mismatch) so the
10
+ // shared X-Notify-Key can't be recovered a byte at a time via response-timing analysis.
11
+ function timingSafeEqualStr(a: string, b: string): boolean {
12
+ const ab = Buffer.from(a);
13
+ const bb = Buffer.from(b);
14
+ return ab.length === bb.length && timingSafeEqual(ab, bb);
15
+ }
16
+
17
+ validateConfig();
18
+
19
+ // Interactive mock mode: console chat loop, no server / no Azure.
20
+ if (process.argv.includes('--interactive')) {
21
+ startInteractiveTest();
22
+ } else {
23
+ const app = express();
24
+ app.use(express.json());
25
+
26
+ const adapter = createBotAdapter();
27
+ // File store by default so subscriptions survive restarts; memory store in mock mode.
28
+ const conversationStore = config.mockMode
29
+ ? new MemoryConversationStore()
30
+ : new FileConversationStore(config.conversationStorePath);
31
+ const bot = new NotificationBot(conversationStore);
32
+
33
+ // Health check endpoint (the deploy pipeline gates on this)
34
+ app.get('/health', (_req: Request, res: Response) => {
35
+ res.json({
36
+ status: 'healthy',
37
+ timestamp: new Date().toISOString(),
38
+ mockMode: config.mockMode,
39
+ });
40
+ });
41
+
42
+ // Bot messaging endpoint
43
+ app.post('/api/messages', async (req: Request, res: Response) => {
44
+ try {
45
+ await adapter.process(req, res, (context) => bot.run(context));
46
+ } catch (error) {
47
+ console.error('Error processing message:', error);
48
+ res.status(500).send('Internal Server Error');
49
+ }
50
+ });
51
+
52
+ // Proactive notification endpoint. Fail-closed: without a configured key the
53
+ // endpoint stays off (this is a public URL — an open POST would be a spam relay).
54
+ app.post('/api/notify', async (req: Request, res: Response) => {
55
+ if (!config.notifyApiKey) {
56
+ res.status(503).json({ error: 'NOTIFY_API_KEY is not configured; endpoint disabled.' });
57
+ return;
58
+ }
59
+ const providedKey = req.get('X-Notify-Key');
60
+ if (!providedKey || !timingSafeEqualStr(providedKey, config.notifyApiKey)) {
61
+ res.status(401).json({ error: 'Missing or invalid X-Notify-Key header.' });
62
+ return;
63
+ }
64
+
65
+ const { title, message, userId } = req.body as {
66
+ title?: string;
67
+ message?: string;
68
+ userId?: string;
69
+ };
70
+ if (!title || !message) {
71
+ res.status(400).json({ error: 'Both "title" and "message" are required.' });
72
+ return;
73
+ }
74
+
75
+ const sendTo = async (reference: Parameters<typeof adapter.continueConversationAsync>[1]) => {
76
+ await adapter.continueConversationAsync(config.botId, reference, async (context) => {
77
+ await context.sendActivity({
78
+ attachments: [NotificationBot.createNotificationCard(title, message)],
79
+ });
80
+ });
81
+ };
82
+
83
+ try {
84
+ if (userId) {
85
+ const reference = await conversationStore.get(userId);
86
+ if (!reference) {
87
+ res.status(404).json({ error: `No conversation reference for user "${userId}".` });
88
+ return;
89
+ }
90
+ await sendTo(reference);
91
+ res.json({ success: true, sent: 1 });
92
+ } else {
93
+ // Broadcast to every stored reference; tolerate stale ones.
94
+ const refs = await conversationStore.getAll();
95
+ let sent = 0;
96
+ let failed = 0;
97
+ for (const [id, reference] of refs) {
98
+ try {
99
+ await sendTo(reference);
100
+ sent++;
101
+ } catch (err) {
102
+ failed++;
103
+ console.warn(`Failed to notify ${id}:`, err);
104
+ }
105
+ }
106
+ res.json({ success: true, sent, failed });
107
+ }
108
+ } catch (error) {
109
+ console.error('Error sending notification:', error);
110
+ res.status(500).json({ error: 'Failed to send notification.' });
111
+ }
112
+ });
113
+
114
+ app.get('/', (_req: Request, res: Response) => {
115
+ res.json({
116
+ name: '{{projectName}}',
117
+ description: 'Microsoft Teams notification bot (Bot Framework SDK v4, proactive messaging)',
118
+ endpoints: { messaging: '/api/messages', notify: '/api/notify', health: '/health' },
119
+ mockMode: config.mockMode,
120
+ });
121
+ });
122
+
123
+ app.listen(config.port, () => {
124
+ console.log(`\nNotification bot server is running!`);
125
+ console.log(` Local: http://localhost:${config.port}`);
126
+ console.log(` Health: http://localhost:${config.port}/health`);
127
+ console.log(` Mode: ${config.mockMode ? 'Mock (development)' : 'Production'}`);
128
+ console.log(`\n Messaging endpoint: /api/messages`);
129
+ console.log(` Notify endpoint: /api/notify (X-Notify-Key required)`);
130
+
131
+ if (config.mockMode) {
132
+ console.log(`\nTip: run with --interactive for console testing:`);
133
+ console.log(` npm run dev -- --interactive`);
134
+ }
135
+ });
136
+ }
@@ -0,0 +1,55 @@
1
+ import { TestAdapter, TurnContext } from 'botbuilder';
2
+ import { NotificationBot } from '../bot/NotificationBot';
3
+ import { MemoryConversationStore } from '../storage/conversationStore';
4
+
5
+ // Create a test adapter for mock mode testing (memory store — nothing persists)
6
+ export function createMockAdapter(): TestAdapter {
7
+ const bot = new NotificationBot(new MemoryConversationStore());
8
+ const adapter = new TestAdapter(async (context: TurnContext) => {
9
+ await bot.run(context);
10
+ });
11
+
12
+ return adapter;
13
+ }
14
+
15
+ // Interactive console testing
16
+ export async function startInteractiveTest(): Promise<void> {
17
+ const readline = await import('readline');
18
+ const rl = readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout,
21
+ });
22
+
23
+ console.log('\n=== Notification Bot Mock Mode ===');
24
+ console.log('Try "subscribe" / "unsubscribe". Type "exit" to quit.\n');
25
+
26
+ const adapter = createMockAdapter();
27
+
28
+ const askQuestion = (): void => {
29
+ rl.question('You: ', async (input) => {
30
+ if (input.toLowerCase() === 'exit') {
31
+ console.log('Goodbye!');
32
+ rl.close();
33
+ process.exit(0);
34
+ return;
35
+ }
36
+
37
+ try {
38
+ await adapter.send(input).assertReply((activity) => {
39
+ if (activity.text) {
40
+ console.log(`Bot: ${activity.text}`);
41
+ }
42
+ if (activity.attachments && activity.attachments.length > 0) {
43
+ console.log('Bot: [Adaptive Card]');
44
+ }
45
+ });
46
+ } catch {
47
+ console.log('Bot: [No response]');
48
+ }
49
+
50
+ askQuestion();
51
+ });
52
+ };
53
+
54
+ askQuestion();
55
+ }
@@ -0,0 +1,84 @@
1
+ import { ConversationReference } from 'botbuilder';
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+
5
+ export interface IConversationStore {
6
+ save(userId: string, reference: Partial<ConversationReference>): Promise<void>;
7
+ get(userId: string): Promise<Partial<ConversationReference> | null>;
8
+ getAll(): Promise<Map<string, Partial<ConversationReference>>>;
9
+ delete(userId: string): Promise<void>;
10
+ }
11
+
12
+ export class MemoryConversationStore implements IConversationStore {
13
+ private store = new Map<string, Partial<ConversationReference>>();
14
+
15
+ async save(userId: string, reference: Partial<ConversationReference>): Promise<void> {
16
+ this.store.set(userId, reference);
17
+ }
18
+
19
+ async get(userId: string): Promise<Partial<ConversationReference> | null> {
20
+ return this.store.get(userId) || null;
21
+ }
22
+
23
+ async getAll(): Promise<Map<string, Partial<ConversationReference>>> {
24
+ return new Map(this.store);
25
+ }
26
+
27
+ async delete(userId: string): Promise<void> {
28
+ this.store.delete(userId);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Write-through JSON-file store so subscriptions survive process restarts.
34
+ * On Azure App Service point it at /home/data/conversations.json — /home is
35
+ * persistent storage that also survives redeploys (unlike wwwroot).
36
+ */
37
+ export class FileConversationStore implements IConversationStore {
38
+ private store = new Map<string, Partial<ConversationReference>>();
39
+ private loaded = false;
40
+
41
+ constructor(private filePath: string) {}
42
+
43
+ private async load(): Promise<void> {
44
+ if (this.loaded) return;
45
+ this.loaded = true;
46
+ try {
47
+ const raw = await fs.readFile(this.filePath, 'utf8');
48
+ const entries = JSON.parse(raw) as [string, Partial<ConversationReference>][];
49
+ this.store = new Map(entries);
50
+ console.log(`Loaded ${this.store.size} conversation reference(s) from ${this.filePath}`);
51
+ } catch (err: unknown) {
52
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {
53
+ console.warn(`Could not read conversation store at ${this.filePath}:`, err);
54
+ }
55
+ }
56
+ }
57
+
58
+ private async persist(): Promise<void> {
59
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
60
+ await fs.writeFile(this.filePath, JSON.stringify([...this.store.entries()], null, 2), 'utf8');
61
+ }
62
+
63
+ async save(userId: string, reference: Partial<ConversationReference>): Promise<void> {
64
+ await this.load();
65
+ this.store.set(userId, reference);
66
+ await this.persist();
67
+ }
68
+
69
+ async get(userId: string): Promise<Partial<ConversationReference> | null> {
70
+ await this.load();
71
+ return this.store.get(userId) || null;
72
+ }
73
+
74
+ async getAll(): Promise<Map<string, Partial<ConversationReference>>> {
75
+ await this.load();
76
+ return new Map(this.store);
77
+ }
78
+
79
+ async delete(userId: string): Promise<void> {
80
+ await this.load();
81
+ this.store.delete(userId);
82
+ await this.persist();
83
+ }
84
+ }
@@ -0,0 +1,8 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ .data/
5
+ .env
6
+ .env.local
7
+ .teams/
8
+ *.log
@@ -0,0 +1,6 @@
1
+ {
2
+ "watch": ["src"],
3
+ "ext": "ts",
4
+ "ignore": ["src/**/*.spec.ts"],
5
+ "exec": "ts-node src/index.ts"
6
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "{{projectName}} — Microsoft Teams bot (Bot Framework SDK v4)",
6
+ "scripts": {
7
+ "dev": "nodemon src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "botbuilder": "^4.22.1",
14
+ "dotenv": "^16.4.1",
15
+ "express": "^4.18.2"
16
+ },
17
+ "devDependencies": {
18
+ "@types/express": "^4.17.21",
19
+ "@types/node": "^20.11.0",
20
+ "nodemon": "^3.0.3",
21
+ "ts-node": "^10.9.2",
22
+ "typescript": "^5.3.3"
23
+ }
24
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ CloudAdapter,
3
+ ConfigurationBotFrameworkAuthentication,
4
+ ConfigurationBotFrameworkAuthenticationOptions,
5
+ TurnContext,
6
+ } from 'botbuilder';
7
+ import { config } from '../config';
8
+
9
+ // Error handler for the adapter
10
+ const onTurnErrorHandler = async (context: TurnContext, error: Error): Promise<void> => {
11
+ console.error(`\n [onTurnError] unhandled error: ${error}`);
12
+ console.error(error.stack);
13
+
14
+ await context.sendActivity('The bot encountered an error or bug.');
15
+
16
+ // Send a trace activity (visible in Bot Framework Emulator)
17
+ await context.sendTraceActivity(
18
+ 'OnTurnError Trace',
19
+ `${error}`,
20
+ 'https://www.botframework.com/schemas/error',
21
+ 'TurnError'
22
+ );
23
+ };
24
+
25
+ export function createBotAdapter(): CloudAdapter {
26
+ // MicrosoftAppTenantId only applies to SingleTenant — a MultiTenant bot must
27
+ // authenticate against the common Bot Framework authority, so omit it there.
28
+ const authConfig: ConfigurationBotFrameworkAuthenticationOptions = config.mockMode
29
+ ? {} // Empty config for mock mode
30
+ : {
31
+ MicrosoftAppId: config.botId,
32
+ MicrosoftAppPassword: config.botPassword,
33
+ MicrosoftAppType: config.appType,
34
+ ...(config.appType === 'SingleTenant'
35
+ ? { MicrosoftAppTenantId: config.botTenantId }
36
+ : {}),
37
+ };
38
+
39
+ const botFrameworkAuth = new ConfigurationBotFrameworkAuthentication(authConfig);
40
+ const adapter = new CloudAdapter(botFrameworkAuth);
41
+
42
+ adapter.onTurnError = onTurnErrorHandler;
43
+
44
+ return adapter;
45
+ }
@@ -0,0 +1,34 @@
1
+ import dotenv from 'dotenv';
2
+
3
+ // .env.local first (the teams-app-manager identity stage writes secrets there),
4
+ // then .env — dotenv never overwrites a key that is already set.
5
+ dotenv.config({ path: ['.env.local', '.env'] });
6
+
7
+ export type AppType = 'SingleTenant' | 'MultiTenant';
8
+
9
+ export const config = {
10
+ // Bot Framework credentials
11
+ botId: process.env.MICROSOFT_APP_ID || '',
12
+ botPassword: process.env.MICROSOFT_APP_PASSWORD || '',
13
+ botTenantId: process.env.MICROSOFT_APP_TENANT_ID || '',
14
+ // Must match the Azure Bot resource's app type; MultiTenant needs no tenant id.
15
+ appType: (process.env.MICROSOFT_APP_TYPE as AppType) || 'SingleTenant',
16
+
17
+ // Shared secret for POST /api/notify (notification bots). Empty = endpoint disabled.
18
+ notifyApiKey: process.env.NOTIFY_API_KEY || '',
19
+
20
+ // Conversation-reference persistence (notification bots).
21
+ conversationStorePath: process.env.CONVERSATION_STORE_PATH || './.data/conversations.json',
22
+
23
+ // Server configuration
24
+ port: parseInt(process.env.PORT || '3978', 10),
25
+
26
+ // Mock mode for development without an Azure registration
27
+ mockMode: process.env.MOCK_MODE === 'true',
28
+ };
29
+
30
+ export function validateConfig(): void {
31
+ if (!config.mockMode && !config.botId) {
32
+ console.warn('Warning: MICROSOFT_APP_ID not set. Set MOCK_MODE=true for local development.');
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "sourceMap": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }