@beam-network/sdk 0.2.23 → 0.3.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.
@@ -1,12 +1,11 @@
1
1
  import { encode, decode } from "@msgpack/msgpack";
2
2
  import { connect } from "nats";
3
- export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v2";
3
+ export const TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION = "transfer-client-control/v3";
4
4
  export const BEAM_DEFAULT_NATS_URL = "tls://nats.b1m.ai:4222";
5
5
  export const BEAM_DEFAULT_NATS_WS_URL = "wss://nats.b1m.ai:443";
6
6
  // NATS enforces max_payload per message. This guard splits signed-route control
7
7
  // messages before the broker rejects them; transfer bytes never flow through NATS.
8
8
  export const BEAM_DEFAULT_MAX_PAYLOAD_BYTES = 24 * 1024 * 1024;
9
- export const ROUTE_RECOVERY_SIGN_MESSAGE_TYPE = "transfer.route_recovery.sign";
10
9
  function buildConnectionOptions(options) {
11
10
  const connectionOptions = {
12
11
  servers: options.natsUrl,
@@ -36,8 +35,12 @@ export class BeamTransferControl {
36
35
  natsUrl;
37
36
  apiKey;
38
37
  nc = null;
38
+ connectionPromise = null;
39
+ closed = false;
39
40
  authToken = null;
40
41
  authTokenExpiresAt = 0;
42
+ authResolvePromise = null;
43
+ terminalWaitCancellations = new Set();
41
44
  constructor(options) {
42
45
  this.apiKey = options.apiKey;
43
46
  this.keyPrefix = options.apiKey.slice(0, 12);
@@ -54,59 +57,29 @@ export class BeamTransferControl {
54
57
  throw new Error("transferRuntimeShardCount must be a positive integer.");
55
58
  }
56
59
  }
57
- async serveRouteRecoverySigner(transferId, handler) {
58
- const nc = await this.connection();
59
- const subject = this.routeRecoverySignSubject(transferId);
60
- const sub = nc.subscribe(subject);
61
- await nc.flush();
62
- let active = true;
63
- void (async () => {
64
- for await (const msg of sub) {
65
- if (!active)
66
- break;
67
- let request = null;
68
- try {
69
- request = decode(msg.data);
70
- if (request.schema_version !== TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION ||
71
- request.environment !== this.environment ||
72
- request.key_prefix !== this.keyPrefix ||
73
- request.transfer_id !== transferId ||
74
- request.message_type !== ROUTE_RECOVERY_SIGN_MESSAGE_TYPE ||
75
- request.producer !== "transfer-runtime") {
76
- throw new Error("route recovery request envelope mismatch");
77
- }
78
- const payload = await handler(request.payload);
79
- msg.respond(encode(this.buildRouteRecoveryReply(request, true, 200, payload)));
80
- }
81
- catch (error) {
82
- const fallback = request ?? {
83
- message_id: "unknown",
84
- request_id: "unknown",
85
- transfer_id: transferId
86
- };
87
- msg.respond(encode(this.buildRouteRecoveryReply(fallback, false, 500, undefined, {
88
- code: "route_recovery_sign_failed",
89
- message: error instanceof Error ? error.message : String(error)
90
- })));
91
- }
92
- }
93
- })();
94
- return () => {
95
- active = false;
96
- sub.unsubscribe();
97
- };
98
- }
99
60
  async close() {
100
- if (!this.nc)
101
- return;
102
- await this.nc.drain();
61
+ this.closed = true;
62
+ for (const cancel of [...this.terminalWaitCancellations])
63
+ cancel();
64
+ if (this.connectionPromise) {
65
+ try {
66
+ await this.connectionPromise;
67
+ }
68
+ catch {
69
+ // A connection that loses the close race closes itself before rejecting.
70
+ }
71
+ }
72
+ const nc = this.nc;
103
73
  this.nc = null;
74
+ if (nc && !nc.isClosed())
75
+ await nc.drain();
104
76
  }
105
- async request(messageType, payload, transferId) {
106
- const nc = await this.connection();
77
+ async request(messageType, payload, options = {}) {
107
78
  const authToken = await this.resolveAuthToken();
108
- const shardId = transferId ? transferShardId(transferId, this.shardCount) : 0;
109
- const requestId = randomId();
79
+ const shardId = options.transferId ? transferShardId(options.transferId, this.shardCount) : 0;
80
+ const requestId = options.idempotencyKey
81
+ ? await stableRequestId(`${messageType}:${options.idempotencyKey}`)
82
+ : randomId();
110
83
  const envelope = {
111
84
  message_id: `${TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION}:${this.environment}:${this.keyPrefix}:${messageType}:${requestId}`,
112
85
  schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
@@ -125,13 +98,120 @@ export class BeamTransferControl {
125
98
  throw new Error(`NATS lifecycle request is ${bytes.byteLength} bytes, above maxPayloadBytes=${this.maxPayloadBytes}`);
126
99
  }
127
100
  const subject = this.requestSubject(messageType, shardId);
128
- const response = await nc.request(subject, bytes, { timeout: this.requestTimeoutMs });
129
- const decoded = decode(response.data);
130
- if (!decoded.ok) {
131
- const body = JSON.stringify(decoded.error ?? {});
132
- throw new Error(`Beam lifecycle request failed with ${decoded.status}: ${body}`);
101
+ let lastError;
102
+ for (let attempt = 0; attempt < LIFECYCLE_REQUEST_MAX_ATTEMPTS; attempt += 1) {
103
+ try {
104
+ const nc = await this.connection();
105
+ const response = await nc.request(subject, bytes, { timeout: this.requestTimeoutMs });
106
+ const decoded = decode(response.data);
107
+ if (decoded.ok)
108
+ return (decoded.payload ?? {});
109
+ const body = JSON.stringify(decoded.error ?? {});
110
+ const error = new Error(`Beam lifecycle request failed with ${decoded.status}: ${body}`);
111
+ if (!isRetryableLifecycleStatus(decoded.status) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
112
+ throw error;
113
+ }
114
+ lastError = error;
115
+ }
116
+ catch (error) {
117
+ if (!isRetryableLifecycleError(error) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
118
+ throw error;
119
+ }
120
+ lastError = error;
121
+ if (this.nc?.isClosed())
122
+ this.nc = null;
123
+ }
124
+ await sleepWithJitter(LIFECYCLE_REQUEST_RETRY_DELAYS_MS[attempt] ?? 1_000);
125
+ }
126
+ throw lastError instanceof Error ? lastError : new Error("Beam lifecycle request failed after retries");
127
+ }
128
+ async openTerminalSignalWaiter(transferId) {
129
+ const nc = await this.connection();
130
+ const pending = [];
131
+ let activeWait = null;
132
+ let closed = false;
133
+ let terminalError = null;
134
+ let subscription = null;
135
+ const settleActive = (event, error) => {
136
+ const wait = activeWait;
137
+ if (!wait)
138
+ return;
139
+ activeWait = null;
140
+ clearTimeout(wait.timer);
141
+ if (error !== undefined)
142
+ wait.reject(error);
143
+ else
144
+ wait.resolve(event);
145
+ };
146
+ const close = async () => {
147
+ if (closed)
148
+ return;
149
+ closed = true;
150
+ this.terminalWaitCancellations.delete(cancel);
151
+ subscription?.unsubscribe();
152
+ settleActive(null);
153
+ };
154
+ const cancel = () => { void close(); };
155
+ try {
156
+ subscription = nc.subscribe(this.terminalSubject(transferId), {
157
+ callback: (error, message) => {
158
+ if (error) {
159
+ terminalError = error;
160
+ subscription?.unsubscribe();
161
+ settleActive(null, error);
162
+ return;
163
+ }
164
+ try {
165
+ const event = parseTerminalEvent(decode(message.data), transferId);
166
+ if (activeWait)
167
+ settleActive(event);
168
+ else if (pending.length === 0)
169
+ pending.push(event);
170
+ }
171
+ catch (decodeError) {
172
+ terminalError = decodeError;
173
+ subscription?.unsubscribe();
174
+ settleActive(null, decodeError);
175
+ }
176
+ }
177
+ });
178
+ await nc.flush();
179
+ if (this.closed) {
180
+ await close();
181
+ throw new Error("NATS lifecycle control is closed");
182
+ }
183
+ this.terminalWaitCancellations.add(cancel);
184
+ if (this.closed) {
185
+ await close();
186
+ throw new Error("NATS lifecycle control is closed");
187
+ }
188
+ return {
189
+ wait: async (timeoutMs) => {
190
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
191
+ throw new Error("timeoutMs must be a positive number.");
192
+ }
193
+ if (closed)
194
+ return null;
195
+ if (terminalError !== null)
196
+ throw terminalError;
197
+ const event = pending.shift();
198
+ if (event)
199
+ return event;
200
+ if (activeWait)
201
+ throw new Error("terminal signal waiter already has an active wait");
202
+ return new Promise((resolve, reject) => {
203
+ const timer = setTimeout(() => settleActive(null), timeoutMs);
204
+ activeWait = { resolve, reject, timer };
205
+ });
206
+ },
207
+ close
208
+ };
209
+ }
210
+ catch (error) {
211
+ subscription?.unsubscribe();
212
+ settleActive(null);
213
+ throw error;
133
214
  }
134
- return (decoded.payload ?? {});
135
215
  }
136
216
  splitRoutesForPayload(messageType, basePayload, routes) {
137
217
  const split = (candidate) => {
@@ -158,56 +238,99 @@ export class BeamTransferControl {
158
238
  return routes.length ? split(routes) : [];
159
239
  }
160
240
  async connection() {
241
+ if (this.closed)
242
+ throw new Error("NATS lifecycle control is closed");
161
243
  if (this.nc && !this.nc.isClosed())
162
244
  return this.nc;
163
- this.nc = await connect(buildConnectionOptions({
164
- natsUrl: this.natsUrl,
165
- apiKey: this.apiKey,
166
- keyPrefix: this.keyPrefix
167
- }));
168
- return this.nc;
245
+ if (this.connectionPromise)
246
+ return this.connectionPromise;
247
+ const pending = (async () => {
248
+ const nc = await connect(buildConnectionOptions({
249
+ natsUrl: this.natsUrl,
250
+ apiKey: this.apiKey,
251
+ keyPrefix: this.keyPrefix
252
+ }));
253
+ if (this.closed) {
254
+ await nc.drain();
255
+ throw new Error("NATS lifecycle control is closed");
256
+ }
257
+ this.nc = nc;
258
+ return nc;
259
+ })();
260
+ this.connectionPromise = pending;
261
+ try {
262
+ return await pending;
263
+ }
264
+ finally {
265
+ if (this.connectionPromise === pending)
266
+ this.connectionPromise = null;
267
+ }
169
268
  }
170
269
  async resolveAuthToken() {
171
270
  const now = Math.floor(Date.now() / 1000);
172
271
  if (this.authToken && this.authTokenExpiresAt - 5 > now)
173
272
  return this.authToken;
174
- const nc = await this.connection();
175
- const response = await nc.request(this.authSubject(), new TextEncoder().encode("{}"), { timeout: this.requestTimeoutMs });
176
- const parsed = JSON.parse(new TextDecoder().decode(response.data));
177
- if (!parsed.ok || !parsed.token) {
178
- throw new Error(`Beam SDK NATS auth resolve failed: ${parsed.error ?? "unknown_error"}`);
273
+ if (this.authResolvePromise)
274
+ return this.authResolvePromise;
275
+ const pending = this.resolveAuthTokenFresh();
276
+ this.authResolvePromise = pending;
277
+ try {
278
+ return await pending;
279
+ }
280
+ finally {
281
+ if (this.authResolvePromise === pending)
282
+ this.authResolvePromise = null;
179
283
  }
180
- const claims = decodeJwtPayload(parsed.token);
181
- this.authToken = parsed.token;
182
- this.authTokenExpiresAt = claims.exp;
183
- return parsed.token;
284
+ }
285
+ async resolveAuthTokenFresh() {
286
+ let lastError;
287
+ for (let attempt = 0; attempt < LIFECYCLE_REQUEST_MAX_ATTEMPTS; attempt += 1) {
288
+ try {
289
+ const nc = await this.connection();
290
+ const response = await nc.request(this.authSubject(), new TextEncoder().encode("{}"), { timeout: this.requestTimeoutMs });
291
+ const parsed = JSON.parse(new TextDecoder().decode(response.data));
292
+ if (!parsed.ok || !parsed.token) {
293
+ throw new Error(`Beam SDK NATS auth resolve failed: ${parsed.error ?? "unknown_error"}`);
294
+ }
295
+ const claims = decodeJwtPayload(parsed.token);
296
+ this.authToken = parsed.token;
297
+ this.authTokenExpiresAt = claims.exp;
298
+ return parsed.token;
299
+ }
300
+ catch (error) {
301
+ if (!isRetryableLifecycleError(error) || attempt + 1 >= LIFECYCLE_REQUEST_MAX_ATTEMPTS) {
302
+ throw error;
303
+ }
304
+ lastError = error;
305
+ if (this.nc?.isClosed())
306
+ this.nc = null;
307
+ await sleepWithJitter(LIFECYCLE_REQUEST_RETRY_DELAYS_MS[attempt] ?? 1_000);
308
+ }
309
+ }
310
+ throw lastError instanceof Error ? lastError : new Error("Beam SDK NATS auth resolve failed after retries");
184
311
  }
185
312
  authSubject() {
186
313
  return `${this.subjectPrefix}.${this.environment}.auth.${this.keyPrefix}.resolve`;
187
314
  }
188
- routeRecoverySignSubject(transferId) {
189
- return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.transfer.${transferId}.route_recovery_sign`;
190
- }
191
- buildRouteRecoveryReply(request, ok, status, payload, error) {
192
- return {
193
- message_id: request.message_id,
194
- schema_version: TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION,
195
- environment: this.environment,
196
- key_prefix: this.keyPrefix,
197
- transfer_id: request.transfer_id,
198
- message_type: ROUTE_RECOVERY_SIGN_MESSAGE_TYPE,
199
- request_id: request.request_id,
200
- occurred_at: new Date().toISOString(),
201
- producer: "sdk",
202
- ok,
203
- status,
204
- ...(payload ? { payload } : {}),
205
- ...(error ? { error } : {})
206
- };
207
- }
208
315
  requestSubject(messageType, shardId) {
209
316
  return `${this.subjectPrefix}.${this.environment}.sdk.${this.keyPrefix}.shard.${shardId}.${messageType.replace(/\./g, "_")}`;
210
317
  }
318
+ terminalSubject(transferId) {
319
+ return `${this.subjectPrefix}.${this.environment}.events.${this.keyPrefix}.${transferId}.terminal`;
320
+ }
321
+ }
322
+ function parseTerminalEvent(value, transferId) {
323
+ if (!value || typeof value !== "object" || Array.isArray(value))
324
+ throw new Error("invalid transfer terminal event");
325
+ const event = value;
326
+ if (event.schema_version !== TRANSFER_CLIENT_CONTROL_SCHEMA_VERSION ||
327
+ event.producer !== "transfer-runtime" ||
328
+ event.transfer_id !== transferId ||
329
+ !["completed", "failed", "cancelled"].includes(String(event.status)) ||
330
+ typeof event.occurred_at !== "string") {
331
+ throw new Error("invalid transfer terminal event");
332
+ }
333
+ return event;
211
334
  }
212
335
  const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
213
336
  "part_number",
@@ -218,9 +341,7 @@ const ROUTE_ATTEMPT_METADATA_KEYS = new Set([
218
341
  "copy_url",
219
342
  "copy_source",
220
343
  "copy_source_range",
221
- "delete_url",
222
- "part_verify_url",
223
- "list_page_url"
344
+ "delete_url"
224
345
  ]);
225
346
  const ROUTE_IDENTITY_METADATA_KEYS = [
226
347
  "source_id",
@@ -232,8 +353,6 @@ const ROUTE_IDENTITY_METADATA_KEYS = [
232
353
  export function compactSignedRoutes(routes) {
233
354
  const source_chunks = [];
234
355
  const sourceRefs = new Map();
235
- const multipart_groups = [];
236
- const groupRefs = new Map();
237
356
  const compactRoutes = [];
238
357
  for (const [routeIndex, route] of routes.entries()) {
239
358
  const sourceKey = JSON.stringify([
@@ -264,21 +383,12 @@ export function compactSignedRoutes(routes) {
264
383
  const deliveryIndex = route.delivery_index ?? nonNegativeInteger(metadata.delivery_index) ?? routeIndex;
265
384
  for (const key of ROUTE_IDENTITY_METADATA_KEYS)
266
385
  delete metadata[key];
267
- const groupBaseKey = multipartGroupKey(route, metadata);
268
- const groupMetadata = Object.fromEntries(Object.entries(metadata).filter(([key]) => !ROUTE_ATTEMPT_METADATA_KEYS.has(key)));
269
- const groupKey = groupBaseKey ? `${groupBaseKey}:${JSON.stringify(groupMetadata)}` : null;
270
- let groupRef;
271
- if (groupKey) {
272
- groupRef = groupRefs.get(groupKey);
273
- if (groupRef === undefined) {
274
- groupRef = multipart_groups.length;
275
- groupRefs.set(groupKey, groupRef);
276
- multipart_groups.push({ group_ref: groupRef, metadata: groupMetadata });
277
- }
278
- for (const key of Object.keys(metadata)) {
279
- if (!ROUTE_ATTEMPT_METADATA_KEYS.has(key))
280
- delete metadata[key];
281
- }
386
+ const multipartGroupId = typeof metadata.multipart_group_id === "string"
387
+ ? metadata.multipart_group_id
388
+ : undefined;
389
+ for (const key of Object.keys(metadata)) {
390
+ if (!ROUTE_ATTEMPT_METADATA_KEYS.has(key))
391
+ delete metadata[key];
282
392
  }
283
393
  compactRoutes.push(compact({
284
394
  source_ref: sourceRef,
@@ -287,21 +397,11 @@ export function compactSignedRoutes(routes) {
287
397
  dest_url: route.dest_url,
288
398
  expires_at: route.expires_at,
289
399
  dest_headers: route.dest_headers,
290
- multipart_group_ref: groupRef,
400
+ multipart_group_id: multipartGroupId,
291
401
  metadata: Object.keys(metadata).length ? metadata : undefined
292
402
  }));
293
403
  }
294
- return { source_chunks, multipart_groups, routes: compactRoutes };
295
- }
296
- function multipartGroupKey(route, metadata) {
297
- const groupId = typeof metadata.multipart_group_id === "string" ? metadata.multipart_group_id : "";
298
- if (groupId)
299
- return `id:${groupId}`;
300
- const uploadId = typeof metadata.upload_id === "string" ? metadata.upload_id : "";
301
- const finalObjectKey = typeof metadata.final_object_key === "string" ? metadata.final_object_key : "";
302
- return uploadId || finalObjectKey
303
- ? JSON.stringify([route.source_id, route.destination_id, uploadId, finalObjectKey])
304
- : null;
404
+ return { source_chunks, routes: compactRoutes };
305
405
  }
306
406
  function nonNegativeInteger(value) {
307
407
  const parsed = typeof value === "number" ? value : Number(value);
@@ -340,3 +440,41 @@ function randomId() {
340
440
  return cryptoApi.randomUUID();
341
441
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
342
442
  }
443
+ const LIFECYCLE_REQUEST_MAX_ATTEMPTS = 3;
444
+ const LIFECYCLE_REQUEST_RETRY_DELAYS_MS = [150, 500];
445
+ function isRetryableLifecycleStatus(status) {
446
+ return status === 408 || status === 425 || status === 429 || status >= 500;
447
+ }
448
+ function isRetryableLifecycleError(error) {
449
+ const record = error && typeof error === "object" ? error : null;
450
+ const code = typeof record?.code === "string" ? record.code : "";
451
+ const message = error instanceof Error ? error.message : String(error);
452
+ const combined = `${code} ${message}`.toLowerCase();
453
+ return [
454
+ "timeout",
455
+ "no responders",
456
+ "connection closed",
457
+ "disconnected",
458
+ "econnreset",
459
+ "econnrefused",
460
+ "etimedout",
461
+ "socket",
462
+ "network"
463
+ ].some((token) => combined.includes(token));
464
+ }
465
+ async function sleepWithJitter(baseMs) {
466
+ const jitter = Math.floor(baseMs * (Math.random() * 0.4 - 0.2));
467
+ await new Promise((resolve) => setTimeout(resolve, Math.max(1, baseMs + jitter)));
468
+ }
469
+ async function stableRequestId(identity) {
470
+ const bytes = new TextEncoder().encode(identity);
471
+ let digest;
472
+ if (globalThis.crypto?.subtle) {
473
+ digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes));
474
+ }
475
+ else {
476
+ const { createHash } = await import("node:crypto");
477
+ digest = new Uint8Array(createHash("sha256").update(bytes).digest());
478
+ }
479
+ return `idempotent-${[...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
480
+ }
@@ -14,16 +14,21 @@ export declare function prepareProviderSourceForPlan(source: ProviderSourceConfi
14
14
  export declare function prepareProviderDestination(destination: ProviderDestinationConfig, options?: {
15
15
  index?: number;
16
16
  }): PreparedDestination;
17
- export declare function createMultipartUpload(destination: ProviderDestinationConfig, objectKey: string): Promise<string>;
17
+ export declare function createMultipartUpload(input: {
18
+ destination: ProviderDestinationConfig;
19
+ objectKey: string;
20
+ metadata: Record<string, string>;
21
+ }): Promise<string>;
22
+ export declare function signFinalObjectHead(destination: ProviderDestinationConfig, objectKey: string, expiresIn: number): Promise<string>;
18
23
  export declare function signCompleteMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
19
24
  export declare function signAbortMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number): Promise<string>;
20
- export declare function signListObjects(destination: ProviderDestinationConfig, prefix: string, expiresIn: number): Promise<string>;
25
+ export declare function signListObjects(destination: ProviderDestinationConfig, prefix: string, expiresIn: number, options?: {
26
+ startAfter?: string;
27
+ }): Promise<string>;
21
28
  export declare function signListMultipartUpload(destination: ProviderDestinationConfig, objectKey: string, uploadId: string, expiresIn: number, options?: {
22
29
  maxParts?: number;
23
30
  partNumberMarker?: number;
24
31
  }): Promise<string>;
25
- export declare function listPartsPageMarker(partNumber: number, pageSize?: number): number | undefined;
26
- export declare function listPartsVerifyMarker(partNumber: number): number | undefined;
27
32
  export declare function signUploadPartCopy(destination: ProviderDestinationConfig, finalObjectKey: string, stagingObjectKey: string, uploadId: string, partNumber: number, expiresIn: number, copySourceRange?: string): Promise<{
28
33
  copyUrl: string;
29
34
  copySource: string;
@@ -36,20 +41,10 @@ export declare function signDestinationRoute(input: {
36
41
  source?: ProviderSourceConfig;
37
42
  destination: ProviderDestinationConfig;
38
43
  expiresIn: number;
39
- uploadId?: string;
40
44
  partNumber?: number;
41
- multipartObjectKey?: string;
42
- completeUrl?: string;
43
- abortUrl?: string;
44
- listUrl?: string;
45
- transferId?: string;
46
45
  multipartGroupId?: string;
47
- multipartCreatedAt?: string;
48
- commitMethod?: "upload_part" | "upload_part_copy";
49
- partVerifyUrl?: string;
50
- listPageUrl?: string;
46
+ commitMethod?: "upload_part_copy";
51
47
  stagingObjectKey?: string;
52
- stagingListUrl?: string;
53
48
  copyUrl?: string;
54
49
  copySource?: string;
55
50
  copySourceRange?: string;