@fluid-tools/fetch-tool 2.1.0-274160 → 2.1.0-276985

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-tools/fetch-tool",
3
- "version": "2.1.0-274160",
3
+ "version": "2.1.0-276985",
4
4
  "description": "Console tool to fetch Fluid data from relay service",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -15,20 +15,22 @@
15
15
  "fluid-fetch": "bin/fluid-fetch"
16
16
  },
17
17
  "dependencies": {
18
- "@fluid-internal/client-utils": "2.1.0-274160",
19
- "@fluidframework/container-runtime": "2.1.0-274160",
20
- "@fluidframework/core-interfaces": "2.1.0-274160",
21
- "@fluidframework/core-utils": "2.1.0-274160",
22
- "@fluidframework/datastore": "2.1.0-274160",
23
- "@fluidframework/driver-definitions": "2.1.0-274160",
24
- "@fluidframework/odsp-doclib-utils": "2.1.0-274160",
25
- "@fluidframework/odsp-driver": "2.1.0-274160",
26
- "@fluidframework/odsp-driver-definitions": "2.1.0-274160",
27
- "@fluidframework/odsp-urlresolver": "2.1.0-274160",
28
- "@fluidframework/routerlicious-driver": "2.1.0-274160",
29
- "@fluidframework/routerlicious-urlresolver": "2.1.0-274160",
30
- "@fluidframework/runtime-definitions": "2.1.0-274160",
31
- "@fluidframework/tool-utils": "2.1.0-274160"
18
+ "@azure/identity": "^4.2.0",
19
+ "@azure/identity-cache-persistence": "^1.1.0",
20
+ "@fluid-internal/client-utils": "2.1.0-276985",
21
+ "@fluidframework/container-runtime": "2.1.0-276985",
22
+ "@fluidframework/core-interfaces": "2.1.0-276985",
23
+ "@fluidframework/core-utils": "2.1.0-276985",
24
+ "@fluidframework/datastore": "2.1.0-276985",
25
+ "@fluidframework/driver-definitions": "2.1.0-276985",
26
+ "@fluidframework/odsp-doclib-utils": "2.1.0-276985",
27
+ "@fluidframework/odsp-driver": "2.1.0-276985",
28
+ "@fluidframework/odsp-driver-definitions": "2.1.0-276985",
29
+ "@fluidframework/odsp-urlresolver": "2.1.0-276985",
30
+ "@fluidframework/routerlicious-driver": "2.1.0-276985",
31
+ "@fluidframework/routerlicious-urlresolver": "2.1.0-276985",
32
+ "@fluidframework/runtime-definitions": "2.1.0-276985",
33
+ "@fluidframework/tool-utils": "2.1.0-276985"
32
34
  },
33
35
  "devDependencies": {
34
36
  "@biomejs/biome": "^1.7.3",
@@ -15,15 +15,6 @@ export let paramSnapshotVersionIndex: number | undefined;
15
15
  export let paramNumSnapshotVersions = 10;
16
16
  export let paramActualFormatting = false;
17
17
 
18
- let paramForceTokenReauth = false;
19
-
20
- // Only return true once, to reauth on first call.
21
- export function getForceTokenReauth() {
22
- const result = paramForceTokenReauth;
23
- paramForceTokenReauth = false;
24
- return result;
25
- }
26
-
27
18
  export let paramSaveDir: string | undefined;
28
19
  export const messageTypeFilter = new Set<string>();
29
20
 
@@ -33,12 +24,12 @@ export let paramJWT: string;
33
24
  export let connectToWebSocket = false;
34
25
 
35
26
  export let localDataOnly = false;
27
+ export let loginHint: string | undefined;
36
28
 
37
29
  const optionsArray = [
38
30
  ["--dump:rawmessage", "dump all messages"],
39
31
  ["--dump:snapshotVersion", "dump a list of snapshot version"],
40
32
  ["--dump:snapshotTree", "dump the snapshot trees"],
41
- ["--forceTokenReauth", "Force reauthorize token (SPO only)"],
42
33
  ["--stat:message", "show message type, channel type, data type statistics"],
43
34
  ["--stat:snapshot", "show a table of snapshot path and blob size"],
44
35
  ["--stat", "Show both messages & snapshot stats"],
@@ -53,6 +44,7 @@ const optionsArray = [
53
44
  ["--snapshotVersionIndex <number>", "Index of the version to dump"],
54
45
  ["--websocket", "Connect to web socket to download initial messages"],
55
46
  ["--local", "Do not connect to storage, use earlier downloaded data. Requires --saveDir."],
47
+ ["--loginHint", "login hint for the user with document access."],
56
48
  ];
57
49
 
58
50
  function printUsage() {
@@ -120,9 +112,6 @@ export function parseArguments() {
120
112
  case "--jwt":
121
113
  paramJWT = parseStrArg(i++, "jwt token");
122
114
  break;
123
- case "--forceTokenReauth":
124
- paramForceTokenReauth = true;
125
- break;
126
115
  case "--snapshotVersionIndex":
127
116
  paramSnapshotVersionIndex = parseIntArg(i++, "version index", true);
128
117
  break;
@@ -141,6 +130,9 @@ export function parseArguments() {
141
130
  case "--local":
142
131
  localDataOnly = true;
143
132
  break;
133
+ case "--loginHint":
134
+ loginHint = parseStrArg(i++, "login hint");
135
+ break;
144
136
  default:
145
137
  try {
146
138
  const url = new URL(arg);
@@ -20,10 +20,9 @@ import {
20
20
  } from "@fluidframework/odsp-urlresolver/internal";
21
21
  import * as r11s from "@fluidframework/routerlicious-driver/internal";
22
22
  import { RouterliciousUrlResolver } from "@fluidframework/routerlicious-urlresolver/internal";
23
- import { getMicrosoftConfiguration } from "@fluidframework/tool-utils/internal";
24
23
 
25
24
  import { localDataOnly, paramJWT } from "./fluidFetchArgs.js";
26
- import { resolveWrapper } from "./fluidFetchSharePoint.js";
25
+ import { resolveWrapper, fetchToolClientConfig } from "./fluidFetchSharePoint.js";
27
26
 
28
27
  export let latestVersionsId: string = "";
29
28
  export let connectionInfo: any;
@@ -66,8 +65,6 @@ async function initializeODSPCore(
66
65
  },
67
66
  server,
68
67
  clientConfig,
69
- undefined,
70
- true,
71
68
  );
72
69
  };
73
70
  // eslint-disable-next-line @typescript-eslint/promise-function-async
@@ -173,7 +170,7 @@ export async function fluidFetchInit(urlStr: string) {
173
170
  return initializeODSPCore(
174
171
  odspResolvedUrl,
175
172
  new URL(odspResolvedUrl.siteUrl).host,
176
- getMicrosoftConfiguration(),
173
+ fetchToolClientConfig,
177
174
  );
178
175
  } else if (resolvedInfo.serviceType === "r11s") {
179
176
  const url = new URL(urlStr);
@@ -96,28 +96,44 @@ async function* loadAllSequencedMessages(
96
96
  let requests = 0;
97
97
  let opsStorage = 0;
98
98
 
99
+ console.log("fetching cached messages");
99
100
  // reading only 1 op to test if there is mismatch
100
101
  const teststream = deltaStorage.fetchMessages(lastSeq + 1, lastSeq + 2);
101
102
 
102
- let statusCode;
103
- let innerMostErrorCode;
104
- let response;
105
-
106
103
  try {
107
104
  await teststream.read();
108
105
  } catch (error: any) {
109
- statusCode = error.getTelemetryProperties().statusCode;
110
- innerMostErrorCode = error.getTelemetryProperties().innerMostErrorCode;
111
- // if there is gap between ops, catch the error and check it is the error we need
106
+ const statusCode = error.getTelemetryProperties().statusCode;
107
+ const innerMostErrorCode = error.getTelemetryProperties().innerMostErrorCode;
112
108
  if (statusCode !== 410 || innerMostErrorCode !== "fluidDeltaDataNotAvailable") {
113
109
  throw error;
114
110
  }
115
- // get firstAvailableDelta from the error response, and set current sequence number to that
116
- response = JSON.parse(error.getTelemetryProperties().response);
117
- firstAvailableDelta = response.error.firstAvailableDelta;
118
- lastSeq = firstAvailableDelta - 1;
111
+
112
+ // This indicates we tried to fetch ops from storage that have been deleted (because they are past some retention policy).
113
+ // In that case, the error message should indicate the first sequence number that is available.
114
+ // We make a best-effort attempt for the original query (fetch all ops) by starting from that sequence number.
115
+ const props = error.getTelemetryProperties();
116
+ const { responseMessage } = props;
117
+ const [_, seq] =
118
+ typeof responseMessage === "string"
119
+ ? responseMessage.match(/GenesisSequenceNumber '(\d+)'/) ?? []
120
+ : [];
121
+ if (seq !== undefined) {
122
+ lastSeq = parseInt(seq, 10);
123
+ firstAvailableDelta = lastSeq + 1;
124
+ console.log(
125
+ `Not all ops are available (older ops may have been deleted from storage). Starting from sequenceNumber: ${firstAvailableDelta}.`,
126
+ );
127
+ } else {
128
+ console.log(props);
129
+ throw new Error(
130
+ `Unexpected structure for 410 error: ${error.message}. Further error properties were logged above. This indicates a problem with fetch-tool.`,
131
+ );
132
+ }
119
133
  }
120
134
 
135
+ console.log("fetching remaining messages from delta storage");
136
+
121
137
  // continue reading rest of the ops
122
138
  const stream = deltaStorage.fetchMessages(
123
139
  lastSeq + 1, // inclusive left
@@ -3,8 +3,8 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import child_process from "child_process";
7
-
6
+ import { InteractiveBrowserCredential, useIdentityPlugin } from "@azure/identity";
7
+ import { cachePersistencePlugin } from "@azure/identity-cache-persistence";
8
8
  import { DriverErrorTypes } from "@fluidframework/driver-definitions/internal";
9
9
  import {
10
10
  IPublicClientConfig,
@@ -13,57 +13,71 @@ import {
13
13
  getChildrenByDriveItem,
14
14
  getDriveItemByServerRelativePath,
15
15
  getDriveItemFromDriveAndItem,
16
- getOdspRefreshTokenFn,
16
+ getAadTenant,
17
+ getOdspScope,
17
18
  } from "@fluidframework/odsp-doclib-utils/internal";
18
- import {
19
- IOdspTokenManagerCacheKey,
20
- OdspTokenConfig,
21
- OdspTokenManager,
22
- getMicrosoftConfiguration,
23
- odspTokensCache,
24
- } from "@fluidframework/tool-utils/internal";
25
19
 
26
- import { getForceTokenReauth } from "./fluidFetchArgs.js";
20
+ import { loginHint } from "./fluidFetchArgs.js";
21
+
22
+ // Note: the following page may be helpful for debugging auth issues:
23
+ // https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/identity/identity/TROUBLESHOOTING.md
24
+ // See e.g. the section on setting 'AZURE_LOG_LEVEL'.
25
+ useIdentityPlugin(cachePersistencePlugin);
26
+
27
+ export const fetchToolClientConfig: IPublicClientConfig = {
28
+ get clientId(): string {
29
+ const clientId = process.env.fetch__tool__clientId;
30
+ if (clientId === undefined) {
31
+ throw new Error(
32
+ "Client ID environment variable not set: fetch__tool__clientId. Use the getkeys tool to populate it.",
33
+ );
34
+ }
35
+ return clientId;
36
+ },
37
+ };
27
38
 
28
39
  export async function resolveWrapper<T>(
29
40
  callback: (authRequestInfo: IOdspAuthRequestInfo) => Promise<T>,
30
41
  server: string,
31
42
  clientConfig: IPublicClientConfig,
32
43
  forceTokenReauth = false,
33
- forToken = false,
34
44
  ): Promise<T> {
35
45
  try {
36
- const odspTokenManager = new OdspTokenManager(odspTokensCache);
37
- const tokenConfig: OdspTokenConfig = {
38
- type: "browserLogin",
39
- navigator: fluidFetchWebNavigator,
40
- };
41
- const tokens = await odspTokenManager.getOdspTokens(
42
- server,
43
- clientConfig,
44
- tokenConfig,
45
- undefined /* forceRefresh */,
46
- forceTokenReauth || getForceTokenReauth(),
47
- );
46
+ const credential = new InteractiveBrowserCredential({
47
+ clientId: fetchToolClientConfig.clientId,
48
+ tenantId: getAadTenant(server),
49
+ // NOTE: fetch-tool flows using multiple sets of user credentials haven't been well-tested.
50
+ // Some of the @azure/identity docs suggest we may need to manage authentication records and choose
51
+ // which one to use explicitly here if we have such scenarios.
52
+ // If we start doing this, it may be worth considering using disableAutomaticAuthentication here so we
53
+ // have better control over when interactive auth may be triggered.
54
+ // For now, fetch-tool doesn't work against personal accounts anyway so the only flow that might necessitate this
55
+ // would be grabbing documents using several identities (e.g. test accounts we use for stress testing).
56
+ // In that case, a simple workaround is to delete the cache that @azure/identity uses before running the tool.
57
+ // See docs on `tokenCachePersistenceOptions.name` for information on where this cache is stored.
58
+ loginHint,
59
+ tokenCachePersistenceOptions: {
60
+ enabled: true,
61
+ name: "fetch-tool",
62
+ },
63
+ });
64
+
65
+ const scope = getOdspScope(server);
66
+
67
+ const { token } = await credential.getToken(scope);
48
68
 
49
- const result = await callback({
50
- accessToken: tokens.accessToken,
51
- refreshTokenFn: getOdspRefreshTokenFn(server, clientConfig, tokens),
69
+ return await callback({
70
+ accessToken: token,
71
+ refreshTokenFn: async () => {
72
+ await credential.authenticate(scope);
73
+ const result = await credential.getToken(scope);
74
+ return result.token;
75
+ },
52
76
  });
53
- // If this is used for getting a token, then refresh the cache with new token.
54
- if (forToken) {
55
- const key: IOdspTokenManagerCacheKey = { isPush: false, userOrServer: server };
56
- await odspTokenManager.updateTokensCache(key, {
57
- accessToken: result as any as string,
58
- refreshToken: tokens.refreshToken,
59
- });
60
- return result;
61
- }
62
- return result;
63
77
  } catch (e: any) {
64
78
  if (e.errorType === DriverErrorTypes.authorizationError && !forceTokenReauth) {
65
79
  // Re-auth
66
- return resolveWrapper<T>(callback, server, clientConfig, true, forToken);
80
+ return resolveWrapper<T>(callback, server, clientConfig, true);
67
81
  }
68
82
  throw e;
69
83
  }
@@ -101,12 +115,10 @@ export async function getSharepointFiles(
101
115
  serverRelativePath: string,
102
116
  recurse: boolean,
103
117
  ) {
104
- const clientConfig = getMicrosoftConfiguration();
105
-
106
118
  const fileInfo = await resolveDriveItemByServerRelativePath(
107
119
  server,
108
120
  serverRelativePath,
109
- clientConfig,
121
+ fetchToolClientConfig,
110
122
  );
111
123
  console.log(fileInfo);
112
124
  const pendingFolder: { path: string; folder: IOdspDriveItem }[] = [];
@@ -124,7 +136,7 @@ export async function getSharepointFiles(
124
136
  break;
125
137
  }
126
138
  const { path, folder } = folderInfo;
127
- const children = await resolveChildrenByDriveItem(server, folder, clientConfig);
139
+ const children = await resolveChildrenByDriveItem(server, folder, fetchToolClientConfig);
128
140
  for (const child of children) {
129
141
  const childPath = `${path}/${child.name}`;
130
142
  if (child.isFolder) {
@@ -140,22 +152,10 @@ export async function getSharepointFiles(
140
152
  }
141
153
 
142
154
  export async function getSingleSharePointFile(server: string, drive: string, item: string) {
143
- const clientConfig = getMicrosoftConfiguration();
144
-
145
155
  return resolveWrapper<IOdspDriveItem>(
146
156
  // eslint-disable-next-line @typescript-eslint/promise-function-async
147
157
  (authRequestInfo) => getDriveItemFromDriveAndItem(server, drive, item, authRequestInfo),
148
158
  server,
149
- clientConfig,
159
+ fetchToolClientConfig,
150
160
  );
151
161
  }
152
-
153
- const fluidFetchWebNavigator = (url: string) => {
154
- let message = "Please open browser and navigate to this URL:";
155
- if (process.platform === "win32") {
156
- child_process.exec(`start "fluid-fetch" /B "${url}"`);
157
- message =
158
- "Opening browser to get authorization code. If that doesn't open, please go to this URL manually";
159
- }
160
- console.log(`${message}\n ${url}`);
161
- };