@microsoft/agents-hosting 0.5.1-g2e246ff274 → 0.5.12-g2d752e9b13

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 (61) hide show
  1. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +4 -4
  2. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
  3. package/dist/src/app/agentApplication.js +1 -1
  4. package/dist/src/app/agentApplication.js.map +1 -1
  5. package/dist/src/app/oauth/authorization.d.ts +1 -0
  6. package/dist/src/app/oauth/authorization.js +3 -4
  7. package/dist/src/app/oauth/authorization.js.map +1 -1
  8. package/dist/src/app/streaming/citation.d.ts +25 -0
  9. package/dist/src/app/streaming/citation.js +7 -0
  10. package/dist/src/app/streaming/citation.js.map +1 -0
  11. package/dist/src/app/streaming/citationUtil.d.ts +31 -0
  12. package/dist/src/app/streaming/citationUtil.js +70 -0
  13. package/dist/src/app/streaming/citationUtil.js.map +1 -0
  14. package/dist/src/app/streaming/streamingResponse.d.ts +140 -0
  15. package/dist/src/app/streaming/streamingResponse.js +333 -0
  16. package/dist/src/app/streaming/streamingResponse.js.map +1 -0
  17. package/dist/src/cards/cardFactory.d.ts +2 -2
  18. package/dist/src/cards/cardFactory.js.map +1 -1
  19. package/dist/src/oauth/index.d.ts +1 -4
  20. package/dist/src/oauth/index.js +1 -4
  21. package/dist/src/oauth/index.js.map +1 -1
  22. package/dist/src/oauth/oAuthFlow.d.ts +9 -10
  23. package/dist/src/oauth/oAuthFlow.js +35 -38
  24. package/dist/src/oauth/oAuthFlow.js.map +1 -1
  25. package/dist/src/oauth/userTokenClient.d.ts +37 -7
  26. package/dist/src/oauth/userTokenClient.js +56 -15
  27. package/dist/src/oauth/userTokenClient.js.map +1 -1
  28. package/dist/src/oauth/userTokenClient.types.d.ts +147 -0
  29. package/dist/src/oauth/{signingResource.js → userTokenClient.types.js} +1 -1
  30. package/dist/src/oauth/userTokenClient.types.js.map +1 -0
  31. package/dist/src/turnContext.d.ts +3 -0
  32. package/dist/src/turnContext.js +5 -0
  33. package/dist/src/turnContext.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/app/adaptiveCards/adaptiveCardsActions.ts +4 -4
  36. package/src/app/agentApplication.ts +1 -1
  37. package/src/app/oauth/authorization.ts +6 -8
  38. package/src/app/streaming/citation.ts +29 -0
  39. package/src/app/streaming/citationUtil.ts +76 -0
  40. package/src/app/streaming/streamingResponse.ts +407 -0
  41. package/src/cards/cardFactory.ts +2 -3
  42. package/src/oauth/index.ts +1 -4
  43. package/src/oauth/oAuthFlow.ts +39 -45
  44. package/src/oauth/userTokenClient.ts +62 -19
  45. package/src/oauth/userTokenClient.types.ts +173 -0
  46. package/src/turnContext.ts +7 -1
  47. package/dist/src/oauth/oAuthCard.d.ts +0 -27
  48. package/dist/src/oauth/oAuthCard.js +0 -5
  49. package/dist/src/oauth/oAuthCard.js.map +0 -1
  50. package/dist/src/oauth/signingResource.d.ts +0 -43
  51. package/dist/src/oauth/signingResource.js.map +0 -1
  52. package/dist/src/oauth/tokenExchangeRequest.d.ts +0 -17
  53. package/dist/src/oauth/tokenExchangeRequest.js +0 -5
  54. package/dist/src/oauth/tokenExchangeRequest.js.map +0 -1
  55. package/dist/src/oauth/tokenResponse.d.ts +0 -29
  56. package/dist/src/oauth/tokenResponse.js +0 -25
  57. package/dist/src/oauth/tokenResponse.js.map +0 -1
  58. package/src/oauth/oAuthCard.ts +0 -30
  59. package/src/oauth/signingResource.ts +0 -48
  60. package/src/oauth/tokenExchangeRequest.ts +0 -20
  61. package/src/oauth/tokenResponse.ts +0 -43
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { ClientCitation } from '@microsoft/agents-activity/src/entity/AIEntity'
7
+
8
+ // import { stringify } from 'yaml'
9
+
10
+ // import { Tokenizer } from './tokenizers'
11
+
12
+ /**
13
+ * Utility functions for manipulating text and citations.
14
+ */
15
+ export class CitationUtil {
16
+ /**
17
+ *
18
+ * Clips the text to a maximum length in case it exceeds the limit.
19
+ * @param {string} text The text to clip.
20
+ * @param {number} maxLength The maximum length of the text to return, cutting off the last whole word.
21
+ * @returns {string} The modified text
22
+ */
23
+ public static snippet (text: string, maxLength: number): string {
24
+ if (text.length <= maxLength) {
25
+ return text
26
+ }
27
+ let snippet = text.slice(0, maxLength)
28
+ snippet = snippet.slice(0, Math.min(snippet.length, snippet.lastIndexOf(' ')))
29
+ snippet += '...'
30
+ return snippet
31
+ }
32
+
33
+ /**
34
+ * Convert citation tags `[doc(s)n]` to `[n]` where n is a number.
35
+ * @param {string} text The text to format.
36
+ * @returns {string} The formatted text.
37
+ */
38
+ public static formatCitationsResponse (text: string): string {
39
+ return text.replace(/\[docs?(\d+)\]/gi, '[$1]')
40
+ }
41
+
42
+ /**
43
+ * Get the citations used in the text. This will remove any citations that are included in the citations array from the response but not referenced in the text.
44
+ * @param {string} text - The text to search for citation references, i.e. [1], [2], etc.
45
+ * @param {ClientCitation[]} citations - The list of citations to search for.
46
+ * @returns {ClientCitation[] | undefined} The list of citations used in the text.
47
+ */
48
+ public static getUsedCitations (text: string, citations: ClientCitation[]): ClientCitation[] | undefined {
49
+ const regex = /\[(\d+)\]/gi
50
+ const matches = text.match(regex)
51
+
52
+ if (!matches) {
53
+ return undefined
54
+ }
55
+
56
+ // Remove duplicates
57
+ const filteredMatches = new Set()
58
+ matches.forEach((match) => {
59
+ if (filteredMatches.has(match)) {
60
+ return
61
+ }
62
+
63
+ filteredMatches.add(match)
64
+ })
65
+
66
+ // Add citations
67
+ const usedCitations: ClientCitation[] = []
68
+ filteredMatches.forEach((match) => {
69
+ const found = citations.find((citation) => `[${citation.position}]` === match)
70
+ if (found) {
71
+ usedCitations.push(found)
72
+ }
73
+ })
74
+ return usedCitations
75
+ }
76
+ }
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { Activity, addAIToActivity, Attachment, Entity } from '@microsoft/agents-activity'
7
+ import { TurnContext } from '../../turnContext'
8
+ import { Citation } from './citation'
9
+ import { CitationUtil } from './citationUtil'
10
+ import { debug } from '../../logger'
11
+ import { ClientCitation, SensitivityUsageInfo } from '@microsoft/agents-activity/src/entity/AIEntity'
12
+
13
+ const logger = debug('agents:streamingResponse')
14
+
15
+ /**
16
+ * A helper class for streaming responses to the client.
17
+ * @remarks
18
+ * This class is used to send a series of updates to the client in a single response. The expected
19
+ * sequence of calls is:
20
+ *
21
+ * `queueInformativeUpdate()`, `queueTextChunk()`, `queueTextChunk()`, ..., `endStream()`.
22
+ *
23
+ * Once `endStream()` is called, the stream is considered ended and no further updates can be sent.
24
+ */
25
+ export class StreamingResponse {
26
+ private readonly _context: TurnContext
27
+ private _nextSequence: number = 1
28
+ private _streamId?: string
29
+ private _message: string = ''
30
+ private _attachments?: Attachment[]
31
+ private _ended = false
32
+
33
+ // Queue for outgoing activities
34
+ private _queue: Array<() => Activity> = []
35
+ private _queueSync: Promise<void> | undefined
36
+ private _chunkQueued = false
37
+
38
+ // Powered by AI feature flags
39
+ private _enableFeedbackLoop = false
40
+ private _feedbackLoopType?: 'default' | 'custom'
41
+ private _enableGeneratedByAILabel = false
42
+ private _citations?: ClientCitation[] = []
43
+ private _sensitivityLabel?: SensitivityUsageInfo
44
+
45
+ /**
46
+ * Creates a new StreamingResponse instance.
47
+ * @param {TurnContext} context - Context for the current turn of conversation with the user.
48
+ * @returns {TurnContext} - The context for the current turn of conversation with the user.
49
+ */
50
+ public constructor (context: TurnContext) {
51
+ this._context = context
52
+ }
53
+
54
+ /**
55
+ * Gets the stream ID of the current response.
56
+ * @returns {string | undefined} - The stream ID of the current response.
57
+ * @remarks
58
+ * Assigned after the initial update is sent.
59
+ */
60
+ public get streamId (): string | undefined {
61
+ return this._streamId
62
+ }
63
+
64
+ /**
65
+ * Gets the citations of the current response.
66
+ */
67
+ public get citations (): ClientCitation[] | undefined {
68
+ return this._citations
69
+ }
70
+
71
+ /**
72
+ * Gets the number of updates sent for the stream.
73
+ * @returns {number} - The number of updates sent for the stream.
74
+ */
75
+ public get updatesSent (): number {
76
+ return this._nextSequence - 1
77
+ }
78
+
79
+ /**
80
+ * Queues an informative update to be sent to the client.
81
+ * @param {string} text Text of the update to send.
82
+ */
83
+ public queueInformativeUpdate (text: string): void {
84
+ if (this._ended) {
85
+ throw new Error('The stream has already ended.')
86
+ }
87
+
88
+ // Queue a typing activity
89
+ this.queueActivity(() => Activity.fromObject({
90
+ type: 'typing',
91
+ text,
92
+ channelData: {
93
+ streamType: 'informative',
94
+ streamSequence: this._nextSequence++
95
+ } as StreamingChannelData
96
+ }))
97
+ }
98
+
99
+ /**
100
+ * Queues a chunk of partial message text to be sent to the client
101
+ * @remarks
102
+ * The text we be sent as quickly as possible to the client. Chunks may be combined before
103
+ * delivery to the client.
104
+ * @param {string} text Partial text of the message to send.
105
+ * @param {Citation[]} citations Citations to be included in the message.
106
+ */
107
+ public queueTextChunk (text: string, citations?: Citation[]): void {
108
+ if (this._ended) {
109
+ throw new Error('The stream has already ended.')
110
+ }
111
+
112
+ // Update full message text
113
+ this._message += text
114
+
115
+ // If there are citations, modify the content so that the sources are numbers instead of [doc1], [doc2], etc.
116
+ this._message = CitationUtil.formatCitationsResponse(this._message)
117
+
118
+ // Queue the next chunk
119
+ this.queueNextChunk()
120
+ }
121
+
122
+ /**
123
+ * Ends the stream by sending the final message to the client.
124
+ * @returns {Promise<void>} - A promise representing the async operation
125
+ */
126
+ public endStream (): Promise<void> {
127
+ if (this._ended) {
128
+ throw new Error('The stream has already ended.')
129
+ }
130
+
131
+ // Queue final message
132
+ this._ended = true
133
+ this.queueNextChunk()
134
+
135
+ // Wait for the queue to drain
136
+ return this.waitForQueue()
137
+ }
138
+
139
+ /**
140
+ * Sets the attachments to attach to the final chunk.
141
+ * @param attachments List of attachments.
142
+ */
143
+ public setAttachments (attachments: Attachment[]): void {
144
+ this._attachments = attachments
145
+ }
146
+
147
+ /**
148
+ * Sets the sensitivity label to attach to the final chunk.
149
+ * @param sensitivityLabel The sensitivty label.
150
+ */
151
+ public setSensitivityLabel (sensitivityLabel: SensitivityUsageInfo): void {
152
+ this._sensitivityLabel = sensitivityLabel
153
+ }
154
+
155
+ /**
156
+ * Sets the citations for the full message.
157
+ * @param {Citation[]} citations Citations to be included in the message.
158
+ */
159
+ public setCitations (citations: Citation[]): void {
160
+ if (citations.length > 0) {
161
+ if (!this._citations) {
162
+ this._citations = []
163
+ }
164
+ let currPos = this._citations.length
165
+
166
+ for (const citation of citations) {
167
+ const clientCitation: ClientCitation = {
168
+ '@type': 'Claim',
169
+ position: currPos + 1,
170
+ appearance: {
171
+ '@type': 'DigitalDocument',
172
+ name: citation.title || `Document #${currPos + 1}`,
173
+ abstract: CitationUtil.snippet(citation.content, 477)
174
+ }
175
+ }
176
+ currPos++
177
+ this._citations.push(clientCitation)
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Sets the Feedback Loop in Teams that allows a user to
184
+ * give thumbs up or down to a response.
185
+ * Default is `false`.
186
+ * @param enableFeedbackLoop If true, the feedback loop is enabled.
187
+ */
188
+ public setFeedbackLoop (enableFeedbackLoop: boolean): void {
189
+ this._enableFeedbackLoop = enableFeedbackLoop
190
+ }
191
+
192
+ /**
193
+ * Sets the type of UI to use for the feedback loop.
194
+ * @param feedbackLoopType The type of the feedback loop.
195
+ */
196
+ public setFeedbackLoopType (feedbackLoopType: 'default' | 'custom'): void {
197
+ this._feedbackLoopType = feedbackLoopType
198
+ }
199
+
200
+ /**
201
+ * Sets the the Generated by AI label in Teams
202
+ * Default is `false`.
203
+ * @param enableGeneratedByAILabel If true, the label is added.
204
+ */
205
+ public setGeneratedByAILabel (enableGeneratedByAILabel: boolean): void {
206
+ this._enableGeneratedByAILabel = enableGeneratedByAILabel
207
+ }
208
+
209
+ /**
210
+ * Returns the most recently streamed message.
211
+ * @returns The streamed message.
212
+ */
213
+ public getMessage (): string {
214
+ return this._message
215
+ }
216
+
217
+ /**
218
+ * Waits for the outgoing activity queue to be empty.
219
+ * @returns {Promise<void>} - A promise representing the async operation.
220
+ */
221
+ public waitForQueue (): Promise<void> {
222
+ return this._queueSync || Promise.resolve()
223
+ }
224
+
225
+ /**
226
+ * Queues the next chunk of text to be sent to the client.
227
+ * @private
228
+ */
229
+ private queueNextChunk (): void {
230
+ // Are we already waiting to send a chunk?
231
+ if (this._chunkQueued) {
232
+ return
233
+ }
234
+
235
+ // Queue a chunk of text to be sent
236
+ this._chunkQueued = true
237
+ this.queueActivity(() => {
238
+ this._chunkQueued = false
239
+ if (this._ended) {
240
+ // Send final message
241
+ return Activity.fromObject({
242
+ type: 'message',
243
+ text: this._message || 'end strean response',
244
+ attachments: this._attachments,
245
+ channelData: {
246
+ streamType: 'final',
247
+ streamSequence: this._nextSequence++
248
+ } as StreamingChannelData
249
+ })
250
+ } else {
251
+ // Send typing activity
252
+ return Activity.fromObject({
253
+ type: 'typing',
254
+ text: this._message,
255
+ channelData: {
256
+ streamType: 'streaming',
257
+ streamSequence: this._nextSequence++
258
+ } as StreamingChannelData
259
+ })
260
+ }
261
+ })
262
+ }
263
+
264
+ /**
265
+ * Queues an activity to be sent to the client.
266
+ */
267
+ private queueActivity (factory: () => Activity): void {
268
+ this._queue.push(factory)
269
+
270
+ // If there's no sync in progress, start one
271
+ if (!this._queueSync) {
272
+ this._queueSync = this.drainQueue().catch((err) => {
273
+ logger.error(`Error occured when sending activity while streaming: "${err}".`)
274
+ // throw err
275
+ })
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Sends any queued activities to the client until the queue is empty.
281
+ * @returns {Promise<void>} - A promise that will be resolved once the queue is empty.
282
+ * @private
283
+ */
284
+ private async drainQueue (): Promise<void> {
285
+ // eslint-disable-next-line no-async-promise-executor
286
+ return new Promise<void>(async (resolve, reject) => {
287
+ try {
288
+ logger.debug(`Draining queue with ${this._queue.length} activities.`)
289
+ while (this._queue.length > 0) {
290
+ const factory = this._queue.shift()!
291
+ const activity = factory()
292
+ await this.sendActivity(activity)
293
+ }
294
+
295
+ resolve()
296
+ } catch (err) {
297
+ reject(err)
298
+ } finally {
299
+ this._queueSync = undefined
300
+ }
301
+ })
302
+ }
303
+
304
+ /**
305
+ * Sends an activity to the client and saves the stream ID returned.
306
+ * @param {Activity} activity - The activity to send.
307
+ * @returns {Promise<void>} - A promise representing the async operation.
308
+ * @private
309
+ */
310
+ private async sendActivity (activity: Activity): Promise<void> {
311
+ // Set activity ID to the assigned stream ID
312
+ if (this._streamId) {
313
+ activity.id = this._streamId
314
+ activity.channelData = Object.assign({}, activity.channelData, { streamId: this._streamId })
315
+ }
316
+
317
+ activity.entities = [
318
+ {
319
+ type: 'streaminfo',
320
+ ...activity.channelData
321
+ } as Entity
322
+ ]
323
+
324
+ if (this._citations && this._citations.length > 0 && !this._ended) {
325
+ // Filter out the citations unused in content.
326
+ const currCitations = CitationUtil.getUsedCitations(this._message, this._citations) ?? undefined
327
+ activity.entities.push({
328
+ type: 'https://schema.org/Message',
329
+ '@type': 'Message',
330
+ '@context': 'https://schema.org',
331
+ '@id': '',
332
+ citation: currCitations
333
+ } as unknown as Entity)
334
+ }
335
+
336
+ // Add in Powered by AI feature flags
337
+ if (this._ended) {
338
+ if (this._enableFeedbackLoop && this._feedbackLoopType) {
339
+ activity.channelData = Object.assign({}, activity.channelData, {
340
+ feedbackLoop: { type: this._feedbackLoopType }
341
+ })
342
+ } else {
343
+ activity.channelData = Object.assign({}, activity.channelData, {
344
+ feedbackLoopEnabled: this._enableFeedbackLoop
345
+ })
346
+ }
347
+
348
+ // Add in Generated by AI
349
+ if (this._enableGeneratedByAILabel) {
350
+ addAIToActivity(activity, this._citations, this._sensitivityLabel)
351
+ // activity.entities.push({
352
+ // type: 'https://schema.org/Message',
353
+ // '@type': 'Message',
354
+ // '@context': 'https://schema.org',
355
+ // '@id': '',
356
+ // additionalType: ['AIGeneratedContent'],
357
+ // citation: this._citations && this._citations.length > 0 ? this._citations : [],
358
+ // usageInfo: this._sensitivityLabel
359
+ // } as unknown as Entity)
360
+ }
361
+ }
362
+
363
+ // Send activity
364
+ const response = await this._context.sendActivity(activity)
365
+ await new Promise((resolve) => setTimeout(resolve, 1500))
366
+
367
+ // Save assigned stream ID
368
+ if (!this._streamId) {
369
+ this._streamId = response?.id
370
+ }
371
+ }
372
+ }
373
+
374
+ /**
375
+ * @private
376
+ * Structure of the outgoing channelData field for streaming responses.
377
+ * @remarks
378
+ * The expected sequence of streamTypes is:
379
+ *
380
+ * `informative`, `streaming`, `streaming`, ..., `final`.
381
+ *
382
+ * Once a `final` message is sent, the stream is considered ended.
383
+ */
384
+ interface StreamingChannelData {
385
+ /**
386
+ * The type of message being sent.
387
+ * @remarks
388
+ * `informative` - An informative update.
389
+ * `streaming` - A chunk of partial message text.
390
+ * `final` - The final message.
391
+ */
392
+ streamType: 'informative' | 'streaming' | 'final';
393
+
394
+ /**
395
+ * Sequence number of the message in the stream.
396
+ * @remarks
397
+ * Starts at 1 for the first message and increments from there.
398
+ */
399
+ streamSequence: number;
400
+
401
+ /**
402
+ * ID of the stream.
403
+ * @remarks
404
+ * Assigned after the initial update is sent.
405
+ */
406
+ streamId?: string;
407
+ }
@@ -13,8 +13,7 @@ import { O365ConnectorCard } from './o365ConnectorCard'
13
13
  import { ThumbnailCard } from './thumbnailCard'
14
14
  import { VideoCard } from './videoCard'
15
15
  import { CardImage } from './cardImage'
16
- import { OAuthCard } from '../oauth/oAuthCard'
17
- import { SigningResource } from '../oauth/signingResource'
16
+ import { OAuthCard, SignInResource } from '../oauth/userTokenClient.types'
18
17
  import { SigninCard } from './signinCard'
19
18
 
20
19
  /**
@@ -218,7 +217,7 @@ export class CardFactory {
218
217
  * @param signingResource The signing resource.
219
218
  * @returns The OAuth card attachment.
220
219
  */
221
- static oauthCard (connectionName: string, title: string, text: string, signingResource: SigningResource) : Attachment {
220
+ static oauthCard (connectionName: string, title: string, text: string, signingResource: SignInResource) : Attachment {
222
221
  const card: Partial<OAuthCard> = {
223
222
  buttons: [{
224
223
  type: ActionTypes.Signin,
@@ -1,6 +1,3 @@
1
- export * from './oAuthCard'
2
- export * from './signingResource'
3
1
  export * from './userTokenClient'
4
- export * from './tokenExchangeRequest'
5
2
  export * from './oAuthFlow'
6
- export * from './tokenResponse'
3
+ export * from './userTokenClient.types'
@@ -4,20 +4,18 @@ import { debug } from './../logger'
4
4
  import { ActivityTypes, Attachment } from '@microsoft/agents-activity'
5
5
  import {
6
6
  CardFactory,
7
- AgentStatePropertyAccessor,
8
- UserState,
9
7
  TurnContext,
8
+ Storage,
10
9
  MessageFactory,
11
- TokenExchangeRequest,
12
- UserTokenClient
13
10
  } from '../'
14
- import { TokenRequestStatus, TokenResponse } from './tokenResponse'
11
+ import { UserTokenClient } from './userTokenClient'
12
+ import { TokenExchangeRequest, TokenResponse } from './userTokenClient.types'
15
13
 
16
14
  const logger = debug('agents:oauth-flow')
17
15
 
18
- export class FlowState {
19
- public flowStarted: boolean = false
20
- public flowExpires: number = 0
16
+ export interface FlowState {
17
+ flowStarted: boolean,
18
+ flowExpires: number
21
19
  }
22
20
 
23
21
  interface TokenVerifyState {
@@ -35,12 +33,7 @@ export class OAuthFlow {
35
33
  /**
36
34
  * The current state of the OAuth flow.
37
35
  */
38
- state: FlowState | null
39
-
40
- /**
41
- * The accessor for managing the flow state in user state.
42
- */
43
- flowStateAccessor: AgentStatePropertyAccessor<FlowState | null>
36
+ state: FlowState
44
37
 
45
38
  /**
46
39
  * The ID of the token exchange request, used to deduplicate requests.
@@ -66,9 +59,8 @@ export class OAuthFlow {
66
59
  * Creates a new instance of OAuthFlow.
67
60
  * @param userState The user state.
68
61
  */
69
- constructor (userState: UserState, absOauthConnectionName: string, tokenClient?: UserTokenClient, cardTitle?: string, cardText?: string) {
70
- this.state = new FlowState()
71
- this.flowStateAccessor = userState.createProperty('flowState')
62
+ constructor (private storage: Storage, absOauthConnectionName: string, tokenClient?: UserTokenClient, cardTitle?: string, cardText?: string) {
63
+ this.state = { flowExpires: 0, flowStarted: false }
72
64
  this.absOauthConnectionName = absOauthConnectionName
73
65
  this.userTokenClient = tokenClient ?? null!
74
66
  this.cardTitle = cardTitle ?? this.cardTitle
@@ -97,7 +89,7 @@ export class OAuthFlow {
97
89
  * @param context The turn context.
98
90
  * @returns A promise that resolves to the user token.
99
91
  */
100
- public async beginFlow (context: TurnContext): Promise<TokenResponse> {
92
+ public async beginFlow (context: TurnContext): Promise<TokenResponse | undefined> {
101
93
  this.state = await this.getUserState(context)
102
94
  if (this.absOauthConnectionName === '') {
103
95
  throw new Error('connectionName is not set')
@@ -105,27 +97,20 @@ export class OAuthFlow {
105
97
  logger.info('Starting OAuth flow for connectionName:', this.absOauthConnectionName)
106
98
  await this.initializeTokenClient(context)
107
99
 
108
- const tokenResponse = await this.userTokenClient.getUserToken(this.absOauthConnectionName, context.activity.channelId!, context.activity.from?.id!)
109
- if (tokenResponse?.status === TokenRequestStatus.Success) {
100
+ const act = context.activity
101
+ const output = await this.userTokenClient.getTokenOrSignInResource(act.from?.id!, this.absOauthConnectionName, act.channelId!, act.getConversationReference(), act.relatesTo!, undefined!)
102
+ if (output && output.tokenResponse) {
110
103
  this.state.flowStarted = false
111
104
  this.state.flowExpires = 0
112
- await this.flowStateAccessor.set(context, this.state)
113
- logger.info('User token retrieved successfully from service')
114
- return tokenResponse
105
+ await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
106
+ return output.tokenResponse
115
107
  }
116
-
117
- const authConfig = context.adapter.authConfig
118
- const signingResource = await this.userTokenClient.getSignInResource(authConfig.clientId!, this.absOauthConnectionName, context.activity.getConversationReference(), context.activity.relatesTo)
119
- const oCard: Attachment = CardFactory.oauthCard(this.absOauthConnectionName, this.cardTitle, this.cardText, signingResource)
108
+ const oCard: Attachment = CardFactory.oauthCard(this.absOauthConnectionName, this.cardTitle, this.cardText, output.signInResource)
120
109
  await context.sendActivity(MessageFactory.attachment(oCard))
121
110
  this.state.flowStarted = true
122
111
  this.state.flowExpires = Date.now() + 30000
123
- await this.flowStateAccessor.set(context, this.state)
124
- logger.info('OAuth begin flow completed, waiting for user to sign in')
125
- return {
126
- token: undefined,
127
- status: TokenRequestStatus.InProgress
128
- }
112
+ await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
113
+ return undefined
129
114
  }
130
115
 
131
116
  /**
@@ -140,7 +125,7 @@ export class OAuthFlow {
140
125
  logger.warn('Flow expired')
141
126
  this.state!.flowStarted = false
142
127
  await context.sendActivity(MessageFactory.text('Sign-in session expired. Please try again.'))
143
- return { status: TokenRequestStatus.Expired, token: undefined }
128
+ return { token: undefined }
144
129
  }
145
130
  const contFlowActivity = context.activity
146
131
  if (contFlowActivity.type === ActivityTypes.Message) {
@@ -161,22 +146,22 @@ export class OAuthFlow {
161
146
  logger.info('Continuing OAuth flow with tokenExchange')
162
147
  const tokenExchangeRequest = contFlowActivity.value as TokenExchangeRequest
163
148
  if (this.tokenExchangeId === tokenExchangeRequest.id) { // dedupe
164
- return { status: TokenRequestStatus.InProgress, token: undefined }
149
+ return { token: undefined }
165
150
  }
166
151
  this.tokenExchangeId = tokenExchangeRequest.id!
167
152
  const userTokenResp = await this.userTokenClient?.exchangeTokenAsync(contFlowActivity.from?.id!, this.absOauthConnectionName, contFlowActivity.channelId!, tokenExchangeRequest)
168
- if (userTokenResp?.status === TokenRequestStatus.Success) {
153
+ if (userTokenResp && userTokenResp.token) {
169
154
  logger.info('Token exchanged')
170
155
  this.state!.flowStarted = false
171
- await this.flowStateAccessor.set(context, this.state)
156
+ await this.storage.write({ [this.getFlowStateKey(context)]: this.state })
172
157
  return userTokenResp
173
158
  } else {
174
159
  logger.warn('Token exchange failed')
175
160
  this.state!.flowStarted = true
176
- return { status: TokenRequestStatus.Failed, token: undefined }
161
+ return { token: undefined }
177
162
  }
178
163
  }
179
- return { status: TokenRequestStatus.Failed, token: undefined }
164
+ return { token: undefined }
180
165
  }
181
166
 
182
167
  /**
@@ -189,7 +174,7 @@ export class OAuthFlow {
189
174
  await this.initializeTokenClient(context)
190
175
  await this.userTokenClient?.signOut(context.activity.from?.id as string, this.absOauthConnectionName, context.activity.channelId as string)
191
176
  this.state!.flowExpires = 0
192
- await this.flowStateAccessor.set(context, this.state)
177
+ this.storage.write({ [this.getFlowStateKey(context)]: this.state })
193
178
  logger.info('User signed out successfully')
194
179
  }
195
180
 
@@ -199,10 +184,9 @@ export class OAuthFlow {
199
184
  * @returns A promise that resolves to the user state.
200
185
  */
201
186
  private async getUserState (context: TurnContext) {
202
- let userProfile: FlowState | null = await this.flowStateAccessor.get(context, null)
203
- if (userProfile === null) {
204
- userProfile = new FlowState()
205
- }
187
+ const key = this.getFlowStateKey(context)
188
+ const data = await this.storage.read([key])
189
+ const userProfile: FlowState = data[key] ?? { flowStarted: false, flowExpires: 0 }
206
190
  return userProfile
207
191
  }
208
192
 
@@ -210,7 +194,17 @@ export class OAuthFlow {
210
194
  if (this.userTokenClient === undefined || this.userTokenClient === null) {
211
195
  const scope = 'https://api.botframework.com'
212
196
  const accessToken = await context.adapter.authProvider.getAccessToken(context.adapter.authConfig, scope)
213
- this.userTokenClient = new UserTokenClient(accessToken)
197
+ this.userTokenClient = new UserTokenClient(accessToken, context.adapter.authConfig.clientId!)
198
+ }
199
+ }
200
+
201
+ private getFlowStateKey (context: TurnContext): string {
202
+ const channelId = context.activity.channelId
203
+ const conversationId = context.activity.conversation?.id
204
+ const userId = context.activity.from?.id
205
+ if (!channelId || !conversationId || !userId) {
206
+ throw new Error('ChannelId, conversationId, and userId must be set in the activity')
214
207
  }
208
+ return `oauth/${channelId}/${conversationId}/${userId}/flowState`
215
209
  }
216
210
  }