@fluidframework/odsp-driver 3.0.2 → 3.1.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 (132) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +30 -34
  3. package/api-report/odsp-driver.legacy.alpha.api.md +5 -1
  4. package/api-report/odsp-driver.legacy.beta.api.md +5 -1
  5. package/api-report/odsp-driver.point-in-time.legacy.beta.api.md +2 -1
  6. package/dist/epochTracker.d.ts +4 -3
  7. package/dist/epochTracker.d.ts.map +1 -1
  8. package/dist/epochTracker.js +9 -5
  9. package/dist/epochTracker.js.map +1 -1
  10. package/dist/fetchSnapshot.d.ts +2 -2
  11. package/dist/fetchSnapshot.d.ts.map +1 -1
  12. package/dist/fetchSnapshot.js +12 -7
  13. package/dist/fetchSnapshot.js.map +1 -1
  14. package/dist/getFileLink.d.ts +1 -1
  15. package/dist/getFileLink.d.ts.map +1 -1
  16. package/dist/getFileLink.js +16 -11
  17. package/dist/getFileLink.js.map +1 -1
  18. package/dist/odspDelayLoadedDeltaStream.d.ts.map +1 -1
  19. package/dist/odspDelayLoadedDeltaStream.js +1 -1
  20. package/dist/odspDelayLoadedDeltaStream.js.map +1 -1
  21. package/dist/odspDocumentDeltaConnection.d.ts +1 -1
  22. package/dist/odspDocumentDeltaConnection.d.ts.map +1 -1
  23. package/dist/odspDocumentDeltaConnection.js +6 -3
  24. package/dist/odspDocumentDeltaConnection.js.map +1 -1
  25. package/dist/odspDocumentServiceFactory.d.ts.map +1 -1
  26. package/dist/odspDocumentServiceFactory.js +1 -0
  27. package/dist/odspDocumentServiceFactory.js.map +1 -1
  28. package/dist/odspDocumentServiceFactoryCore.d.ts +4 -1
  29. package/dist/odspDocumentServiceFactoryCore.d.ts.map +1 -1
  30. package/dist/odspDocumentServiceFactoryCore.js +11 -5
  31. package/dist/odspDocumentServiceFactoryCore.js.map +1 -1
  32. package/dist/odspDocumentStorageManager.d.ts.map +1 -1
  33. package/dist/odspDocumentStorageManager.js +11 -11
  34. package/dist/odspDocumentStorageManager.js.map +1 -1
  35. package/dist/odspDriverUrlResolverForShareLink.d.ts +5 -0
  36. package/dist/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  37. package/dist/odspDriverUrlResolverForShareLink.js +3 -1
  38. package/dist/odspDriverUrlResolverForShareLink.js.map +1 -1
  39. package/dist/odspVersionManager/odspFileVersionFetcher.d.ts +1 -0
  40. package/dist/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -1
  41. package/dist/odspVersionManager/odspFileVersionFetcher.js +6 -2
  42. package/dist/odspVersionManager/odspFileVersionFetcher.js.map +1 -1
  43. package/dist/packageVersion.d.ts +1 -1
  44. package/dist/packageVersion.js +1 -1
  45. package/dist/packageVersion.js.map +1 -1
  46. package/dist/pointInTimeDriver/createPointInTimeDocumentService.d.ts +1 -1
  47. package/dist/pointInTimeDriver/createPointInTimeDocumentService.d.ts.map +1 -1
  48. package/dist/pointInTimeDriver/createPointInTimeDocumentService.js +5 -4
  49. package/dist/pointInTimeDriver/createPointInTimeDocumentService.js.map +1 -1
  50. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +1 -1
  51. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -1
  52. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js +21 -1
  53. package/dist/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -1
  54. package/dist/prefetchLatestSnapshot.d.ts.map +1 -1
  55. package/dist/prefetchLatestSnapshot.js +2 -2
  56. package/dist/prefetchLatestSnapshot.js.map +1 -1
  57. package/dist/requestHeaders.d.ts +19 -0
  58. package/dist/requestHeaders.d.ts.map +1 -0
  59. package/dist/requestHeaders.js +48 -0
  60. package/dist/requestHeaders.js.map +1 -0
  61. package/lib/epochTracker.d.ts +4 -3
  62. package/lib/epochTracker.d.ts.map +1 -1
  63. package/lib/epochTracker.js +9 -5
  64. package/lib/epochTracker.js.map +1 -1
  65. package/lib/fetchSnapshot.d.ts +2 -2
  66. package/lib/fetchSnapshot.d.ts.map +1 -1
  67. package/lib/fetchSnapshot.js +12 -7
  68. package/lib/fetchSnapshot.js.map +1 -1
  69. package/lib/getFileLink.d.ts +1 -1
  70. package/lib/getFileLink.d.ts.map +1 -1
  71. package/lib/getFileLink.js +16 -11
  72. package/lib/getFileLink.js.map +1 -1
  73. package/lib/odspDelayLoadedDeltaStream.d.ts.map +1 -1
  74. package/lib/odspDelayLoadedDeltaStream.js +1 -1
  75. package/lib/odspDelayLoadedDeltaStream.js.map +1 -1
  76. package/lib/odspDocumentDeltaConnection.d.ts +1 -1
  77. package/lib/odspDocumentDeltaConnection.d.ts.map +1 -1
  78. package/lib/odspDocumentDeltaConnection.js +6 -3
  79. package/lib/odspDocumentDeltaConnection.js.map +1 -1
  80. package/lib/odspDocumentServiceFactory.d.ts.map +1 -1
  81. package/lib/odspDocumentServiceFactory.js +1 -0
  82. package/lib/odspDocumentServiceFactory.js.map +1 -1
  83. package/lib/odspDocumentServiceFactoryCore.d.ts +4 -1
  84. package/lib/odspDocumentServiceFactoryCore.d.ts.map +1 -1
  85. package/lib/odspDocumentServiceFactoryCore.js +11 -5
  86. package/lib/odspDocumentServiceFactoryCore.js.map +1 -1
  87. package/lib/odspDocumentStorageManager.d.ts.map +1 -1
  88. package/lib/odspDocumentStorageManager.js +11 -11
  89. package/lib/odspDocumentStorageManager.js.map +1 -1
  90. package/lib/odspDriverUrlResolverForShareLink.d.ts +5 -0
  91. package/lib/odspDriverUrlResolverForShareLink.d.ts.map +1 -1
  92. package/lib/odspDriverUrlResolverForShareLink.js +3 -1
  93. package/lib/odspDriverUrlResolverForShareLink.js.map +1 -1
  94. package/lib/odspVersionManager/odspFileVersionFetcher.d.ts +1 -0
  95. package/lib/odspVersionManager/odspFileVersionFetcher.d.ts.map +1 -1
  96. package/lib/odspVersionManager/odspFileVersionFetcher.js +6 -2
  97. package/lib/odspVersionManager/odspFileVersionFetcher.js.map +1 -1
  98. package/lib/packageVersion.d.ts +1 -1
  99. package/lib/packageVersion.js +1 -1
  100. package/lib/packageVersion.js.map +1 -1
  101. package/lib/pointInTimeDriver/createPointInTimeDocumentService.d.ts +1 -1
  102. package/lib/pointInTimeDriver/createPointInTimeDocumentService.d.ts.map +1 -1
  103. package/lib/pointInTimeDriver/createPointInTimeDocumentService.js +5 -4
  104. package/lib/pointInTimeDriver/createPointInTimeDocumentService.js.map +1 -1
  105. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts +1 -1
  106. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.d.ts.map +1 -1
  107. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js +21 -1
  108. package/lib/pointInTimeDriver/odspPointInTimeDocumentService.js.map +1 -1
  109. package/lib/prefetchLatestSnapshot.d.ts.map +1 -1
  110. package/lib/prefetchLatestSnapshot.js +2 -2
  111. package/lib/prefetchLatestSnapshot.js.map +1 -1
  112. package/lib/requestHeaders.d.ts +19 -0
  113. package/lib/requestHeaders.d.ts.map +1 -0
  114. package/lib/requestHeaders.js +44 -0
  115. package/lib/requestHeaders.js.map +1 -0
  116. package/package.json +11 -11
  117. package/src/epochTracker.ts +9 -1
  118. package/src/fetchSnapshot.ts +17 -4
  119. package/src/getFileLink.ts +21 -7
  120. package/src/odspDelayLoadedDeltaStream.ts +1 -0
  121. package/src/odspDocumentDeltaConnection.ts +6 -0
  122. package/src/odspDocumentServiceFactory.ts +1 -0
  123. package/src/odspDocumentServiceFactoryCore.ts +14 -2
  124. package/src/odspDocumentStorageManager.ts +13 -3
  125. package/src/odspDriverUrlResolverForShareLink.ts +8 -0
  126. package/src/odspVersionManager/DEV.md +5 -1
  127. package/src/odspVersionManager/odspFileVersionFetcher.ts +7 -2
  128. package/src/packageVersion.ts +1 -1
  129. package/src/pointInTimeDriver/createPointInTimeDocumentService.ts +5 -1
  130. package/src/pointInTimeDriver/odspPointInTimeDocumentService.ts +40 -10
  131. package/src/prefetchLatestSnapshot.ts +4 -0
  132. package/src/requestHeaders.ts +56 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestHeaders.js","sourceRoot":"","sources":["../src/requestHeaders.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC;IACtC,eAAe;IACf,cAAc;IACd,eAAe;IACf,wBAAwB;IACxB,gBAAgB;CAChB,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CACjC,cAA4D;IAE5D,IAAI,cAAc,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9E,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,MAAM,aAAa,GAAG,MAAM,CAAC,WAAW,CACvC,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,MAAM,CACpC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,sBAAsB,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAC3D,CACD,CAAC;IACF,OAAO,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;AAC3F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAClC,WAAyD,EACzD,cAAuC;IAEvC,MAAM,eAAe,GAAG,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACxD,IAAI,eAAe,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,cAAc,CAAC;IACvB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;QAClC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,OAAO,CAAC,cAAc,CAAC,EAAE,CAAC;YACxD,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACxB,CAAC;IACF,CAAC;IACD,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nconst driverOwnedHeaderNames = new Set([\n\t\"authorization\",\n\t\"content-type\",\n\t\"x-fluid-epoch\",\n\t\"x-http-method-override\",\n\t\"x-requeststats\",\n]);\n\n/**\n * Creates an immutable snapshot of host-provided per-session headers after removing names owned by\n * the ODSP driver. The factory, URL resolver, epoch tracker, and Socket.IO connection use the\n * returned value so later host mutations cannot change an active document session or override\n * authentication and protocol headers.\n */\nexport function copyRequestHeaders(\n\trequestHeaders: Readonly<Record<string, string>> | undefined,\n): Readonly<Record<string, string>> | undefined {\n\tif (requestHeaders === undefined || Object.keys(requestHeaders).length === 0) {\n\t\treturn undefined;\n\t}\n\tconst copiedHeaders = Object.fromEntries(\n\t\tObject.entries(requestHeaders).filter(\n\t\t\t([name]) => !driverOwnedHeaderNames.has(name.toLowerCase()),\n\t\t),\n\t);\n\treturn Object.keys(copiedHeaders).length === 0 ? undefined : Object.freeze(copiedHeaders);\n}\n\n/**\n * Adds sanitized host headers to one ODSP request. Request-specific headers are applied last, so\n * driver-generated values win case-insensitive collisions. ODSP fetch paths use this helper for\n * snapshot, delta, blob, share-link, file-link, and version requests; Socket.IO applies the copied\n * headers separately as Node.js `extraHeaders`.\n */\nexport function mergeRequestHeaders(\n\thostHeaders: Readonly<Record<string, string>> | undefined,\n\trequestHeaders: HeadersInit | undefined,\n): HeadersInit | undefined {\n\tconst safeHostHeaders = copyRequestHeaders(hostHeaders);\n\tif (safeHostHeaders === undefined) {\n\t\treturn requestHeaders;\n\t}\n\n\tconst merged = new Headers(safeHostHeaders);\n\tif (requestHeaders !== undefined) {\n\t\tfor (const [key, value] of new Headers(requestHeaders)) {\n\t\t\tmerged.set(key, value);\n\t\t}\n\t}\n\treturn Object.fromEntries(merged);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/odsp-driver",
3
- "version": "3.0.2",
3
+ "version": "3.1.0",
4
4
  "description": "Socket storage implementation for SPO and ODC",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -66,22 +66,22 @@
66
66
  "temp-directory": "nyc/.nyc_output"
67
67
  },
68
68
  "dependencies": {
69
- "@fluid-internal/client-utils": "~3.0.2",
70
- "@fluidframework/core-interfaces": "~3.0.2",
71
- "@fluidframework/core-utils": "~3.0.2",
72
- "@fluidframework/driver-base": "~3.0.2",
73
- "@fluidframework/driver-definitions": "~3.0.2",
74
- "@fluidframework/driver-utils": "~3.0.2",
75
- "@fluidframework/odsp-doclib-utils": "~3.0.2",
76
- "@fluidframework/odsp-driver-definitions": "~3.0.2",
77
- "@fluidframework/telemetry-utils": "~3.0.2",
69
+ "@fluid-internal/client-utils": "~3.1.0",
70
+ "@fluidframework/core-interfaces": "~3.1.0",
71
+ "@fluidframework/core-utils": "~3.1.0",
72
+ "@fluidframework/driver-base": "~3.1.0",
73
+ "@fluidframework/driver-definitions": "~3.1.0",
74
+ "@fluidframework/driver-utils": "~3.1.0",
75
+ "@fluidframework/odsp-doclib-utils": "~3.1.0",
76
+ "@fluidframework/odsp-driver-definitions": "~3.1.0",
77
+ "@fluidframework/telemetry-utils": "~3.1.0",
78
78
  "socket.io-client": "^4.8.3",
79
79
  "uuid": "^11.1.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@arethetypeswrong/cli": "^0.18.5",
83
83
  "@biomejs/biome": "~2.4.5",
84
- "@fluid-internal/mocha-test-setup": "~3.0.2",
84
+ "@fluid-internal/mocha-test-setup": "~3.1.0",
85
85
  "@fluid-tools/build-cli": "^0.67.0",
86
86
  "@fluidframework/build-common": "^2.0.3",
87
87
  "@fluidframework/build-tools": "^0.67.0",
@@ -51,6 +51,7 @@ import {
51
51
  getOdspResolvedUrl,
52
52
  } from "./odspUtils.js";
53
53
  import { pkgVersion as driverVersion } from "./packageVersion.js";
54
+ import { copyRequestHeaders, mergeRequestHeaders } from "./requestHeaders.js";
54
55
 
55
56
  /**
56
57
  * @legacy
@@ -100,6 +101,7 @@ export const Odsp409Error = "Odsp409Error";
100
101
  */
101
102
  export class EpochTracker implements IPersistedFileCache {
102
103
  private _fluidEpoch: string | undefined;
104
+ private readonly requestHeaders: Readonly<Record<string, string>> | undefined;
103
105
 
104
106
  private readonly snapshotCacheExpiryTimeoutMs: number;
105
107
  public readonly rateLimiter: RateLimiter;
@@ -112,8 +114,10 @@ export class EpochTracker implements IPersistedFileCache {
112
114
  protected readonly fileEntry: IFileEntry,
113
115
  protected readonly logger: ITelemetryLoggerExt,
114
116
  protected readonly clientIsSummarizer?: boolean,
117
+ requestHeaders?: Readonly<Record<string, string>>,
115
118
  ) {
116
119
  this.loggerInternal = extractTelemetryLoggerExt(logger);
120
+ this.requestHeaders = copyRequestHeaders(requestHeaders);
117
121
 
118
122
  // Limits the max number of concurrent requests to 24.
119
123
  this.rateLimiter = new RateLimiter(24);
@@ -298,6 +302,7 @@ export class EpochTracker implements IPersistedFileCache {
298
302
  const clientCorrelationId = this.formatClientCorrelationId(fetchReason);
299
303
  // Add epoch in fetch request.
300
304
  this.addEpochInRequest(fetchOptions, addInBody, clientCorrelationId);
305
+ fetchOptions.headers = mergeRequestHeaders(this.requestHeaders, fetchOptions.headers);
301
306
  let epochFromResponse: string | undefined;
302
307
  return this.rateLimiter
303
308
  .schedule(async () => fetcher(url, fetchOptions))
@@ -523,8 +528,9 @@ export class EpochTrackerWithRedemption extends EpochTracker {
523
528
  protected readonly fileEntry: IFileEntry,
524
529
  logger: TelemetryLoggerExt,
525
530
  protected readonly clientIsSummarizer?: boolean,
531
+ requestHeaders?: Readonly<Record<string, string>>,
526
532
  ) {
527
- super(cache, fileEntry, toITelemetryLoggerExt(logger), clientIsSummarizer);
533
+ super(cache, fileEntry, toITelemetryLoggerExt(logger), clientIsSummarizer, requestHeaders);
528
534
  // Handles the rejected promise within treesLatestDeferral.
529
535
  this.treesLatestDeferral.promise.catch(() => {});
530
536
  }
@@ -656,12 +662,14 @@ export function createOdspCacheAndTracker(
656
662
  fileEntry: IFileEntry,
657
663
  logger: TelemetryLoggerExt,
658
664
  clientIsSummarizer?: boolean,
665
+ requestHeaders?: Readonly<Record<string, string>>,
659
666
  ): ICacheAndTracker {
660
667
  const epochTracker = new EpochTrackerWithRedemption(
661
668
  persistedCacheArg,
662
669
  fileEntry,
663
670
  logger,
664
671
  clientIsSummarizer,
672
+ requestHeaders,
665
673
  );
666
674
  return {
667
675
  cache: {
@@ -68,6 +68,7 @@ import {
68
68
  type TokenFetchOptionsEx,
69
69
  } from "./odspUtils.js";
70
70
  import { pkgVersion } from "./packageVersion.js";
71
+ import { mergeRequestHeaders } from "./requestHeaders.js";
71
72
 
72
73
  /**
73
74
  * Enum to support different types of snapshot formats.
@@ -131,6 +132,7 @@ export async function fetchSnapshotWithRedeem(
131
132
  removeEntries: () => Promise<void>,
132
133
  loadingGroupIds: string[] | undefined,
133
134
  enableRedeemFallback?: boolean,
135
+ requestHeaders?: Readonly<Record<string, string>>,
134
136
  ): Promise<ISnapshot> {
135
137
  // back-compat: This block to be removed with #8784 when we only consume/consider odsp resolvers that are >= 0.51
136
138
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
@@ -154,7 +156,7 @@ export async function fetchSnapshotWithRedeem(
154
156
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
155
157
  if (enableRedeemFallback && isRedeemSharingLinkError(odspResolvedUrl, error)) {
156
158
  // Execute the redeem fallback
157
- await redeemSharingLink(odspResolvedUrl, storageTokenFetcher, logger);
159
+ await redeemSharingLink(odspResolvedUrl, storageTokenFetcher, logger, requestHeaders);
158
160
 
159
161
  const shareLinkInfo = { ...odspResolvedUrl.shareLinkInfo };
160
162
  delete shareLinkInfo.sharingLinkToRedeem;
@@ -205,7 +207,12 @@ export async function fetchSnapshotWithRedeem(
205
207
  ...getOdspResolvedUrl(error.redirectUrl),
206
208
  shareLinkInfo: odspResolvedUrl.shareLinkInfo,
207
209
  };
208
- await redeemSharingLink(redirectedResolvedUrl, storageTokenFetcher, logger);
210
+ await redeemSharingLink(
211
+ redirectedResolvedUrl,
212
+ storageTokenFetcher,
213
+ logger,
214
+ requestHeaders,
215
+ );
209
216
  } catch (redeemError) {
210
217
  logger.sendErrorEvent({ eventName: "RedirectRedeemFallbackError" }, redeemError);
211
218
  }
@@ -236,6 +243,7 @@ async function redeemSharingLink(
236
243
  odspResolvedUrl: IOdspResolvedUrl,
237
244
  getAuthHeader: InstrumentedStorageTokenFetcher,
238
245
  logger: TelemetryLoggerExt,
246
+ requestHeaders?: Readonly<Record<string, string>>,
239
247
  ): Promise<void> {
240
248
  await PerformanceEvent.timedExecAsync(
241
249
  logger,
@@ -272,7 +280,10 @@ async function redeemSharingLink(
272
280
  );
273
281
  const headers = getHeadersWithAuth(authHeader);
274
282
  headers.prefer = isRedemptionNonDurable ? "nonDurableRedeem" : "redeemSharingLink";
275
- await fetchAndParseAsJSONHelper(url, { headers, method });
283
+ await fetchAndParseAsJSONHelper(url, {
284
+ headers: mergeRequestHeaders(requestHeaders, headers),
285
+ method,
286
+ });
276
287
  });
277
288
  }
278
289
 
@@ -735,6 +746,7 @@ export const downloadSnapshot = mockify(
735
746
  controller?: AbortController,
736
747
  epochTracker?: EpochTracker,
737
748
  scenarioName?: string,
749
+ requestHeaders?: Readonly<Record<string, string>>,
738
750
  ): Promise<ISnapshotRequestAndResponseOptions> => {
739
751
  // back-compat: This block to be removed with #8784 when we only consume/consider odsp resolvers that are >= 0.51
740
752
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
@@ -791,7 +803,7 @@ export const downloadSnapshot = mockify(
791
803
  const { body, headers } = getFormBodyAndHeaders(odspResolvedUrl, authHeader, header);
792
804
  const fetchOptions = {
793
805
  body,
794
- headers,
806
+ headers: mergeRequestHeaders(requestHeaders, headers),
795
807
  signal: controller?.signal,
796
808
  method,
797
809
  };
@@ -806,6 +818,7 @@ export const downloadSnapshot = mockify(
806
818
  headers.accept = `application/json, application/ms-fluid; v=${currentReadVersion}`;
807
819
  }
808
820
  }
821
+ fetchOptions.headers = mergeRequestHeaders(requestHeaders, headers);
809
822
 
810
823
  const odspResponse = await (epochTracker?.fetch(
811
824
  url,
@@ -27,6 +27,7 @@ import {
27
27
  toInstrumentedOdspStorageTokenFetcher,
28
28
  } from "./odspUtils.js";
29
29
  import { pkgVersion as driverVersion } from "./packageVersion.js";
30
+ import { copyRequestHeaders, mergeRequestHeaders } from "./requestHeaders.js";
30
31
  import { runWithRetry as runWithRetryForCoherencyAndServiceReadOnlyErrors } from "./retryUtils.js";
31
32
 
32
33
  // Store cached responses for the lifetime of web session as file link remains the same for given file item
@@ -48,7 +49,9 @@ export const getFileLink = mockify(
48
49
  getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
49
50
  resolvedUrl: IOdspResolvedUrl,
50
51
  logger: TelemetryLoggerExt,
52
+ requestHeaders?: Readonly<Record<string, string>>,
51
53
  ): Promise<string> => {
54
+ const immutableRequestHeaders = copyRequestHeaders(requestHeaders);
52
55
  const cacheKey = `${resolvedUrl.siteUrl}_${resolvedUrl.driveId}_${resolvedUrl.itemId}`;
53
56
  const maybeFileLinkCacheEntry = fileLinkCache.get(cacheKey);
54
57
  if (maybeFileLinkCacheEntry !== undefined) {
@@ -63,7 +66,12 @@ export const getFileLink = mockify(
63
66
  async () =>
64
67
  runWithRetryForCoherencyAndServiceReadOnlyErrors(
65
68
  async () =>
66
- getFileLinkWithLocationRedirectionHandling(getToken, resolvedUrl, logger),
69
+ getFileLinkWithLocationRedirectionHandling(
70
+ getToken,
71
+ resolvedUrl,
72
+ logger,
73
+ immutableRequestHeaders,
74
+ ),
67
75
  "getFileLinkCore",
68
76
  logger,
69
77
  ),
@@ -121,6 +129,7 @@ async function getFileLinkWithLocationRedirectionHandling(
121
129
  getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
122
130
  resolvedUrl: IOdspResolvedUrl,
123
131
  logger: TelemetryLoggerExt,
132
+ requestHeaders?: Readonly<Record<string, string>>,
124
133
  ): Promise<string> {
125
134
  // We can have chains of location redirection one after the other, so have a for loop
126
135
  // so that we can keep handling the same type of error. Set max number of redirection to 5.
@@ -128,7 +137,7 @@ async function getFileLinkWithLocationRedirectionHandling(
128
137
  let locationRedirected = false;
129
138
  for (let count = 1; count <= 5; count++) {
130
139
  try {
131
- const fileItem = await getFileItemLite(getToken, resolvedUrl, logger);
140
+ const fileItem = await getFileItemLite(getToken, resolvedUrl, logger, requestHeaders);
132
141
  // Sometimes the siteUrl in the actual file is different from the siteUrl in the resolvedUrl due to location
133
142
  // redirection. This creates issues in the getSharingInformation call. So we need to update the siteUrl in the
134
143
  // resolvedUrl to the siteUrl in the fileItem which is the updated siteUrl.
@@ -142,7 +151,7 @@ async function getFileLinkWithLocationRedirectionHandling(
142
151
  });
143
152
  renameTenantInOdspResolvedUrl(resolvedUrl, newSiteDomain);
144
153
  }
145
- return await getFileLinkCore(getToken, resolvedUrl, logger, fileItem);
154
+ return await getFileLinkCore(getToken, resolvedUrl, logger, fileItem, requestHeaders);
146
155
  } catch (error: unknown) {
147
156
  lastError = error;
148
157
  // If the getSharingLink call fails with the 401/403/404 error, then it could be due to that the file has moved
@@ -167,6 +176,7 @@ async function getFileLinkCore(
167
176
  odspUrlParts: IOdspUrlParts,
168
177
  logger: TelemetryLoggerExt,
169
178
  fileItem: FileItemLite,
179
+ requestHeaders?: Readonly<Record<string, string>>,
170
180
  ): Promise<string> {
171
181
  // ODSP link requires extra call to return link that is resistant to file being renamed or moved to different folder
172
182
  return PerformanceEvent.timedExecAsync(
@@ -202,11 +212,11 @@ async function getFileLinkCore(
202
212
  const headers = getHeadersWithAuth(authHeader);
203
213
  const requestInit = {
204
214
  method,
205
- headers: {
215
+ headers: mergeRequestHeaders(requestHeaders, {
206
216
  "Content-Type": "application/json;odata=verbose",
207
- "Accept": "application/json;odata=verbose",
217
+ Accept: "application/json;odata=verbose",
208
218
  ...headers,
209
- },
219
+ }),
210
220
  };
211
221
  const response = await fetchHelper(url, requestInit);
212
222
  additionalProps = response.propsToLog;
@@ -263,6 +273,7 @@ async function getFileItemLite(
263
273
  getToken: TokenFetcher<OdspResourceTokenFetchOptions>,
264
274
  odspUrlParts: IOdspUrlParts,
265
275
  logger: TelemetryLoggerExt,
276
+ requestHeaders?: Readonly<Record<string, string>>,
266
277
  ): Promise<FileItemLite> {
267
278
  return PerformanceEvent.timedExecAsync(
268
279
  logger,
@@ -290,7 +301,10 @@ async function getFileItemLite(
290
301
  );
291
302
 
292
303
  const headers = getHeadersWithAuth(authHeader);
293
- const requestInit = { method, headers };
304
+ const requestInit = {
305
+ method,
306
+ headers: mergeRequestHeaders(requestHeaders, headers),
307
+ };
294
308
  const response = await fetchHelper(url, requestInit);
295
309
  additionalProps = response.propsToLog;
296
310
 
@@ -559,6 +559,7 @@ export class OdspDelayLoadedDeltaStream {
559
559
  this.epochTracker,
560
560
  this.socketReferenceKeyPrefix,
561
561
  connectionId,
562
+ this.hostPolicy.hostProvidedRequestHeaders,
562
563
  );
563
564
  const duration = performanceNow() - startTime;
564
565
  // This event happens rather often, so it adds up to cost of telemetry.
@@ -32,6 +32,7 @@ import type { EpochTracker } from "./epochTracker.js";
32
32
  import { errorObjectFromSocketError } from "./odspError.js";
33
33
  import { pkgVersion } from "./packageVersion.js";
34
34
  import { SocketIOClientStatic } from "./socketModule.js";
35
+ import { copyRequestHeaders } from "./requestHeaders.js";
35
36
 
36
37
  const protocolVersions = ["^0.4.0", "^0.3.0", "^0.2.0", "^0.1.0"];
37
38
  const feature_get_ops = "api_get_ops";
@@ -259,6 +260,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
259
260
  epochTracker: EpochTracker,
260
261
  socketReferenceKeyPrefix: string | undefined,
261
262
  connectionId: string = uuid(),
263
+ requestHeaders?: Readonly<Record<string, string>>,
262
264
  ): Promise<OdspDocumentDeltaConnection> {
263
265
  const mc = loggerToMonitoringContext(telemetryLogger);
264
266
 
@@ -269,6 +271,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
269
271
 
270
272
  // do not include the specific tenant/doc id in the ref key when multiplexing
271
273
  // this will allow multiple documents to share the same websocket connection
274
+ const immutableRequestHeaders = copyRequestHeaders(requestHeaders);
272
275
  const key = socketReferenceKeyPrefix ? `${socketReferenceKeyPrefix},${url}` : url;
273
276
  const socketReferenceKey = enableMultiplexing ? key : `${key},${tenantId},${documentId}`;
274
277
 
@@ -279,6 +282,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
279
282
  enableMultiplexing,
280
283
  tenantId,
281
284
  documentId,
285
+ immutableRequestHeaders,
282
286
  );
283
287
 
284
288
  const socket = socketReference.socket;
@@ -388,6 +392,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
388
392
  enableMultiplexing: boolean,
389
393
  tenantId: string,
390
394
  documentId: string,
395
+ requestHeaders?: Readonly<Record<string, string>>,
391
396
  ): SocketReference {
392
397
  // eslint-disable-next-line unicorn/no-array-method-this-argument
393
398
  const existingSocketReference = SocketReference.find(key);
@@ -403,6 +408,7 @@ export class OdspDocumentDeltaConnection extends DocumentDeltaConnection {
403
408
  reconnection: false,
404
409
  transports: ["websocket"],
405
410
  timeout: timeoutMs,
411
+ ...(requestHeaders === undefined ? {} : { extraHeaders: requestHeaders }),
406
412
  });
407
413
 
408
414
  return new SocketReference(key, socket);
@@ -90,6 +90,7 @@ export function createOdspDocumentServiceFactory(
90
90
  clientIsSummarizer,
91
91
  persistedCache,
92
92
  getStorageToken: options.getStorageToken,
93
+ requestHeaders: this.requestHeaders,
93
94
  createDocumentService: async (url, odspLogger, cacheAndTracker, isSummarizer) =>
94
95
  this.createDocumentServiceCore(url, odspLogger, cacheAndTracker, isSummarizer),
95
96
  });
@@ -55,6 +55,7 @@ import {
55
55
  toInstrumentedOdspStorageTokenFetcher,
56
56
  toInstrumentedOdspTokenFetcher,
57
57
  } from "./odspUtils.js";
58
+ import { copyRequestHeaders } from "./requestHeaders.js";
58
59
 
59
60
  /**
60
61
  * An ODSP document service factory that supports point-in-time (sequence-number-based) loading.
@@ -104,6 +105,8 @@ export interface IOdspPointInTimeDocumentServiceImplementationProps {
104
105
  readonly persistedCache: IPersistedCache;
105
106
  /** Fetches storage access tokens for ODSP requests. */
106
107
  readonly getStorageToken: TokenFetcher<OdspResourceTokenFetchOptions>;
108
+ /** Host-owned attribution metadata for ODSP requests. */
109
+ readonly requestHeaders?: Readonly<Record<string, string>>;
107
110
  /**
108
111
  * Creates an ODSP document service for a resolved URL.
109
112
  *
@@ -141,6 +144,8 @@ export class OdspDocumentServiceFactoryCore
141
144
  {
142
145
  private readonly nonPersistentCache: INonPersistentCache = new NonPersistentCache();
143
146
  private readonly socketReferenceKeyPrefix?: string;
147
+ private readonly hostPolicy: HostStoragePolicy;
148
+ public readonly requestHeaders?: Readonly<Record<string, string>>;
144
149
 
145
150
  public get snapshotPrefetchResultCache(): PromiseCache<string, IPrefetchSnapshotContents> {
146
151
  return this.nonPersistentCache.snapshotPrefetchResultCache;
@@ -231,6 +236,7 @@ export class OdspDocumentServiceFactoryCore
231
236
  fileEntry,
232
237
  odspLogger,
233
238
  clientIsSummarizer,
239
+ this.requestHeaders,
234
240
  );
235
241
 
236
242
  return PerformanceEvent.timedExecAsync(
@@ -307,9 +313,14 @@ export class OdspDocumentServiceFactoryCore
307
313
  | TokenFetcher<OdspResourceTokenFetchOptions>
308
314
  | undefined,
309
315
  protected persistedCache: IPersistedCache = new LocalPersistentCache(),
310
- private readonly hostPolicy: HostStoragePolicy = {},
316
+ hostPolicy: HostStoragePolicy = {},
311
317
  ) {
312
- if (this.hostPolicy.isolateSocketCache === true) {
318
+ this.requestHeaders = copyRequestHeaders(hostPolicy.hostProvidedRequestHeaders);
319
+ this.hostPolicy =
320
+ this.requestHeaders === undefined
321
+ ? hostPolicy
322
+ : { ...hostPolicy, hostProvidedRequestHeaders: this.requestHeaders };
323
+ if (this.hostPolicy.isolateSocketCache === true || this.requestHeaders !== undefined) {
313
324
  // create the key to separate the socket reuse cache
314
325
  this.socketReferenceKeyPrefix = uuid();
315
326
  }
@@ -396,6 +407,7 @@ export class OdspDocumentServiceFactoryCore
396
407
  },
397
408
  extLogger,
398
409
  clientIsSummarizer,
410
+ this.requestHeaders,
399
411
  );
400
412
 
401
413
  const storageTokenFetcher = toInstrumentedOdspStorageTokenFetcher(
@@ -284,6 +284,7 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
284
284
  hostSnapshotOptions,
285
285
  snapshotFetchOptions.loadingGroupIds,
286
286
  snapshotFetchOptions.scenarioName,
287
+ true /* avoidPrefetchSnapshotCache */,
287
288
  );
288
289
  method = "networkOnly";
289
290
  } else {
@@ -355,6 +356,7 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
355
356
  hostSnapshotOptions,
356
357
  snapshotFetchOptions.loadingGroupIds,
357
358
  snapshotFetchOptions.scenarioName,
359
+ false /* avoidPrefetchSnapshotCache */,
358
360
  );
359
361
 
360
362
  // Ensure that failures on both paths are ignored initially.
@@ -412,6 +414,7 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
412
414
  hostSnapshotOptions,
413
415
  snapshotFetchOptions.loadingGroupIds,
414
416
  snapshotFetchOptions.scenarioName,
417
+ false /* avoidPrefetchSnapshotCache */,
415
418
  );
416
419
  }
417
420
  }
@@ -553,11 +556,13 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
553
556
  hostSnapshotOptions: ISnapshotOptions | undefined,
554
557
  loadingGroupIds: string[] | undefined,
555
558
  scenarioName?: string,
559
+ avoidPrefetchSnapshotCache: boolean = false,
556
560
  ): Promise<ISnapshot | IPrefetchSnapshotContents> {
557
561
  return this.fetchSnapshotFromNetworkCore(
558
562
  hostSnapshotOptions,
559
563
  loadingGroupIds,
560
564
  scenarioName,
565
+ avoidPrefetchSnapshotCache,
561
566
  ).catch((error) => {
562
567
  // Issue #5895:
563
568
  // If we are offline, this error is retryable. But that means that RetriableDocumentStorageService
@@ -576,10 +581,13 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
576
581
  hostSnapshotOptions: ISnapshotOptions | undefined,
577
582
  loadingGroupIds: string[] | undefined,
578
583
  scenarioName?: string,
584
+ avoidPrefetchSnapshotCache: boolean = false,
579
585
  ): Promise<ISnapshot | IPrefetchSnapshotContents> {
580
- // Don't look into cache, if the host specifically tells us so. Also, if request is
581
- // for initial snapshot, don't consult the prefetch cache.
582
- if (!this.hostPolicy.avoidPrefetchSnapshotCache && this.firstSnapshotFetchCall) {
586
+ if (
587
+ !avoidPrefetchSnapshotCache &&
588
+ !this.hostPolicy.avoidPrefetchSnapshotCache &&
589
+ this.firstSnapshotFetchCall
590
+ ) {
583
591
  const prefetchCacheKey = getKeyForCacheEntry(
584
592
  createCacheSnapshotKey(
585
593
  this.odspResolvedUrl,
@@ -668,6 +676,7 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
668
676
  removeEntries,
669
677
  loadingGroupIds,
670
678
  this.hostPolicy.enableRedeemFallback,
679
+ this.hostPolicy.hostProvidedRequestHeaders,
671
680
  );
672
681
  return odspSnapshot;
673
682
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -709,6 +718,7 @@ export class OdspDocumentStorageService extends OdspDocumentStorageServiceBase {
709
718
  removeEntries,
710
719
  loadingGroupIds,
711
720
  this.hostPolicy.enableRedeemFallback,
721
+ this.hostPolicy.hostProvidedRequestHeaders,
712
722
  );
713
723
  return odspSnapshot;
714
724
  }
@@ -21,6 +21,7 @@ import type { TelemetryLoggerExt } from "@fluidframework/telemetry-utils/interna
21
21
  import { type OdspFluidDataStoreLocator, SharingLinkHeader } from "./contractsPublic.js";
22
22
  import { createOdspUrl } from "./createOdspUrl.js";
23
23
  import { getFileLink } from "./getFileLink.js";
24
+ import { copyRequestHeaders } from "./requestHeaders.js";
24
25
  import { OdspDriverUrlResolver } from "./odspDriverUrlResolver.js";
25
26
  import {
26
27
  getLocatorFromOdspUrl,
@@ -48,6 +49,11 @@ export interface ShareLinkFetcherProps {
48
49
  * Identity type determining the shape of share link as it differs for Enterprise and Consumer users.
49
50
  */
50
51
  identityType: IdentityType;
52
+ /**
53
+ * Host-owned attribution metadata for ODSP requests made while resolving share links.
54
+ * Driver-owned headers take precedence on case-insensitive collisions.
55
+ */
56
+ requestHeaders?: Readonly<Record<string, string>>;
51
57
  }
52
58
 
53
59
  /**
@@ -90,6 +96,7 @@ export class OdspDriverUrlResolverForShareLink implements IUrlResolver {
90
96
  this.shareLinkFetcherProps = {
91
97
  ...shareLinkFetcherProps,
92
98
  tokenFetcher: shareLinkFetcherProps.tokenFetcher,
99
+ requestHeaders: copyRequestHeaders(shareLinkFetcherProps.requestHeaders),
93
100
  };
94
101
  }
95
102
  }
@@ -209,6 +216,7 @@ export class OdspDriverUrlResolverForShareLink implements IUrlResolver {
209
216
  this.shareLinkFetcherProps.tokenFetcher,
210
217
  resolvedUrl,
211
218
  this.logger,
219
+ this.shareLinkFetcherProps.requestHeaders,
212
220
  ).catch((error) => {
213
221
  // This should imply that error is a non-retriable error.
214
222
  this.logger.sendErrorEvent({ eventName: "FluidFileUrlError" }, error);
@@ -571,7 +571,11 @@ The injected implementation:
571
571
  document services: a **recoverable** one bound to that base version (its storage is the base
572
572
  snapshot) and a **live** one (its delta storage supplies the ops to replay). Both are created via
573
573
  `createDocumentServiceCore` with a **single shared** `EpochTracker` (the same one the version
574
- manager reads through) — this is the structural lineage guard; see the next question.
574
+ manager reads through) — this is the structural lineage guard; see the next question. The
575
+ point-in-time storage wrapper forces the recoverable service's initial snapshot read to bypass
576
+ both the persistent latest-snapshot cache and the in-memory snapshot-prefetch cache so the
577
+ selected base cannot be replaced by a newer cached live snapshot. Both loader snapshot paths
578
+ (`getSnapshot` and the legacy `getVersions` + `getSnapshotTree` path) apply this rule.
575
579
  3. Return an `OdspPointInTimeDocumentService` composing the two.
576
580
 
577
581
  It lives in this package rather than a generic wrapping driver (e.g. `@fluidframework/replay-driver`)
@@ -28,6 +28,7 @@ import { convertOdspSnapshotToSnapshotTreeAndBlobs } from "../odspSnapshotParser
28
28
  import { getApiRoot } from "../odspUrlHelper.js";
29
29
  import { fetchArray, getWithRetryForTokenRefresh } from "../odspUtils.js";
30
30
  import { pkgVersion as driverVersion } from "../packageVersion.js";
31
+ import { mergeRequestHeaders } from "../requestHeaders.js";
31
32
 
32
33
  /**
33
34
  * A single ODSP file version, as listed by the file's version history.
@@ -96,6 +97,7 @@ export interface OdspFileVersionFetcherProps {
96
97
  readonly getAuthHeader: InstrumentedStorageTokenFetcher;
97
98
  readonly epochTracker: EpochTracker;
98
99
  readonly logger: TelemetryLoggerExt;
100
+ readonly requestHeaders?: Readonly<Record<string, string>>;
99
101
  }
100
102
 
101
103
  /**
@@ -104,7 +106,7 @@ export interface OdspFileVersionFetcherProps {
104
106
  export function createOdspFileVersionFetcher(
105
107
  props: OdspFileVersionFetcherProps,
106
108
  ): IOdspFileVersionFetcher {
107
- const { urlParts, getAuthHeader, epochTracker, logger } = props;
109
+ const { urlParts, getAuthHeader, epochTracker, logger, requestHeaders } = props;
108
110
  const { siteUrl, driveId, itemId } = urlParts;
109
111
 
110
112
  const listFileVersions = async (): Promise<OdspFileVersionRef[]> =>
@@ -209,7 +211,10 @@ export function createOdspFileVersionFetcher(
209
211
  scenarioName,
210
212
  );
211
213
  const headers = getHeadersWithAuth(token);
212
- const response = await fetchArray(url, { method, headers });
214
+ const response = await fetchArray(url, {
215
+ method,
216
+ headers: mergeRequestHeaders(requestHeaders, headers),
217
+ });
213
218
  return response.headers.get("x-fluid-epoch") ?? undefined;
214
219
  });
215
220
 
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/odsp-driver";
9
- export const pkgVersion = "3.0.2";
9
+ export const pkgVersion = "3.1.0";
@@ -51,6 +51,7 @@ async function createVersionManager(
51
51
  logger: TelemetryLoggerExt,
52
52
  epochTracker: EpochTracker,
53
53
  getStorageToken: IOdspPointInTimeDocumentServiceImplementationProps["getStorageToken"],
54
+ requestHeaders?: Readonly<Record<string, string>>,
54
55
  ): Promise<IOdspVersionManager> {
55
56
  const urlParts: IOdspUrlParts = {
56
57
  siteUrl: odspResolvedUrl.siteUrl,
@@ -67,6 +68,7 @@ async function createVersionManager(
67
68
  getAuthHeader,
68
69
  epochTracker,
69
70
  logger,
71
+ requestHeaders,
70
72
  });
71
73
  }
72
74
 
@@ -120,6 +122,7 @@ export async function createPointInTimeDocumentServiceCore(
120
122
  clientIsSummarizer,
121
123
  persistedCache,
122
124
  getStorageToken,
125
+ requestHeaders,
123
126
  createDocumentService,
124
127
  }: IOdspPointInTimeDocumentServiceImplementationProps,
125
128
  dependencies: IPointInTimeDocumentServiceDependencies = {},
@@ -137,12 +140,13 @@ export async function createPointInTimeDocumentServiceCore(
137
140
  },
138
141
  extLogger,
139
142
  clientIsSummarizer,
143
+ requestHeaders,
140
144
  );
141
145
 
142
146
  const versionManager = await (
143
147
  dependencies.createVersionManager ??
144
148
  (async (url, versionLogger, epochTracker) =>
145
- createVersionManager(url, versionLogger, epochTracker, getStorageToken))
149
+ createVersionManager(url, versionLogger, epochTracker, getStorageToken, requestHeaders))
146
150
  )(odspResolvedUrl, extLogger, cacheAndTracker.epochTracker);
147
151
  const baseResult = await versionManager.findBaseForSeq(targetSequenceNumber);
148
152
  if (baseResult.kind === "noBaseVersion") {