@fluidframework/odsp-driver 0.54.2 → 0.56.0-49831

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 (149) hide show
  1. package/.eslintrc.js +1 -2
  2. package/dist/checkUrl.d.ts.map +1 -1
  3. package/dist/checkUrl.js +0 -1
  4. package/dist/checkUrl.js.map +1 -1
  5. package/dist/contractsPublic.d.ts +7 -1
  6. package/dist/contractsPublic.d.ts.map +1 -1
  7. package/dist/contractsPublic.js +7 -1
  8. package/dist/contractsPublic.js.map +1 -1
  9. package/dist/createFile.d.ts +3 -3
  10. package/dist/createFile.d.ts.map +1 -1
  11. package/dist/createFile.js +7 -7
  12. package/dist/createFile.js.map +1 -1
  13. package/dist/epochTracker.d.ts +3 -3
  14. package/dist/epochTracker.d.ts.map +1 -1
  15. package/dist/epochTracker.js +23 -10
  16. package/dist/epochTracker.js.map +1 -1
  17. package/dist/fetchSnapshot.d.ts +3 -2
  18. package/dist/fetchSnapshot.d.ts.map +1 -1
  19. package/dist/fetchSnapshot.js +61 -47
  20. package/dist/fetchSnapshot.js.map +1 -1
  21. package/dist/getFileLink.js +4 -4
  22. package/dist/getFileLink.js.map +1 -1
  23. package/dist/getUrlAndHeadersWithAuth.d.ts +1 -1
  24. package/dist/getUrlAndHeadersWithAuth.d.ts.map +1 -1
  25. package/dist/getUrlAndHeadersWithAuth.js +20 -33
  26. package/dist/getUrlAndHeadersWithAuth.js.map +1 -1
  27. package/dist/odspDeltaStorageService.d.ts.map +1 -1
  28. package/dist/odspDeltaStorageService.js +1 -4
  29. package/dist/odspDeltaStorageService.js.map +1 -1
  30. package/dist/odspDocumentDeltaConnection.js +1 -1
  31. package/dist/odspDocumentDeltaConnection.js.map +1 -1
  32. package/dist/odspDocumentService.d.ts.map +1 -1
  33. package/dist/odspDocumentService.js +9 -0
  34. package/dist/odspDocumentService.js.map +1 -1
  35. package/dist/odspDocumentServiceFactoryCore.d.ts.map +1 -1
  36. package/dist/odspDocumentServiceFactoryCore.js +2 -2
  37. package/dist/odspDocumentServiceFactoryCore.js.map +1 -1
  38. package/dist/odspDocumentStorageManager.d.ts +1 -0
  39. package/dist/odspDocumentStorageManager.d.ts.map +1 -1
  40. package/dist/odspDocumentStorageManager.js +16 -9
  41. package/dist/odspDocumentStorageManager.js.map +1 -1
  42. package/dist/odspDriverUrlResolver.d.ts.map +1 -1
  43. package/dist/odspDriverUrlResolver.js +5 -2
  44. package/dist/odspDriverUrlResolver.js.map +1 -1
  45. package/dist/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  46. package/dist/odspDriverUrlResolverForShareLink.js +1 -2
  47. package/dist/odspDriverUrlResolverForShareLink.js.map +1 -1
  48. package/dist/odspSummaryUploadManager.d.ts +2 -1
  49. package/dist/odspSummaryUploadManager.d.ts.map +1 -1
  50. package/dist/odspSummaryUploadManager.js +3 -2
  51. package/dist/odspSummaryUploadManager.js.map +1 -1
  52. package/dist/packageVersion.d.ts +1 -1
  53. package/dist/packageVersion.d.ts.map +1 -1
  54. package/dist/packageVersion.js +1 -1
  55. package/dist/packageVersion.js.map +1 -1
  56. package/dist/prefetchLatestSnapshot.d.ts +2 -1
  57. package/dist/prefetchLatestSnapshot.d.ts.map +1 -1
  58. package/dist/prefetchLatestSnapshot.js +3 -2
  59. package/dist/prefetchLatestSnapshot.js.map +1 -1
  60. package/dist/retryErrorsStorageAdapter.d.ts +1 -1
  61. package/dist/retryErrorsStorageAdapter.d.ts.map +1 -1
  62. package/dist/retryErrorsStorageAdapter.js.map +1 -1
  63. package/dist/vroom.d.ts.map +1 -1
  64. package/dist/vroom.js +19 -20
  65. package/dist/vroom.js.map +1 -1
  66. package/lib/checkUrl.d.ts.map +1 -1
  67. package/lib/checkUrl.js +0 -1
  68. package/lib/checkUrl.js.map +1 -1
  69. package/lib/contractsPublic.d.ts +7 -1
  70. package/lib/contractsPublic.d.ts.map +1 -1
  71. package/lib/contractsPublic.js +6 -0
  72. package/lib/contractsPublic.js.map +1 -1
  73. package/lib/createFile.d.ts +3 -3
  74. package/lib/createFile.d.ts.map +1 -1
  75. package/lib/createFile.js +7 -7
  76. package/lib/createFile.js.map +1 -1
  77. package/lib/epochTracker.d.ts +3 -3
  78. package/lib/epochTracker.d.ts.map +1 -1
  79. package/lib/epochTracker.js +24 -11
  80. package/lib/epochTracker.js.map +1 -1
  81. package/lib/fetchSnapshot.d.ts +3 -2
  82. package/lib/fetchSnapshot.d.ts.map +1 -1
  83. package/lib/fetchSnapshot.js +61 -47
  84. package/lib/fetchSnapshot.js.map +1 -1
  85. package/lib/getFileLink.js +4 -4
  86. package/lib/getFileLink.js.map +1 -1
  87. package/lib/getUrlAndHeadersWithAuth.d.ts +1 -1
  88. package/lib/getUrlAndHeadersWithAuth.d.ts.map +1 -1
  89. package/lib/getUrlAndHeadersWithAuth.js +20 -33
  90. package/lib/getUrlAndHeadersWithAuth.js.map +1 -1
  91. package/lib/odspDeltaStorageService.d.ts.map +1 -1
  92. package/lib/odspDeltaStorageService.js +1 -4
  93. package/lib/odspDeltaStorageService.js.map +1 -1
  94. package/lib/odspDocumentDeltaConnection.js +1 -1
  95. package/lib/odspDocumentDeltaConnection.js.map +1 -1
  96. package/lib/odspDocumentService.d.ts.map +1 -1
  97. package/lib/odspDocumentService.js +9 -0
  98. package/lib/odspDocumentService.js.map +1 -1
  99. package/lib/odspDocumentServiceFactoryCore.d.ts.map +1 -1
  100. package/lib/odspDocumentServiceFactoryCore.js +2 -2
  101. package/lib/odspDocumentServiceFactoryCore.js.map +1 -1
  102. package/lib/odspDocumentStorageManager.d.ts +1 -0
  103. package/lib/odspDocumentStorageManager.d.ts.map +1 -1
  104. package/lib/odspDocumentStorageManager.js +16 -9
  105. package/lib/odspDocumentStorageManager.js.map +1 -1
  106. package/lib/odspDriverUrlResolver.d.ts.map +1 -1
  107. package/lib/odspDriverUrlResolver.js +5 -2
  108. package/lib/odspDriverUrlResolver.js.map +1 -1
  109. package/lib/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  110. package/lib/odspDriverUrlResolverForShareLink.js +1 -2
  111. package/lib/odspDriverUrlResolverForShareLink.js.map +1 -1
  112. package/lib/odspSummaryUploadManager.d.ts +2 -1
  113. package/lib/odspSummaryUploadManager.d.ts.map +1 -1
  114. package/lib/odspSummaryUploadManager.js +3 -2
  115. package/lib/odspSummaryUploadManager.js.map +1 -1
  116. package/lib/packageVersion.d.ts +1 -1
  117. package/lib/packageVersion.d.ts.map +1 -1
  118. package/lib/packageVersion.js +1 -1
  119. package/lib/packageVersion.js.map +1 -1
  120. package/lib/prefetchLatestSnapshot.d.ts +2 -1
  121. package/lib/prefetchLatestSnapshot.d.ts.map +1 -1
  122. package/lib/prefetchLatestSnapshot.js +3 -2
  123. package/lib/prefetchLatestSnapshot.js.map +1 -1
  124. package/lib/retryErrorsStorageAdapter.d.ts +1 -1
  125. package/lib/retryErrorsStorageAdapter.d.ts.map +1 -1
  126. package/lib/retryErrorsStorageAdapter.js.map +1 -1
  127. package/lib/vroom.d.ts.map +1 -1
  128. package/lib/vroom.js +19 -20
  129. package/lib/vroom.js.map +1 -1
  130. package/package.json +19 -18
  131. package/src/checkUrl.ts +0 -1
  132. package/src/contractsPublic.ts +11 -1
  133. package/src/createFile.ts +16 -4
  134. package/src/epochTracker.ts +24 -8
  135. package/src/fetchSnapshot.ts +74 -47
  136. package/src/getFileLink.ts +7 -2
  137. package/src/getUrlAndHeadersWithAuth.ts +24 -38
  138. package/src/odspDeltaStorageService.ts +1 -3
  139. package/src/odspDocumentDeltaConnection.ts +1 -1
  140. package/src/odspDocumentService.ts +9 -0
  141. package/src/odspDocumentServiceFactoryCore.ts +1 -0
  142. package/src/odspDocumentStorageManager.ts +39 -8
  143. package/src/odspDriverUrlResolver.ts +3 -0
  144. package/src/odspDriverUrlResolverForShareLink.ts +1 -2
  145. package/src/odspSummaryUploadManager.ts +6 -1
  146. package/src/packageVersion.ts +1 -1
  147. package/src/prefetchLatestSnapshot.ts +3 -0
  148. package/src/retryErrorsStorageAdapter.ts +1 -1
  149. package/src/vroom.ts +23 -23
package/src/createFile.ts CHANGED
@@ -55,6 +55,7 @@ export async function createNewFluidFile(
55
55
  epochTracker: EpochTracker,
56
56
  fileEntry: IFileEntry,
57
57
  createNewCaching: boolean,
58
+ forceAccessTokenViaAuthorizationHeader: boolean,
58
59
  ): Promise<IOdspResolvedUrl> {
59
60
  // Check for valid filename before the request to create file is actually made.
60
61
  if (isInvalidFileName(newFileInfo.filename)) {
@@ -67,10 +68,17 @@ export async function createNewFluidFile(
67
68
  let sharingLink: string | undefined;
68
69
  let sharingLinkErrorReason: string | undefined;
69
70
  if (createNewSummary === undefined) {
70
- itemId = await createNewEmptyFluidFile(getStorageToken, newFileInfo, logger, epochTracker);
71
+ itemId = await createNewEmptyFluidFile(
72
+ getStorageToken, newFileInfo, logger, epochTracker, forceAccessTokenViaAuthorizationHeader);
71
73
  } else {
72
74
  const content = await createNewFluidFileFromSummary(
73
- getStorageToken, newFileInfo, logger, createNewSummary, epochTracker);
75
+ getStorageToken,
76
+ newFileInfo,
77
+ logger,
78
+ createNewSummary,
79
+ epochTracker,
80
+ forceAccessTokenViaAuthorizationHeader,
81
+ );
74
82
  itemId = content.itemId;
75
83
  summaryHandle = content.id;
76
84
  sharingLink = content.sharingLink;
@@ -108,6 +116,7 @@ export async function createNewEmptyFluidFile(
108
116
  newFileInfo: INewFileInfo,
109
117
  logger: ITelemetryLogger,
110
118
  epochTracker: EpochTracker,
119
+ forceAccessTokenViaAuthorizationHeader: boolean,
111
120
  ): Promise<string> {
112
121
  const filePath = newFileInfo.filePath ? encodeURIComponent(`/${newFileInfo.filePath}`) : "";
113
122
  // add .tmp extension to empty file (host is expected to rename)
@@ -123,7 +132,8 @@ export async function createNewEmptyFluidFile(
123
132
  logger,
124
133
  { eventName: "createNewEmptyFile" },
125
134
  async (event) => {
126
- const { url, headers } = getUrlAndHeadersWithAuth(initialUrl, storageToken);
135
+ const { url, headers } = getUrlAndHeadersWithAuth(
136
+ initialUrl, storageToken, forceAccessTokenViaAuthorizationHeader);
127
137
  headers["Content-Type"] = "application/json";
128
138
 
129
139
  const fetchResponse = await runWithRetry(
@@ -163,6 +173,7 @@ export async function createNewFluidFileFromSummary(
163
173
  logger: ITelemetryLogger,
164
174
  createNewSummary: ISummaryTree,
165
175
  epochTracker: EpochTracker,
176
+ forceAccessTokenViaAuthorizationHeader: boolean,
166
177
  ): Promise<ICreateFileResponse> {
167
178
  const filePath = newFileInfo.filePath ? encodeURIComponent(`/${newFileInfo.filePath}`) : "";
168
179
  const encodedFilename = encodeURIComponent(newFileInfo.filename);
@@ -181,7 +192,8 @@ export async function createNewFluidFileFromSummary(
181
192
  logger,
182
193
  { eventName: "createNewFile" },
183
194
  async (event) => {
184
- const { url, headers } = getUrlAndHeadersWithAuth(initialUrl, storageToken);
195
+ const { url, headers } = getUrlAndHeadersWithAuth(
196
+ initialUrl, storageToken, forceAccessTokenViaAuthorizationHeader);
185
197
  headers["Content-Type"] = "application/json";
186
198
 
187
199
  const fetchResponse = await runWithRetry(
@@ -18,13 +18,14 @@ import {
18
18
  } from "@fluidframework/odsp-driver-definitions";
19
19
  import { DriverErrorType } from "@fluidframework/driver-definitions";
20
20
  import { PerformanceEvent, isFluidError, normalizeError } from "@fluidframework/telemetry-utils";
21
- import { fetchAndParseAsJSONHelper, fetchArray, IOdspResponse } from "./odspUtils";
21
+ import { fetchAndParseAsJSONHelper, fetchArray, getOdspResolvedUrl, IOdspResponse } from "./odspUtils";
22
22
  import {
23
23
  IOdspCache,
24
24
  INonPersistentCache,
25
25
  IPersistedFileCache,
26
26
  } from "./odspCache";
27
27
  import { IVersionedValueWithEpoch, persistedCacheValueVersion } from "./contracts";
28
+ import { ClpCompliantAppHeader } from "./contractsPublic";
28
29
 
29
30
  export type FetchType = "blob" | "createBlob" | "createFile" | "joinSession" | "ops" | "test" | "snapshotTree" |
30
31
  "treesLatest" | "uploadSummary" | "push" | "versions";
@@ -33,6 +34,7 @@ export type FetchTypeInternal = FetchType | "cache";
33
34
 
34
35
  export const Odsp409Error = "Odsp409Error";
35
36
 
37
+ // Please update the README file in odsp-driver-definitions if you change the defaultCacheExpiryTimeoutMs.
36
38
  export const defaultCacheExpiryTimeoutMs: number = 2 * 24 * 60 * 60 * 1000;
37
39
 
38
40
  /**
@@ -169,8 +171,9 @@ export class EpochTracker implements IPersistedFileCache {
169
171
  fetchOptions: RequestInit,
170
172
  fetchType: FetchType,
171
173
  addInBody: boolean = false,
174
+ fetchReason?: string,
172
175
  ): Promise<IOdspResponse<T>> {
173
- const clientCorrelationId = this.formatClientCorrelationId();
176
+ const clientCorrelationId = this.formatClientCorrelationId(fetchReason);
174
177
  // Add epoch in fetch request.
175
178
  this.addEpochInRequest(fetchOptions, addInBody, clientCorrelationId);
176
179
  let epochFromResponse: string | undefined;
@@ -210,8 +213,9 @@ export class EpochTracker implements IPersistedFileCache {
210
213
  fetchOptions: {[index: string]: any},
211
214
  fetchType: FetchType,
212
215
  addInBody: boolean = false,
216
+ fetchReason?: string,
213
217
  ) {
214
- const clientCorrelationId = this.formatClientCorrelationId();
218
+ const clientCorrelationId = this.formatClientCorrelationId(fetchReason);
215
219
  // Add epoch in fetch request.
216
220
  this.addEpochInRequest(fetchOptions, addInBody, clientCorrelationId);
217
221
  let epochFromResponse: string | undefined;
@@ -244,12 +248,16 @@ export class EpochTracker implements IPersistedFileCache {
244
248
  addInBody: boolean,
245
249
  clientCorrelationId: string,
246
250
  ) {
251
+ const isClpCompliantApp = getOdspResolvedUrl(this.fileEntry.resolvedUrl).isClpCompliantApp;
247
252
  if (addInBody) {
248
253
  const headers: {[key: string]: string} = {};
249
254
  headers["X-RequestStats"] = clientCorrelationId;
250
255
  if (this.fluidEpoch !== undefined) {
251
256
  headers["x-fluid-epoch"] = this.fluidEpoch;
252
257
  }
258
+ if (isClpCompliantApp) {
259
+ headers[ClpCompliantAppHeader.isClpCompliantApp] = isClpCompliantApp.toString();
260
+ }
253
261
  this.addParamInBody(fetchOptions, headers);
254
262
  } else {
255
263
  const addHeader = (key: string, val: string) => {
@@ -263,6 +271,9 @@ export class EpochTracker implements IPersistedFileCache {
263
271
  if (this.fluidEpoch !== undefined) {
264
272
  addHeader("x-fluid-epoch", this.fluidEpoch);
265
273
  }
274
+ if (isClpCompliantApp) {
275
+ addHeader(ClpCompliantAppHeader.isClpCompliantApp, isClpCompliantApp.toString());
276
+ }
266
277
  }
267
278
  }
268
279
 
@@ -284,8 +295,12 @@ export class EpochTracker implements IPersistedFileCache {
284
295
  fetchOptions.body = formParams.join("\r\n");
285
296
  }
286
297
 
287
- private formatClientCorrelationId() {
288
- return `driverId=${this.driverId}, RequestNumber=${this.networkCallNumber++}`;
298
+ private formatClientCorrelationId(fetchReason?: string) {
299
+ const items: string[] = [`driverId=${this.driverId}`, `RequestNumber=${this.networkCallNumber++}`];
300
+ if (fetchReason !== undefined) {
301
+ items.push(`fetchReason=${fetchReason}`);
302
+ }
303
+ return items.join(", ");
289
304
  }
290
305
 
291
306
  protected validateEpochFromResponse(
@@ -394,13 +409,14 @@ export class EpochTrackerWithRedemption extends EpochTracker {
394
409
  fetchOptions: {[index: string]: any},
395
410
  fetchType: FetchType,
396
411
  addInBody: boolean = false,
412
+ fetchReason?: string,
397
413
  ): Promise<IOdspResponse<T>> {
398
414
  // Optimize the flow if we know that treesLatestDeferral was already completed by the timer we started
399
415
  // joinSession call. If we did - there is no reason to repeat the call as it will fail with same error.
400
416
  const completed = this.treesLatestDeferral.isCompleted;
401
417
 
402
418
  try {
403
- return await super.fetchAndParseAsJSON<T>(url, fetchOptions, fetchType, addInBody);
419
+ return await super.fetchAndParseAsJSON<T>(url, fetchOptions, fetchType, addInBody, fetchReason);
404
420
  } catch (error) {
405
421
  // Only handling here treesLatest. If createFile failed, we should never try to do joinSession.
406
422
  // Similar, if getVersions failed, we should not do any further storage calls.
@@ -428,8 +444,8 @@ export class EpochTrackerWithRedemption extends EpochTracker {
428
444
  async (event) => {
429
445
  const timeoutRes = 51; // anything will work here
430
446
  let timer: ReturnType<typeof setTimeout>;
431
- const timeoutP = new Promise<number>((accept) => {
432
- timer = setTimeout(() => { accept(timeoutRes); }, 15000);
447
+ const timeoutP = new Promise<number>((resolve) => {
448
+ timer = setTimeout(() => { resolve(timeoutRes); }, 15000);
433
449
  });
434
450
  const res = await Promise.race([
435
451
  timeoutP,
@@ -39,6 +39,7 @@ import { EpochTracker } from "./epochTracker";
39
39
  * @param storageFetchWrapper - Implementation of the get/post methods used to fetch the snapshot
40
40
  * @param versionId - id of specific snapshot to be fetched
41
41
  * @param fetchFullSnapshot - whether we want to fetch full snapshot(with blobs)
42
+ * @param forceAccessTokenViaAuthorizationHeader - whether to force passing given token via authorization header
42
43
  * @returns A promise of the snapshot and the status code of the response
43
44
  */
44
45
  export async function fetchSnapshot(
@@ -46,6 +47,7 @@ export async function fetchSnapshot(
46
47
  token: string | null,
47
48
  versionId: string,
48
49
  fetchFullSnapshot: boolean,
50
+ forceAccessTokenViaAuthorizationHeader: boolean,
49
51
  logger: ITelemetryLogger,
50
52
  snapshotDownloader: (url: string, fetchOptions: {[index: string]: any}) => Promise<IOdspResponse<unknown>>,
51
53
  ): Promise<ISnapshotContents> {
@@ -61,7 +63,8 @@ export async function fetchSnapshot(
61
63
  }
62
64
 
63
65
  const queryString = getQueryString(queryParams);
64
- const { url, headers } = getUrlAndHeadersWithAuth(`${snapshotUrl}${path}${queryString}`, token);
66
+ const { url, headers } = getUrlAndHeadersWithAuth(
67
+ `${snapshotUrl}${path}${queryString}`, token, forceAccessTokenViaAuthorizationHeader);
65
68
  const response = await PerformanceEvent.timedExecAsync(
66
69
  logger,
67
70
  {
@@ -77,6 +80,7 @@ export async function fetchSnapshotWithRedeem(
77
80
  odspResolvedUrl: IOdspResolvedUrl,
78
81
  storageTokenFetcher: InstrumentedStorageTokenFetcher,
79
82
  snapshotOptions: ISnapshotOptions | undefined,
83
+ forceAccessTokenViaAuthorizationHeader: boolean,
80
84
  logger: ITelemetryLogger,
81
85
  snapshotDownloader: (
82
86
  finalOdspResolvedUrl: IOdspResolvedUrl,
@@ -88,6 +92,12 @@ export async function fetchSnapshotWithRedeem(
88
92
  removeEntries: () => Promise<void>,
89
93
  enableRedeemFallback?: boolean,
90
94
  ): Promise<ISnapshotContents> {
95
+ // back-compat: This block to be removed with #8784 when we only consume/consider odsp resolvers that are >= 0.51
96
+ const sharingLinkToRedeem = (odspResolvedUrl as any).sharingLinkToRedeem;
97
+ if(sharingLinkToRedeem) {
98
+ odspResolvedUrl.shareLinkInfo = {...odspResolvedUrl.shareLinkInfo, sharingLinkToRedeem}
99
+ }
100
+
91
101
  return fetchLatestSnapshotCore(
92
102
  odspResolvedUrl,
93
103
  storageTokenFetcher,
@@ -95,6 +105,7 @@ export async function fetchSnapshotWithRedeem(
95
105
  logger,
96
106
  snapshotDownloader,
97
107
  putInCache,
108
+ enableRedeemFallback,
98
109
  ).catch(async (error) => {
99
110
  if (enableRedeemFallback && isRedeemSharingLinkError(odspResolvedUrl, error)) {
100
111
  // Execute the redeem fallback
@@ -102,16 +113,15 @@ export async function fetchSnapshotWithRedeem(
102
113
  eventName: "RedeemFallback",
103
114
  errorType: error.errorType,
104
115
  }, error);
105
- await redeemSharingLink(odspResolvedUrl, storageTokenFetcher, logger);
116
+ await redeemSharingLink(
117
+ odspResolvedUrl, storageTokenFetcher, logger, forceAccessTokenViaAuthorizationHeader);
106
118
  const odspResolvedUrlWithoutShareLink: IOdspResolvedUrl =
107
- { ...odspResolvedUrl, sharingLinkToRedeem: undefined };
108
-
109
- if(odspResolvedUrlWithoutShareLink.shareLinkInfo) {
110
- odspResolvedUrlWithoutShareLink.shareLinkInfo = {
111
- ...odspResolvedUrlWithoutShareLink.shareLinkInfo,
112
- sharingLinkToRedeem: undefined,
119
+ { ...odspResolvedUrl,
120
+ shareLinkInfo: {
121
+ ...odspResolvedUrl.shareLinkInfo,
122
+ sharingLinkToRedeem: undefined
123
+ }
113
124
  };
114
- }
115
125
 
116
126
  return fetchLatestSnapshotCore(
117
127
  odspResolvedUrlWithoutShareLink,
@@ -140,6 +150,7 @@ async function redeemSharingLink(
140
150
  odspResolvedUrl: IOdspResolvedUrl,
141
151
  storageTokenFetcher: InstrumentedStorageTokenFetcher,
142
152
  logger: ITelemetryLogger,
153
+ forceAccessTokenViaAuthorizationHeader: boolean,
143
154
  ) {
144
155
  return PerformanceEvent.timedExecAsync(
145
156
  logger,
@@ -147,12 +158,13 @@ async function redeemSharingLink(
147
158
  eventName: "RedeemShareLink",
148
159
  },
149
160
  async () => getWithRetryForTokenRefresh(async (tokenFetchOptions) => {
150
- assert(!!odspResolvedUrl.sharingLinkToRedeem,
161
+ assert(!!odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem,
151
162
  0x1ed /* "Share link should be present" */);
152
163
  const storageToken = await storageTokenFetcher(tokenFetchOptions, "RedeemShareLink");
153
- const encodedShareUrl = getEncodedShareUrl(odspResolvedUrl.sharingLinkToRedeem);
164
+ const encodedShareUrl = getEncodedShareUrl(odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem);
154
165
  const redeemUrl = `${odspResolvedUrl.siteUrl}/_api/v2.0/shares/${encodedShareUrl}`;
155
- const { url, headers } = getUrlAndHeadersWithAuth(redeemUrl, storageToken);
166
+ const { url, headers } = getUrlAndHeadersWithAuth(
167
+ redeemUrl, storageToken, forceAccessTokenViaAuthorizationHeader);
156
168
  headers.prefer = "redeemSharingLink";
157
169
  return fetchAndParseAsJSONHelper(url, { headers });
158
170
  }),
@@ -171,6 +183,7 @@ async function fetchLatestSnapshotCore(
171
183
  controller?: AbortController,
172
184
  ) => Promise<ISnapshotRequestAndResponseOptions>,
173
185
  putInCache: (valueWithEpoch: IVersionedValueWithEpoch) => Promise<void>,
186
+ enableRedeemFallback?: boolean,
174
187
  ): Promise<ISnapshotContents> {
175
188
  return getWithRetryForTokenRefresh(async (tokenFetchOptions) => {
176
189
  const storageToken = await storageTokenFetcher(tokenFetchOptions, "TreesLatest", true);
@@ -187,6 +200,8 @@ async function fetchLatestSnapshotCore(
187
200
  const perfEvent = {
188
201
  eventName: "TreesLatest",
189
202
  attempts: tokenFetchOptions.refresh ? 2 : 1,
203
+ shareLinkPresent: odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem !== undefined,
204
+ redeemFallbackEnabled: enableRedeemFallback,
190
205
  };
191
206
  if (snapshotOptions !== undefined) {
192
207
  Object.entries(snapshotOptions).forEach(([key, value]) => {
@@ -344,30 +359,9 @@ async function fetchSnapshotContentsCoreV1(
344
359
  ): Promise<ISnapshotRequestAndResponseOptions> {
345
360
  const snapshotUrl = odspResolvedUrl.endpoints.snapshotStorageUrl;
346
361
  const url = `${snapshotUrl}/trees/latest?ump=1`;
347
- const formBoundary = uuid();
348
- const formParams: string[] = [];
349
- formParams.push(`--${formBoundary}`);
350
- formParams.push(`Authorization: Bearer ${storageToken}`);
351
- formParams.push(`X-HTTP-Method-Override: GET`);
352
- if (snapshotOptions !== undefined) {
353
- Object.entries(snapshotOptions).forEach(([key, value]) => {
354
- if (value !== undefined) {
355
- formParams.push(`${key}: ${value}`);
356
- }
357
- });
358
- }
359
- if (odspResolvedUrl.sharingLinkToRedeem) {
360
- formParams.push(`sl: ${odspResolvedUrl.sharingLinkToRedeem}`);
361
- }
362
- formParams.push(`_post: 1`);
363
- formParams.push(`\r\n--${formBoundary}--`);
364
- const postBody = formParams.join("\r\n");
365
- const headers: {[index: string]: any} = {
366
- "Content-Type": `multipart/form-data;boundary=${formBoundary}`,
367
- };
368
-
362
+ const { body, headers } = getFormBodyAndHeaders(odspResolvedUrl, storageToken, snapshotOptions);
369
363
  const fetchOptions = {
370
- body: postBody,
364
+ body,
371
365
  headers,
372
366
  signal: controller?.signal,
373
367
  method: "POST",
@@ -401,30 +395,57 @@ async function fetchSnapshotContentsCoreV2(
401
395
  epochTracker?: EpochTracker,
402
396
  ): Promise<ISnapshotRequestAndResponseOptions> {
403
397
  const fullUrl = `${odspResolvedUrl.siteUrl}/_api/v2.1/drives/${odspResolvedUrl.driveId}/items/${
404
- odspResolvedUrl.itemId}/opStream/attachments/latest/content`;
405
- const queryParams = { ...snapshotOptions };
406
- if (odspResolvedUrl.sharingLinkToRedeem) {
407
- // eslint-disable-next-line @typescript-eslint/dot-notation
408
- queryParams["sl"] = odspResolvedUrl.sharingLinkToRedeem;
409
- }
410
- const queryString = getQueryString(queryParams);
411
- const { url, headers } = getUrlAndHeadersWithAuth(`${fullUrl}${queryString}`, storageToken);
398
+ odspResolvedUrl.itemId}/opStream/attachments/latest/content?ump=1`;
399
+
400
+ const { body, headers } = getFormBodyAndHeaders(odspResolvedUrl, storageToken, snapshotOptions);
412
401
  const fetchOptions = {
402
+ body,
413
403
  headers,
414
404
  signal: controller?.signal,
405
+ method: "POST",
415
406
  };
416
- const response = await (epochTracker?.fetchArray(url, fetchOptions, "treesLatest") ??
417
- fetchArray(url, fetchOptions));
407
+
408
+ const response = await (epochTracker?.fetchArray(fullUrl, fetchOptions, "treesLatest", true) ??
409
+ fetchArray(fullUrl, fetchOptions));
418
410
  const snapshotContents: ISnapshotContents = parseCompactSnapshotResponse(
419
411
  new ReadBuffer(new Uint8Array(response.content)));
420
412
  const finalSnapshotContents: IOdspResponse<ISnapshotContents> = { ...response, content: snapshotContents };
421
413
  return {
422
414
  odspSnapshotResponse: finalSnapshotContents,
423
415
  requestHeaders: headers,
424
- requestUrl: url,
416
+ requestUrl: fullUrl,
425
417
  };
426
418
  }
427
419
 
420
+ function getFormBodyAndHeaders(
421
+ odspResolvedUrl: IOdspResolvedUrl,
422
+ storageToken: string,
423
+ snapshotOptions: ISnapshotOptions | undefined,
424
+ ) {
425
+ const formBoundary = uuid();
426
+ const formParams: string[] = [];
427
+ formParams.push(`--${formBoundary}`);
428
+ formParams.push(`Authorization: Bearer ${storageToken}`);
429
+ formParams.push(`X-HTTP-Method-Override: GET`);
430
+ if (snapshotOptions !== undefined) {
431
+ Object.entries(snapshotOptions).forEach(([key, value]) => {
432
+ if (value !== undefined) {
433
+ formParams.push(`${key}: ${value}`);
434
+ }
435
+ });
436
+ }
437
+ if (odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem) {
438
+ formParams.push(`sl: ${odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem}`);
439
+ }
440
+ formParams.push(`_post: 1`);
441
+ formParams.push(`\r\n--${formBoundary}--`);
442
+ const postBody = formParams.join("\r\n");
443
+ const headers: {[index: string]: any} = {
444
+ "Content-Type": `multipart/form-data;boundary=${formBoundary}`,
445
+ };
446
+ return { body: postBody, headers };
447
+ }
448
+
428
449
  function validateAndEvalBlobsAndTrees(snapshot: ISnapshotContents) {
429
450
  assert(snapshot.snapshotTree !== undefined,
430
451
  0x200 /* "Returned odsp snapshot is malformed. No trees!" */);
@@ -457,6 +478,12 @@ export async function downloadSnapshot(
457
478
  controller?: AbortController,
458
479
  epochTracker?: EpochTracker,
459
480
  ): Promise<ISnapshotRequestAndResponseOptions> {
481
+ // back-compat: This block to be removed with #8784 when we only consume/consider odsp resolvers that are >= 0.51
482
+ const sharingLinkToRedeem = (odspResolvedUrl as any).sharingLinkToRedeem;
483
+ if(sharingLinkToRedeem) {
484
+ odspResolvedUrl.shareLinkInfo = {...odspResolvedUrl.shareLinkInfo, sharingLinkToRedeem}
485
+ }
486
+
460
487
  if (fetchBinarySnapshotFormat) {
461
488
  // Logging an event here as it is not supposed to be used in production yet and only in experimental mode.
462
489
  logger.sendTelemetryEvent({ eventName: "BinarySnapshotFetched" });
@@ -467,7 +494,7 @@ export async function downloadSnapshot(
467
494
  }
468
495
 
469
496
  function isRedeemSharingLinkError(odspResolvedUrl: IOdspResolvedUrl, error: any) {
470
- if (odspResolvedUrl.sharingLinkToRedeem !== undefined
497
+ if (odspResolvedUrl.shareLinkInfo?.sharingLinkToRedeem !== undefined
471
498
  && (typeof error === "object" && error !== null)
472
499
  && (error.errorType === DriverErrorType.authorizationError
473
500
  || error.errorType === DriverErrorType.fileNotFoundOrAccessDeniedError)) {
@@ -84,7 +84,7 @@ async function getFileLinkCore(
84
84
  identityType: IdentityType,
85
85
  logger: ITelemetryLogger,
86
86
  ): Promise<string> {
87
- const fileItem = await getFileItemLite(getToken, odspUrlParts, logger);
87
+ const fileItem = await getFileItemLite(getToken, odspUrlParts, logger, identityType === "Consumer");
88
88
 
89
89
  // ODC canonical link does not require any additional processing
90
90
  if (identityType === "Consumer") {
@@ -104,7 +104,10 @@ async function getFileLinkCore(
104
104
  const { url, headers } = getUrlAndHeadersWithAuth(
105
105
  `${odspUrlParts.siteUrl}/_api/web/GetFileByUrl(@a1)/ListItemAllFields/GetSharingInformation?@a1=${
106
106
  encodeURIComponent(`'${fileItem.webDavUrl}'`)
107
- }`, tokenFromResponse(token));
107
+ }`,
108
+ tokenFromResponse(token),
109
+ false,
110
+ );
108
111
  const requestInit = {
109
112
  method: "POST",
110
113
  headers: {
@@ -152,6 +155,7 @@ async function getFileItemLite(
152
155
  getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
153
156
  odspUrlParts: IOdspUrlParts,
154
157
  logger: ITelemetryLogger,
158
+ forceAccessTokenViaAuthorizationHeader: boolean,
155
159
  ): Promise<FileItemLite> {
156
160
  return PerformanceEvent.timedExecAsync(
157
161
  logger,
@@ -166,6 +170,7 @@ async function getFileItemLite(
166
170
  const { url, headers } = getUrlAndHeadersWithAuth(
167
171
  `${siteUrl}/_api/v2.0/drives/${driveId}/items/${itemId}?select=webUrl,webDavUrl`,
168
172
  tokenFromResponse(token),
173
+ forceAccessTokenViaAuthorizationHeader,
169
174
  );
170
175
  const requestInit = { method: "GET", headers };
171
176
  const response = await fetchHelper(url, requestInit);
@@ -3,48 +3,34 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- /**
7
- * Gets the length of the query string portion of a url.
8
- * @param url The full url
9
- */
10
- function getQueryStringLength(url: string): number {
11
- const queryParamStart = url.indexOf("?");
12
-
13
- if (queryParamStart === -1) {
14
- return 0;
15
- }
16
-
17
- return url.length - queryParamStart - 1;
18
- }
19
-
20
- // eslint-disable-next-line max-len
21
- export function getUrlAndHeadersWithAuth(url: string, token: string | null): { url: string, headers: { [index: string]: string } } {
6
+ export function getUrlAndHeadersWithAuth(
7
+ url: string,
8
+ token: string | null,
9
+ forceAccessTokenViaAuthorizationHeader: boolean,
10
+ ): { url: string, headers: { [index: string]: string } } {
22
11
  if (!token || token.length === 0) {
23
12
  return { url, headers: {} };
24
13
  }
25
14
 
26
- const queryParamStart = url.indexOf("?");
27
-
28
- // Determine if we need to add ?, &, or nothing (if the url ends with ?)
29
- let tokenQueryParam = queryParamStart === -1 ? "?" : (queryParamStart !== url.length - 1 ? `&` : "");
30
-
31
- const tokenIsQueryParam = token.startsWith("?");
32
- if (tokenIsQueryParam) {
33
- // The token itself is a query param
34
- tokenQueryParam += token.substring(1);
35
- } else {
36
- tokenQueryParam += `access_token=${encodeURIComponent(token)}`;
37
- }
38
-
39
- // ODSP APIs have a limitation that the query string cannot exceed 2048 characters.
40
- // We try to stick the access token in the URL to make it a simple XHR request and avoid an options call.
41
- // If the query string exceeds 2048, we have to fall back to sending the access token as a header, which
42
- // has a negative performance implication as it adds a performance overhead.
43
- if (tokenIsQueryParam || getQueryStringLength(url + tokenQueryParam) <= 2048) {
44
- return {
45
- headers: {},
46
- url: url + tokenQueryParam,
47
- };
15
+ if (!forceAccessTokenViaAuthorizationHeader) {
16
+ // Pass access token via query string: this will make request be treated as 'simple' request
17
+ // which does not require OPTIONS call as part of CORS check.
18
+ const urlWithAccessTokenInQueryString = new URL(url);
19
+ // IMPORTANT: Do not apply encodeURIComponent to token, param value is automatically encoded
20
+ // when set via URLSearchParams class
21
+ urlWithAccessTokenInQueryString.searchParams.set("access_token", token);
22
+ // ODSP APIs have a limitation that the query string cannot exceed 2048 characters.
23
+ // If the query string exceeds 2048, we have to fall back to sending the access token as a header, which
24
+ // has a negative performance implication as it adds a performance overhead.
25
+ // NOTE: URL.search.length value includes '?' symbol and it is unclear whether backend logic which enforces
26
+ // query length limit accounts for it. This logic errs on side of caution and includes that key in overall
27
+ // query length.
28
+ if (urlWithAccessTokenInQueryString.search.length <= 2048) {
29
+ return {
30
+ headers: {},
31
+ url: urlWithAccessTokenInQueryString.href,
32
+ };
33
+ }
48
34
  }
49
35
 
50
36
  return {
@@ -53,9 +53,6 @@ export class OdspDeltaStorageService {
53
53
  let postBody = `--${formBoundary}\r\n`;
54
54
  postBody += `Authorization: Bearer ${storageToken}\r\n`;
55
55
  postBody += `X-HTTP-Method-Override: GET\r\n`;
56
- if (fetchReason !== undefined) {
57
- postBody += `fetchReason: ${fetchReason}\r\n`;
58
- }
59
56
 
60
57
  postBody += `_post: 1\r\n`;
61
58
  postBody += `\r\n--${formBoundary}--`;
@@ -81,6 +78,7 @@ export class OdspDeltaStorageService {
81
78
  },
82
79
  "ops",
83
80
  true,
81
+ fetchReason,
84
82
  );
85
83
  clearTimeout(timer);
86
84
  const deltaStorageResponse = response.content;
@@ -234,7 +234,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
234
234
 
235
235
  // Reference to this client supporting get_ops flow.
236
236
  connectMessage.supportedFeatures = { };
237
- if (mc.config.getBoolean("Fluid.Driver.Odsp.GetOpsEnabled") === true) {
237
+ if (mc.config.getBoolean("Fluid.Driver.Odsp.GetOpsEnabled") !== false) {
238
238
  connectMessage.supportedFeatures[feature_get_ops] = true;
239
239
  }
240
240
 
@@ -17,6 +17,7 @@ import {
17
17
  IResolvedUrl,
18
18
  IDocumentStorageService,
19
19
  IDocumentServicePolicies,
20
+ DriverErrorType,
20
21
  } from "@fluidframework/driver-definitions";
21
22
  import { DeltaStreamConnectionForbiddenError, NonRetryableError } from "@fluidframework/driver-utils";
22
23
  import { IFacetCodes } from "@fluidframework/odsp-doclib-utils";
@@ -281,6 +282,14 @@ export class OdspDocumentService implements IDocumentService {
281
282
  connection.on("op", (documentId, ops: ISequencedDocumentMessage[]) => {
282
283
  this.opsReceived(ops);
283
284
  });
285
+ // On disconnect with 401/403 error code, we can just clear the joinSession cache as we will again
286
+ // get the auth error on reconnecting and face latency.
287
+ connection.on("disconnect", (error: any) => {
288
+ if (typeof error === "object" && error !== null
289
+ && error.errorType === DriverErrorType.authorizationError) {
290
+ this.cache.sessionJoinCache.remove(this.joinSessionKey);
291
+ }
292
+ });
284
293
  this.currentConnection = connection;
285
294
  return connection;
286
295
  } catch (error) {
@@ -104,6 +104,7 @@ export class OdspDocumentServiceFactoryCore implements IDocumentServiceFactory {
104
104
  cacheAndTracker.epochTracker,
105
105
  fileEntry,
106
106
  this.hostPolicy.cacheCreateNewSummary ?? true,
107
+ !!this.hostPolicy.sessionOptions?.forceAccessTokenViaAuthorizationHeader,
107
108
  );
108
109
  const docService = this.createDocumentServiceCore(odspResolvedUrl, odspLogger, cacheAndTracker);
109
110
  event.end({