@milaboratories/pl-client 3.8.1 → 3.9.1

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.
@@ -27,6 +27,18 @@ function isVersionAtLeast(version, target) {
27
27
  for (let i = 0; i < 3; i++) if (parsed[i] !== target[i]) return parsed[i] > target[i];
28
28
  return true;
29
29
  }
30
+ function isAfterVersion(version, target) {
31
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(.*)$/.exec(version);
32
+ if (!match) return false;
33
+ const parsed = [
34
+ Number(match[1]),
35
+ Number(match[2]),
36
+ Number(match[3])
37
+ ];
38
+ const suffix = match[4];
39
+ for (let i = 0; i < 3; i++) if (parsed[i] !== target[i]) return parsed[i] > target[i];
40
+ return suffix !== "";
41
+ }
30
42
  var WireClientProviderImpl = class {
31
43
  client = void 0;
32
44
  constructor(wireOpts, clientConstructor) {
@@ -446,6 +458,22 @@ var LLPlClient = class LLPlClient {
446
458
  ]);
447
459
  }
448
460
  /**
461
+ * True if the backend honors per-file `permissions` on workdir fill rules
462
+ * (PR #1830 in milaboratory/pl). Backends before this change ignore the
463
+ * requested mode and always land files at the canonical archive perm,
464
+ * making `exec.builder().writeFile/addFile({ writable: true })` a no-op.
465
+ *
466
+ * Tagged at 3.5.0 cut without the change, so [3, 5, 0] excludes the tagged
467
+ * release but includes dev builds past the tag (e.g. "3.5.0-224-g0ca182").
468
+ */
469
+ get supportsWritableWorkdirFiles() {
470
+ return isAfterVersion(this.serverInfo.coreVersion, [
471
+ 3,
472
+ 5,
473
+ 0
474
+ ]);
475
+ }
476
+ /**
449
477
  * Detects the best available wire protocol.
450
478
  * If wireProtocol is explicitly configured, does nothing.
451
479
  * Otherwise probes the current protocol via ping; if it fails, switches to the alternative.
@@ -1 +1 @@
1
- {"version":3,"file":"ll_client.js","names":["GrpcPlApiClient","status","GrpcStatus"],"sources":["../../src/core/ll_client.ts"],"sourcesContent":["import { PlatformClient as GrpcPlApiClient } from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client\";\nimport type { ClientOptions, Interceptor } from \"@grpc/grpc-js\";\nimport {\n ChannelCredentials,\n InterceptingCall,\n status as GrpcStatus,\n compressionAlgorithms,\n} from \"@grpc/grpc-js\";\nimport type {\n AuthInformation,\n AuthOps,\n PlClientConfig,\n PlConnectionStatus,\n PlConnectionStatusListener,\n} from \"./config\";\nimport { plAddressToConfig, type wireProtocol, SUPPORTED_WIRE_PROTOCOLS } from \"./config\";\nimport type { GrpcOptions } from \"@protobuf-ts/grpc-transport\";\nimport { GrpcTransport } from \"@protobuf-ts/grpc-transport\";\nimport { LLPlTransaction } from \"./ll_transaction\";\nimport { parsePlJwt } from \"../util/pl\";\nimport { type Dispatcher, interceptors } from \"undici\";\nimport type { Middleware } from \"openapi-fetch\";\nimport { inferAuthRefreshTime } from \"./auth\";\nimport { hasCapability, type BackendCapability } from \"./capabilities\";\nimport { defaultHttpDispatcher } from \"@milaboratories/pl-http\";\nimport type { WireClientProvider, WireClientProviderFactory, WireConnection } from \"./wire\";\nimport { parseHttpAuth } from \"@milaboratories/pl-model-common\";\nimport type * as grpcTypes from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport {\n type PlApiPaths,\n type PlRestClientType,\n createClient,\n parseResponseError,\n} from \"../proto-rest\";\nimport { notEmpty, retry, withTimeout, type RetryOptions } from \"@milaboratories/ts-helpers\";\nimport { Code } from \"../proto-grpc/google/rpc/code\";\nimport { WebSocketBiDiStream } from \"./websocket_stream\";\nimport {\n AuthAPI_Role,\n TxAPI_ClientMessage,\n TxAPI_ServerMessage,\n} from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { isAbortedError } from \"./errors\";\n\nexport interface PlCallOps {\n timeout?: number;\n abortSignal?: AbortSignal;\n}\n\n// Parses leading \"<major>.<minor>.<patch>\" from a version string like\n// \"3.1.1\" or \"3.1.1-rc1\" and returns true if the parsed version is >= target.\n// Returns false for unparseable versions (safer to assume an old backend).\nfunction isVersionAtLeast(version: string, target: [number, number, number]): boolean {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)/.exec(version);\n if (!match) return false;\n const parsed: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];\n for (let i = 0; i < 3; i++) {\n if (parsed[i] !== target[i]) return parsed[i] > target[i];\n }\n return true;\n}\n\nclass WireClientProviderImpl<Client> implements WireClientProvider<Client> {\n private client: Client | undefined = undefined;\n\n constructor(\n private readonly wireOpts: () => WireConnection,\n private readonly clientConstructor: (wireOpts: WireConnection) => Client,\n ) {}\n\n public reset(): void {\n this.client = undefined;\n }\n\n public get(): Client {\n if (this.client === undefined) this.client = this.clientConstructor(this.wireOpts());\n return this.client;\n }\n}\n\n/** Abstract out low level networking and authorization details */\nexport class LLPlClient implements WireClientProviderFactory {\n /** Initial authorization information */\n private authInformation?: AuthInformation;\n /** Will be executed by the client when it is required */\n private readonly onAuthUpdate?: (newInfo: AuthInformation) => void;\n /** Will be executed if auth-related error happens during normal client operation */\n private readonly onAuthError?: () => void;\n /** Will be executed by the client when it is required */\n private readonly onAuthRefreshProblem?: (error: unknown) => void;\n /** Threshold after which auth info refresh is required */\n private refreshTimestamp?: number;\n\n /** Cached Ping response. Populated by build() before it returns; refreshed by every later ping(). */\n private _serverInfo?: grpcTypes.MaintenanceAPI_Ping_Response;\n private _authMethodsSync?: grpcTypes.AuthAPI_ListMethods_Response;\n\n private _status: PlConnectionStatus = \"OK\";\n private readonly statusListener?: PlConnectionStatusListener;\n\n private _wireProto: wireProtocol = \"grpc\";\n private _wireConn!: WireConnection;\n\n private readonly _restInterceptors: Dispatcher.DispatcherComposeInterceptor[];\n private readonly _restMiddlewares: Middleware[];\n private readonly _grpcInterceptors: Interceptor[];\n private readonly providers: WeakRef<WireClientProviderImpl<any>>[] = [];\n\n public readonly clientProvider: WireClientProvider<PlRestClientType | GrpcPlApiClient>;\n\n public readonly httpDispatcher: Dispatcher;\n\n public static async build(\n configOrAddress: PlClientConfig | string,\n ops: {\n auth?: AuthOps;\n statusListener?: PlConnectionStatusListener;\n shouldUseGzip?: boolean;\n logger?: MiLogger;\n useAutoDetectWireProtocol?: boolean;\n } = {},\n ) {\n const conf =\n typeof configOrAddress === \"string\" ? plAddressToConfig(configOrAddress) : configOrAddress;\n\n const pl = new LLPlClient(conf, ops);\n\n // FIXME(rfiskov)[MILAB-5275]: Investigate why autodetect randomly fails; temporary turn it off.\n if (ops.useAutoDetectWireProtocol) {\n await pl.detectOptimalWireProtocol();\n }\n\n // Guarantee a ping happened so capability-gated paths (login, refresh) can branch synchronously.\n // In the autodetect path the loop's last successful ping already populated _serverInfo via the\n // side-effect in ping(); this fallback covers the path where autodetect is disabled.\n if (!pl._serverInfo) await pl.ping();\n\n // Guarantee authMethods happened so client can make weighted decision on which auth method to use.\n if (!pl._authMethodsSync) await pl.authMethods();\n\n return pl;\n }\n\n private constructor(\n public readonly conf: PlClientConfig,\n private readonly ops: {\n auth?: AuthOps;\n statusListener?: PlConnectionStatusListener;\n shouldUseGzip?: boolean;\n logger?: MiLogger;\n } = {},\n ) {\n const { auth, statusListener } = ops;\n\n if (auth !== undefined) {\n this.refreshTimestamp = inferAuthRefreshTime(\n auth.authInformation,\n this.conf.authMaxRefreshSeconds,\n );\n this.authInformation = auth.authInformation;\n this.onAuthUpdate = auth.onUpdate;\n this.onAuthRefreshProblem = auth.onUpdateError;\n this.onAuthError = auth.onAuthError;\n }\n\n this._restInterceptors = [];\n this._restMiddlewares = [];\n this._grpcInterceptors = [];\n\n if (auth !== undefined) {\n this._restInterceptors.push(this.createRestAuthInterceptor());\n this._grpcInterceptors.push(this.createGrpcAuthInterceptor());\n }\n this._restInterceptors.push(interceptors.retry({ statusCodes: [] })); // Handle errors with openapi-fetch middleware.\n this._restMiddlewares.push(this.createRestErrorMiddleware());\n this._grpcInterceptors.push(this.createGrpcErrorInterceptor());\n\n this.httpDispatcher = defaultHttpDispatcher(this.conf.httpProxy);\n if (this.conf.wireProtocol) {\n this._wireProto = this.conf.wireProtocol;\n }\n\n this.initWireConnection(this._wireProto);\n\n if (statusListener !== undefined) {\n this.statusListener = statusListener;\n statusListener(this._status);\n }\n\n this.clientProvider = this.createWireClientProvider((wireConn) => {\n if (wireConn.type === \"grpc\") {\n return new GrpcPlApiClient(wireConn.Transport);\n } else {\n return createClient<PlApiPaths>({\n hostAndPort: wireConn.Config.hostAndPort,\n ssl: wireConn.Config.ssl,\n dispatcher: wireConn.Dispatcher,\n middlewares: wireConn.Middlewares,\n });\n }\n });\n }\n\n private initWireConnection(protocol: wireProtocol) {\n switch (protocol) {\n case \"rest\":\n this.initRestConnection();\n return;\n case \"grpc\":\n this.initGrpcConnection(this.ops.shouldUseGzip ?? false);\n return;\n default:\n ((v: never) => {\n throw new Error(\n `Unsupported wire protocol '${v as string}'. Use one of: ${SUPPORTED_WIRE_PROTOCOLS.join(\", \")}`,\n );\n })(protocol);\n }\n }\n\n private initRestConnection(): void {\n const dispatcher = defaultHttpDispatcher(this.conf.grpcProxy, this._restInterceptors);\n this._replaceWireConnection({\n type: \"rest\",\n Config: this.conf,\n Dispatcher: dispatcher,\n Middlewares: this._restMiddlewares,\n });\n }\n\n /**\n * Initializes (or reinitializes) _grpcTransport\n * @param gzip - whether to enable gzip compression\n */\n private initGrpcConnection(gzip: boolean) {\n const clientOptions: ClientOptions = {\n \"grpc.keepalive_time_ms\": 30_000, // 30 seconds\n \"grpc.service_config_disable_resolution\": 1, // Disable DNS TXT lookups for service config\n interceptors: this._grpcInterceptors,\n };\n\n if (gzip) clientOptions[\"grpc.default_compression_algorithm\"] = compressionAlgorithms.gzip;\n\n //\n // Leaving it here for now\n // https://github.com/grpc/grpc-node/issues/2788\n //\n // We should implement message pooling algorithm to overcome hardcoded NO_DELAY behaviour\n // of HTTP/2 and allow our small messages to batch together.\n //\n const grpcOptions: GrpcOptions = {\n host: this.conf.hostAndPort,\n timeout: this.conf.defaultRequestTimeout,\n channelCredentials: this.conf.ssl\n ? ChannelCredentials.createSsl()\n : ChannelCredentials.createInsecure(),\n clientOptions,\n };\n\n const grpcProxy =\n typeof this.conf.grpcProxy === \"string\" ? { url: this.conf.grpcProxy } : this.conf.grpcProxy;\n\n if (grpcProxy?.url) {\n const url = new URL(grpcProxy.url);\n if (grpcProxy.auth) {\n const parsed = parseHttpAuth(grpcProxy.auth);\n if (parsed.scheme !== \"Basic\") {\n throw new Error(`Unsupported auth scheme: ${parsed.scheme as string}.`);\n }\n url.username = parsed.username;\n url.password = parsed.password;\n }\n process.env.grpc_proxy = url.toString();\n } else {\n delete process.env.grpc_proxy;\n }\n\n this._replaceWireConnection({ type: \"grpc\", Transport: new GrpcTransport(grpcOptions) });\n }\n\n private _replaceWireConnection(newConn: WireConnection): void {\n const oldConn = this._wireConn;\n this._wireConn = newConn;\n this._wireProto = newConn.type;\n\n // Reset all providers to let them reinitialize their clients\n for (let i = 0; i < this.providers.length; i++) {\n const provider = this.providers[i].deref();\n if (provider === undefined) {\n // at the same time we need to remove providers that are no longer valid\n this.providers.splice(i, 1);\n i--;\n } else {\n provider.reset();\n }\n }\n\n if (oldConn !== undefined && oldConn.type === \"grpc\") oldConn.Transport.close();\n }\n\n private providerCleanupCounter = 0;\n\n /**\n * Creates a provider for a grpc client. Returned provider will create fresh client whenever the underlying transport is reset.\n *\n * @param clientConstructor - a factory function that creates a grpc client\n */\n public createWireClientProvider<Client>(\n clientConstructor: (transport: WireConnection) => Client,\n ): WireClientProvider<Client> {\n // We need to cleanup providers periodically to avoid memory leaks.\n // This is a simple heuristic to avoid memory leaks.\n // We could use a more sophisticated algorithm, but this is good enough for now.\n this.providerCleanupCounter++;\n if (this.providerCleanupCounter >= 16) {\n for (let i = 0; i < this.providers.length; i++) {\n const provider = this.providers[i].deref();\n if (provider === undefined) {\n this.providers.splice(i, 1);\n i--;\n }\n }\n this.providerCleanupCounter = 0;\n }\n\n const provider = new WireClientProviderImpl<Client>(() => this._wireConn, clientConstructor);\n this.providers.push(new WeakRef(provider));\n return provider;\n }\n\n public get wireConnection(): WireConnection {\n return this._wireConn;\n }\n\n public get wireProtocol(): wireProtocol | undefined {\n return this._wireProto;\n }\n\n /** Returns true if client is authenticated. Even with anonymous auth information\n * connection is considered authenticated. Unauthenticated clients are used for\n * login and similar tasks, see {@link UnauthenticatedPlClient}. */\n public get authenticated(): boolean {\n return this.authInformation !== undefined;\n }\n\n /** null means anonymous connection */\n public get authUser(): string | null {\n if (!this.authenticated) throw new Error(\"Client is not authenticated\");\n if (this.authInformation?.jwtToken) {\n if (this.hasCapability(\"auth:v2\")) {\n return parsePlJwt(this.authInformation?.jwtToken).sub;\n }\n return parsePlJwt(this.authInformation?.jwtToken).user.login;\n } else return null;\n }\n\n private updateStatus(newStatus: PlConnectionStatus) {\n process.nextTick(() => {\n if (this._status !== newStatus) {\n this._status = newStatus;\n if (this.statusListener !== undefined) this.statusListener(this._status);\n if (newStatus === \"Unauthenticated\" && this.onAuthError !== undefined) this.onAuthError();\n }\n });\n }\n\n public get status(): PlConnectionStatus {\n return this._status;\n }\n\n private authRefreshInProgress: boolean = false;\n\n private refreshAuthInformationIfNeeded(): void {\n if (\n this.refreshTimestamp === undefined ||\n Date.now() < this.refreshTimestamp ||\n this.authRefreshInProgress ||\n this._status === \"Unauthenticated\"\n )\n return;\n\n // Running refresh in background`\n this.authRefreshInProgress = true;\n void (async () => {\n try {\n const ttl = BigInt(this.conf.authTTLSeconds);\n const token = this.hasCapability(\"auth:v2\")\n ? await this.refreshToken({ ttlSeconds: ttl })\n : await this.getJwtToken(ttl);\n this.authInformation = { jwtToken: token };\n this.refreshTimestamp = inferAuthRefreshTime(\n this.authInformation,\n this.conf.authMaxRefreshSeconds,\n );\n if (this.onAuthUpdate) this.onAuthUpdate(this.authInformation);\n } catch (e: unknown) {\n if (this.onAuthRefreshProblem) this.onAuthRefreshProblem(e);\n } finally {\n this.authRefreshInProgress = false;\n }\n })();\n }\n\n /**\n * Creates middleware that parses error responses and handles them centrally.\n * This middleware runs before openapi-fetch parses the response, so we need to\n * manually parse the response body for error responses.\n */\n private createRestErrorMiddleware(): Middleware {\n return {\n onResponse: async ({ request: _request, response, options: _options }) => {\n const { body, ...resOptions } = response;\n\n if ([502, 503, 504].includes(response.status)) {\n // Service unavailable, bad gateway, gateway timeout\n this.updateStatus(\"Disconnected\");\n return new Response(body, { ...resOptions, status: response.status });\n }\n\n const respErr = await parseResponseError(response);\n if (!respErr.error) {\n // No error: nice!\n return new Response(respErr.origBody ?? body, { ...resOptions, status: response.status });\n }\n\n if (typeof respErr.error === \"string\") {\n // Non-standard error or normal response: let later middleware to deal wit it.\n return new Response(respErr.error, { ...resOptions, status: response.status });\n }\n\n if (respErr.error.code === Code.UNAUTHENTICATED) {\n this.updateStatus(\"Unauthenticated\");\n }\n\n // Let later middleware to deal with standard gRPC error.\n return new Response(respErr.origBody, { ...resOptions, status: response.status });\n },\n };\n }\n\n /** Detects certain errors and update client status accordingly when using GRPC wire connection */\n private createGrpcErrorInterceptor(): Interceptor {\n return (options, nextCall) => {\n return new InterceptingCall(nextCall(options), {\n start: (metadata, listener, next) => {\n next(metadata, {\n onReceiveStatus: (status, next) => {\n if (status.code == GrpcStatus.UNAUTHENTICATED)\n // (!!!) don't change to \"===\"\n this.updateStatus(\"Unauthenticated\");\n if (status.code == GrpcStatus.UNAVAILABLE)\n // (!!!) don't change to \"===\"\n this.updateStatus(\"Disconnected\");\n next(status);\n },\n });\n },\n });\n };\n }\n\n private createRestAuthInterceptor(): Dispatcher.DispatcherComposeInterceptor {\n return (dispatch) => {\n return (options, handler) => {\n if (this.authInformation?.jwtToken !== undefined) {\n // TODO: check this magic really works and gets called\n options.headers = {\n ...options.headers,\n authorization: \"Bearer \" + this.authInformation.jwtToken,\n };\n this.refreshAuthInformationIfNeeded();\n }\n\n return dispatch(options, handler);\n };\n };\n }\n\n /** Injects authentication information if needed */\n private createGrpcAuthInterceptor(): Interceptor {\n return (options, nextCall) => {\n return new InterceptingCall(nextCall(options), {\n start: (metadata, listener, next) => {\n if (this.authInformation?.jwtToken !== undefined) {\n metadata.set(\"authorization\", \"Bearer \" + this.authInformation.jwtToken);\n this.refreshAuthInformationIfNeeded();\n next(metadata, listener);\n } else {\n next(metadata, listener);\n }\n },\n });\n };\n }\n\n public async getJwtToken(\n ttlSeconds: bigint,\n options?: { authorization?: string; role?: AuthAPI_Role },\n ): Promise<string> {\n const cl = this.clientProvider.get();\n const role = options?.role ?? AuthAPI_Role.UNSPECIFIED;\n\n if (cl instanceof GrpcPlApiClient) {\n const meta: Record<string, string> = {};\n if (options?.authorization) meta.authorization = options.authorization;\n return (\n await cl.getJWTToken(\n {\n expiration: { seconds: ttlSeconds, nanos: 0 },\n requestedRole: role,\n },\n { meta },\n ).response\n ).token;\n } else {\n const headers: Record<string, string> = {};\n if (options?.authorization) headers.authorization = options.authorization;\n const resp = cl.POST(\"/v1/auth/jwt-token\", {\n body: { expiration: `${ttlSeconds}s`, requestedRole: role },\n headers,\n });\n return notEmpty((await resp).data, \"REST: empty response for JWT token request\").token;\n }\n }\n\n /** Login via username/password. Returns a fresh JWT. Backend creates a new session per call. */\n public async loginBasic(\n user: string,\n password: string,\n opts: { ttlSeconds?: bigint; role?: AuthAPI_Role } = {},\n ): Promise<string> {\n const cl = this.clientProvider.get();\n const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);\n const role = opts.role ?? AuthAPI_Role.UNSPECIFIED;\n\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.login({\n credentials: {\n oneofKind: \"basic\",\n basic: { login: user, password },\n },\n expiration: { seconds: ttl, nanos: 0 },\n requestedRole: role,\n }).response\n ).token;\n } else {\n const resp = cl.POST(\"/v1/auth/login\", {\n // openapi-typescript generated all body fields as required, but Login.Request\n // has a credentials oneof — only one of `basic`/`token` is sent. Cast around it.\n body: {\n basic: { login: user, password },\n expiration: `${ttl}s`,\n requestedRole: role,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any,\n });\n return notEmpty((await resp).data, \"REST: empty response for login request\").token;\n }\n }\n\n /** Login via opaque bearer token (controller pre-shared secret, OIDC id-token, etc.).\n * String input is UTF-8 encoded. Returns a fresh Platforma JWT. */\n public async loginWithToken(\n token: Uint8Array | string,\n opts: { ttlSeconds?: bigint; role?: AuthAPI_Role } = {},\n ): Promise<string> {\n const cl = this.clientProvider.get();\n const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);\n const role = opts.role ?? AuthAPI_Role.UNSPECIFIED;\n const bytes = typeof token === \"string\" ? Buffer.from(token, \"utf8\") : token;\n\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.login({\n credentials: {\n oneofKind: \"token\",\n token: { token: bytes },\n },\n expiration: { seconds: ttl, nanos: 0 },\n requestedRole: role,\n }).response\n ).token;\n } else {\n const resp = cl.POST(\"/v1/auth/login\", {\n // openapi-typescript marks all body fields as required, but Login.Request has a oneof.\n // REST encodes `bytes` as a base64 string.\n body: {\n token: { token: Buffer.from(bytes).toString(\"base64\") },\n expiration: `${ttl}s`,\n requestedRole: role,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any,\n });\n return notEmpty((await resp).data, \"REST: empty response for login request\").token;\n }\n }\n\n /** Refresh the current JWT, preserving session id and role. */\n public async refreshToken(opts: { ttlSeconds?: bigint } = {}): Promise<string> {\n const cl = this.clientProvider.get();\n const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);\n const currentToken = notEmpty(\n this.authInformation?.jwtToken,\n \"refreshToken called without a current JWT\",\n );\n\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.refreshToken({\n token: currentToken,\n expiration: { seconds: ttl, nanos: 0 },\n }).response\n ).token;\n } else {\n const resp = cl.POST(\"/v1/auth/refresh\", {\n body: { token: currentToken, expiration: `${ttl}s` },\n });\n return notEmpty((await resp).data, \"REST: empty response for refresh request\").token;\n }\n }\n\n public async ping(): Promise<grpcTypes.MaintenanceAPI_Ping_Response> {\n const cl = this.clientProvider.get();\n let resp: grpcTypes.MaintenanceAPI_Ping_Response;\n if (cl instanceof GrpcPlApiClient) {\n resp = (await cl.ping({})).response;\n } else {\n // The REST ping response predates the `capabilities` field (proto field 9).\n // Old servers omit it; treat absence as empty capability list.\n const pingData = notEmpty(\n (await cl.GET(\"/v1/ping\")).data,\n \"REST: empty response for ping request\",\n );\n resp = {\n ...(pingData as unknown as grpcTypes.MaintenanceAPI_Ping_Response),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n capabilities: (pingData as any).capabilities ?? [],\n };\n }\n this._serverInfo = resp;\n return resp;\n }\n\n /** Cached Ping response. Always populated post-build(); throws if accessed earlier. */\n public get serverInfo(): grpcTypes.MaintenanceAPI_Ping_Response {\n if (!this._serverInfo) {\n throw new Error(\"LLPlClient.serverInfo accessed before build() completed\");\n }\n return this._serverInfo;\n }\n\n /** Synchronous capability check against the cached Ping response. */\n public hasCapability(capability: BackendCapability): boolean {\n return hasCapability(this.serverInfo.capabilities, capability);\n }\n\n /** True if the backend implements the setDefaultColor TX request. */\n public get supportsSetDefaultColor(): boolean {\n return isVersionAtLeast(this.serverInfo.coreVersion, [3, 3, 0]);\n }\n\n /**\n * Detects the best available wire protocol.\n * If wireProtocol is explicitly configured, does nothing.\n * Otherwise probes the current protocol via ping; if it fails, switches to the alternative.\n */\n private async detectOptimalWireProtocol() {\n if (this.conf.wireProtocol) {\n return;\n }\n\n // Each retry is:\n // - ping request timeout (100 to 3_000ms)\n // - backoff delay (30 to 500ms)\n //\n // 30 attempts are ~43 seconds of overall waiting time.\n // Think twice on overall time this thing takes to complete when changing these parameters.\n // It may block UI when connecting to the server and loading projects list.\n const pingTimeoutFactor = 1.3;\n const maxPingTimeoutMs = 3_000;\n const retryOptions: RetryOptions = {\n type: \"exponentialBackoff\",\n maxAttempts: 30,\n initialDelay: 30,\n backoffMultiplier: 1.3,\n jitter: 0.2,\n maxDelay: 500,\n };\n\n let attempt = 1;\n let pingTimeoutMs = 100;\n await retry(\n () => withTimeout(this.ping(), pingTimeoutMs),\n retryOptions,\n (e: unknown) => {\n if (isAbortedError(e)) {\n this.ops.logger?.info(\n `Wire proto autodetect: ping timed out after ${pingTimeoutMs}ms: attempt=${attempt}, wire=${this._wireProto}`,\n );\n\n if (attempt % 2 === 0) {\n // We have 2 wire protocols to check. Increase timeout each 2 attempts.\n pingTimeoutMs = Math.min(\n Math.round(pingTimeoutMs * pingTimeoutFactor),\n maxPingTimeoutMs,\n );\n }\n } else {\n this.ops.logger?.info(\n `Wire proto autodetect: ping failed: attempt=${attempt}, wire=${this._wireProto}, err=${String(e)}`,\n );\n }\n\n attempt++;\n const protocol = this._wireProto === \"grpc\" ? \"rest\" : \"grpc\";\n this.ops.logger?.info(\n `Wire protocol autodetect next attempt: will try wire '${protocol}' with timeout ${pingTimeoutMs}ms`,\n );\n this.initWireConnection(protocol);\n return true;\n },\n );\n }\n\n public async license(): Promise<grpcTypes.MaintenanceAPI_License_Response> {\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n return (await cl.license({})).response;\n } else {\n const resp = notEmpty(\n (await cl.GET(\"/v1/license\")).data,\n \"REST: empty response for license request\",\n );\n return {\n status: resp.status,\n isOk: resp.isOk,\n responseBody: Uint8Array.from(Buffer.from(resp.responseBody)),\n };\n }\n }\n\n public async authMethods(): Promise<grpcTypes.AuthAPI_ListMethods_Response> {\n const cl = this.clientProvider.get();\n let resp: grpcTypes.AuthAPI_ListMethods_Response;\n if (cl instanceof GrpcPlApiClient) {\n resp = (await cl.authMethods({})).response;\n } else {\n const wsResponse = notEmpty(\n (await cl.GET(\"/v1/auth/methods\")).data,\n \"REST: empty response for auth methods request\",\n );\n // OpenAPI schema flattens the protobuf oneof into `{ basic?, token? }`,\n // while protobuf-ts models it as a discriminated union. Reshape per item.\n resp = {\n methods: (wsResponse.methods ?? []).map((m): grpcTypes.AuthAPI_ListMethods_MethodInfo => {\n const base = { id: m.id, description: m.description };\n if (m.basic !== undefined) {\n return { ...base, method: { oneofKind: \"basic\", basic: m.basic } };\n }\n if (m.token !== undefined) {\n return { ...base, method: { oneofKind: \"token\", token: m.token } };\n }\n if (m.sso !== undefined) {\n return { ...base, method: { oneofKind: \"sso\", sso: m.sso } };\n }\n return { ...base, method: { oneofKind: undefined } };\n }),\n };\n }\n\n this._authMethodsSync = resp;\n return resp;\n }\n\n public get authMethodsSync(): grpcTypes.AuthAPI_ListMethods_Response {\n if (!this._authMethodsSync) {\n throw new Error(\"LLPlClient.authMethodsSync accessed before build() completed\");\n }\n return this._authMethodsSync;\n }\n\n public async getUserRoot(\n opts: { login?: string; createIfNotExists?: boolean } = {},\n ): Promise<grpcTypes.AuthAPI_GetUserRoot_Response> {\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.getUserRoot({\n login: opts.login ?? \"\",\n createIfNotExists: opts.createIfNotExists ?? false,\n })\n ).response;\n } else {\n const resp = notEmpty(\n (\n await cl.POST(\"/v1/auth/user-root\", {\n body: {\n login: opts.login ?? \"\",\n createIfNotExists: opts.createIfNotExists ?? false,\n },\n })\n ).data,\n \"REST: empty response for getUserRoot request\",\n );\n return {\n userRoot: resp.userRoot\n ? {\n resourceId: BigInt(resp.userRoot.resourceId),\n resourceSignature: Uint8Array.from(\n Buffer.from(resp.userRoot.resourceSignature, \"base64\"),\n ),\n }\n : undefined,\n };\n }\n }\n\n public async listUserResources(\n opts: { login?: string; startFrom?: bigint; limit?: number } = {},\n ): Promise<grpcTypes.AuthAPI_ListUserResources_Response[]> {\n const cl = this.clientProvider.get();\n\n if (!(cl instanceof GrpcPlApiClient)) {\n throw new Error(\"ListUserResources requires gRPC wire protocol; REST is not supported\");\n }\n\n const call = cl.listUserResources({\n login: opts.login ?? \"\",\n startFrom: opts.startFrom ?? 0n,\n limit: opts.limit ?? 0,\n });\n const responses: grpcTypes.AuthAPI_ListUserResources_Response[] = [];\n for await (const msg of call.responses) {\n responses.push(msg);\n }\n return responses;\n }\n\n public async txSync(txId: bigint): Promise<void> {\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n await cl.txSync({ txId: BigInt(txId) });\n } else {\n await cl.POST(\"/v1/tx-sync\", { body: { txId: txId.toString() } });\n }\n }\n\n createTx(rw: boolean, ops: PlCallOps = {}): LLPlTransaction {\n return new LLPlTransaction((abortSignal) => {\n let totalAbortSignal = abortSignal;\n if (ops.abortSignal) totalAbortSignal = AbortSignal.any([totalAbortSignal, ops.abortSignal]);\n\n const timeout =\n ops.timeout ??\n (rw ? this.conf.defaultRWTransactionTimeout : this.conf.defaultROTransactionTimeout);\n\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n return cl.tx({\n abort: totalAbortSignal,\n timeout,\n });\n }\n\n const wireConn = this.wireConnection;\n if (wireConn.type === \"rest\") {\n // For REST/WebSocket protocol, timeout needs to be converted to AbortSignal\n if (timeout !== undefined) {\n totalAbortSignal = AbortSignal.any([totalAbortSignal, AbortSignal.timeout(timeout)]);\n }\n\n // The gRPC transport has the auth interceptor that already handles it, but here we need to refresh the auth information to be safe.\n this.refreshAuthInformationIfNeeded();\n\n const wsUrl = this.conf.ssl\n ? `wss://${this.conf.hostAndPort}/v1/ws/tx`\n : `ws://${this.conf.hostAndPort}/v1/ws/tx`;\n\n return new WebSocketBiDiStream(\n wsUrl,\n (msg) => TxAPI_ClientMessage.toBinary(msg),\n (data) => TxAPI_ServerMessage.fromBinary(new Uint8Array(data)),\n {\n abortSignal: totalAbortSignal,\n jwtToken: this.authInformation?.jwtToken,\n dispatcher: wireConn.Dispatcher,\n\n onComplete: async (stream) =>\n stream.requests.send({\n // Ask server to gracefully close the stream on its side, if not done yet.\n requestId: 0,\n request: { oneofKind: \"streamClose\", streamClose: {} },\n }),\n },\n );\n }\n\n throw new Error(`transactions are not supported for wire protocol ${this._wireProto}`);\n });\n }\n\n /** Closes underlying transport */\n public async close() {\n if (this.wireConnection.type === \"grpc\") {\n this.wireConnection.Transport.close();\n } else {\n // TODO: close all WS connections\n }\n await this.httpDispatcher.destroy();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ll_client.js","names":["GrpcPlApiClient","status","GrpcStatus"],"sources":["../../src/core/ll_client.ts"],"sourcesContent":["import { PlatformClient as GrpcPlApiClient } from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client\";\nimport type { ClientOptions, Interceptor } from \"@grpc/grpc-js\";\nimport {\n ChannelCredentials,\n InterceptingCall,\n status as GrpcStatus,\n compressionAlgorithms,\n} from \"@grpc/grpc-js\";\nimport type {\n AuthInformation,\n AuthOps,\n PlClientConfig,\n PlConnectionStatus,\n PlConnectionStatusListener,\n} from \"./config\";\nimport { plAddressToConfig, type wireProtocol, SUPPORTED_WIRE_PROTOCOLS } from \"./config\";\nimport type { GrpcOptions } from \"@protobuf-ts/grpc-transport\";\nimport { GrpcTransport } from \"@protobuf-ts/grpc-transport\";\nimport { LLPlTransaction } from \"./ll_transaction\";\nimport { parsePlJwt } from \"../util/pl\";\nimport { type Dispatcher, interceptors } from \"undici\";\nimport type { Middleware } from \"openapi-fetch\";\nimport { inferAuthRefreshTime } from \"./auth\";\nimport { hasCapability, type BackendCapability } from \"./capabilities\";\nimport { defaultHttpDispatcher } from \"@milaboratories/pl-http\";\nimport type { WireClientProvider, WireClientProviderFactory, WireConnection } from \"./wire\";\nimport { parseHttpAuth } from \"@milaboratories/pl-model-common\";\nimport type * as grpcTypes from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport {\n type PlApiPaths,\n type PlRestClientType,\n createClient,\n parseResponseError,\n} from \"../proto-rest\";\nimport { notEmpty, retry, withTimeout, type RetryOptions } from \"@milaboratories/ts-helpers\";\nimport { Code } from \"../proto-grpc/google/rpc/code\";\nimport { WebSocketBiDiStream } from \"./websocket_stream\";\nimport {\n AuthAPI_Role,\n TxAPI_ClientMessage,\n TxAPI_ServerMessage,\n} from \"../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api\";\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport { isAbortedError } from \"./errors\";\n\nexport interface PlCallOps {\n timeout?: number;\n abortSignal?: AbortSignal;\n}\n\n// Parses leading \"<major>.<minor>.<patch>\" from a version string like\n// \"3.1.1\" or \"3.1.1-rc1\" and returns true if the parsed version is >= target.\n// Returns false for unparseable versions (safer to assume an old backend).\nfunction isVersionAtLeast(version: string, target: [number, number, number]): boolean {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)/.exec(version);\n if (!match) return false;\n const parsed: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];\n for (let i = 0; i < 3; i++) {\n if (parsed[i] !== target[i]) return parsed[i] > target[i];\n }\n return true;\n}\n\n// Returns true iff `version` is strictly after the release tag `target`. Dev\n// builds (`git describe`-style, e.g. \"3.5.0-224-g0ca182\") are considered\n// AFTER the matching release tag — they include commits past the tag and so\n// have any change merged after it. Released versions with the same triplet\n// return false (we want the tag itself to be excluded).\n//\n// Examples for target [3,5,0]:\n// \"3.5.0\" → false (the tagged release)\n// \"3.5.0-224-g0ca182\" → true (dev build past the tag)\n// \"3.5.1\" → true\n// \"3.4.9\" → false\n// Returns false for unparseable versions.\nfunction isAfterVersion(version: string, target: [number, number, number]): boolean {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(.*)$/.exec(version);\n if (!match) return false;\n const parsed: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];\n const suffix = match[4];\n for (let i = 0; i < 3; i++) {\n if (parsed[i] !== target[i]) return parsed[i] > target[i];\n }\n return suffix !== \"\";\n}\n\nclass WireClientProviderImpl<Client> implements WireClientProvider<Client> {\n private client: Client | undefined = undefined;\n\n constructor(\n private readonly wireOpts: () => WireConnection,\n private readonly clientConstructor: (wireOpts: WireConnection) => Client,\n ) {}\n\n public reset(): void {\n this.client = undefined;\n }\n\n public get(): Client {\n if (this.client === undefined) this.client = this.clientConstructor(this.wireOpts());\n return this.client;\n }\n}\n\n/** Abstract out low level networking and authorization details */\nexport class LLPlClient implements WireClientProviderFactory {\n /** Initial authorization information */\n private authInformation?: AuthInformation;\n /** Will be executed by the client when it is required */\n private readonly onAuthUpdate?: (newInfo: AuthInformation) => void;\n /** Will be executed if auth-related error happens during normal client operation */\n private readonly onAuthError?: () => void;\n /** Will be executed by the client when it is required */\n private readonly onAuthRefreshProblem?: (error: unknown) => void;\n /** Threshold after which auth info refresh is required */\n private refreshTimestamp?: number;\n\n /** Cached Ping response. Populated by build() before it returns; refreshed by every later ping(). */\n private _serverInfo?: grpcTypes.MaintenanceAPI_Ping_Response;\n private _authMethodsSync?: grpcTypes.AuthAPI_ListMethods_Response;\n\n private _status: PlConnectionStatus = \"OK\";\n private readonly statusListener?: PlConnectionStatusListener;\n\n private _wireProto: wireProtocol = \"grpc\";\n private _wireConn!: WireConnection;\n\n private readonly _restInterceptors: Dispatcher.DispatcherComposeInterceptor[];\n private readonly _restMiddlewares: Middleware[];\n private readonly _grpcInterceptors: Interceptor[];\n private readonly providers: WeakRef<WireClientProviderImpl<any>>[] = [];\n\n public readonly clientProvider: WireClientProvider<PlRestClientType | GrpcPlApiClient>;\n\n public readonly httpDispatcher: Dispatcher;\n\n public static async build(\n configOrAddress: PlClientConfig | string,\n ops: {\n auth?: AuthOps;\n statusListener?: PlConnectionStatusListener;\n shouldUseGzip?: boolean;\n logger?: MiLogger;\n useAutoDetectWireProtocol?: boolean;\n } = {},\n ) {\n const conf =\n typeof configOrAddress === \"string\" ? plAddressToConfig(configOrAddress) : configOrAddress;\n\n const pl = new LLPlClient(conf, ops);\n\n // FIXME(rfiskov)[MILAB-5275]: Investigate why autodetect randomly fails; temporary turn it off.\n if (ops.useAutoDetectWireProtocol) {\n await pl.detectOptimalWireProtocol();\n }\n\n // Guarantee a ping happened so capability-gated paths (login, refresh) can branch synchronously.\n // In the autodetect path the loop's last successful ping already populated _serverInfo via the\n // side-effect in ping(); this fallback covers the path where autodetect is disabled.\n if (!pl._serverInfo) await pl.ping();\n\n // Guarantee authMethods happened so client can make weighted decision on which auth method to use.\n if (!pl._authMethodsSync) await pl.authMethods();\n\n return pl;\n }\n\n private constructor(\n public readonly conf: PlClientConfig,\n private readonly ops: {\n auth?: AuthOps;\n statusListener?: PlConnectionStatusListener;\n shouldUseGzip?: boolean;\n logger?: MiLogger;\n } = {},\n ) {\n const { auth, statusListener } = ops;\n\n if (auth !== undefined) {\n this.refreshTimestamp = inferAuthRefreshTime(\n auth.authInformation,\n this.conf.authMaxRefreshSeconds,\n );\n this.authInformation = auth.authInformation;\n this.onAuthUpdate = auth.onUpdate;\n this.onAuthRefreshProblem = auth.onUpdateError;\n this.onAuthError = auth.onAuthError;\n }\n\n this._restInterceptors = [];\n this._restMiddlewares = [];\n this._grpcInterceptors = [];\n\n if (auth !== undefined) {\n this._restInterceptors.push(this.createRestAuthInterceptor());\n this._grpcInterceptors.push(this.createGrpcAuthInterceptor());\n }\n this._restInterceptors.push(interceptors.retry({ statusCodes: [] })); // Handle errors with openapi-fetch middleware.\n this._restMiddlewares.push(this.createRestErrorMiddleware());\n this._grpcInterceptors.push(this.createGrpcErrorInterceptor());\n\n this.httpDispatcher = defaultHttpDispatcher(this.conf.httpProxy);\n if (this.conf.wireProtocol) {\n this._wireProto = this.conf.wireProtocol;\n }\n\n this.initWireConnection(this._wireProto);\n\n if (statusListener !== undefined) {\n this.statusListener = statusListener;\n statusListener(this._status);\n }\n\n this.clientProvider = this.createWireClientProvider((wireConn) => {\n if (wireConn.type === \"grpc\") {\n return new GrpcPlApiClient(wireConn.Transport);\n } else {\n return createClient<PlApiPaths>({\n hostAndPort: wireConn.Config.hostAndPort,\n ssl: wireConn.Config.ssl,\n dispatcher: wireConn.Dispatcher,\n middlewares: wireConn.Middlewares,\n });\n }\n });\n }\n\n private initWireConnection(protocol: wireProtocol) {\n switch (protocol) {\n case \"rest\":\n this.initRestConnection();\n return;\n case \"grpc\":\n this.initGrpcConnection(this.ops.shouldUseGzip ?? false);\n return;\n default:\n ((v: never) => {\n throw new Error(\n `Unsupported wire protocol '${v as string}'. Use one of: ${SUPPORTED_WIRE_PROTOCOLS.join(\", \")}`,\n );\n })(protocol);\n }\n }\n\n private initRestConnection(): void {\n const dispatcher = defaultHttpDispatcher(this.conf.grpcProxy, this._restInterceptors);\n this._replaceWireConnection({\n type: \"rest\",\n Config: this.conf,\n Dispatcher: dispatcher,\n Middlewares: this._restMiddlewares,\n });\n }\n\n /**\n * Initializes (or reinitializes) _grpcTransport\n * @param gzip - whether to enable gzip compression\n */\n private initGrpcConnection(gzip: boolean) {\n const clientOptions: ClientOptions = {\n \"grpc.keepalive_time_ms\": 30_000, // 30 seconds\n \"grpc.service_config_disable_resolution\": 1, // Disable DNS TXT lookups for service config\n interceptors: this._grpcInterceptors,\n };\n\n if (gzip) clientOptions[\"grpc.default_compression_algorithm\"] = compressionAlgorithms.gzip;\n\n //\n // Leaving it here for now\n // https://github.com/grpc/grpc-node/issues/2788\n //\n // We should implement message pooling algorithm to overcome hardcoded NO_DELAY behaviour\n // of HTTP/2 and allow our small messages to batch together.\n //\n const grpcOptions: GrpcOptions = {\n host: this.conf.hostAndPort,\n timeout: this.conf.defaultRequestTimeout,\n channelCredentials: this.conf.ssl\n ? ChannelCredentials.createSsl()\n : ChannelCredentials.createInsecure(),\n clientOptions,\n };\n\n const grpcProxy =\n typeof this.conf.grpcProxy === \"string\" ? { url: this.conf.grpcProxy } : this.conf.grpcProxy;\n\n if (grpcProxy?.url) {\n const url = new URL(grpcProxy.url);\n if (grpcProxy.auth) {\n const parsed = parseHttpAuth(grpcProxy.auth);\n if (parsed.scheme !== \"Basic\") {\n throw new Error(`Unsupported auth scheme: ${parsed.scheme as string}.`);\n }\n url.username = parsed.username;\n url.password = parsed.password;\n }\n process.env.grpc_proxy = url.toString();\n } else {\n delete process.env.grpc_proxy;\n }\n\n this._replaceWireConnection({ type: \"grpc\", Transport: new GrpcTransport(grpcOptions) });\n }\n\n private _replaceWireConnection(newConn: WireConnection): void {\n const oldConn = this._wireConn;\n this._wireConn = newConn;\n this._wireProto = newConn.type;\n\n // Reset all providers to let them reinitialize their clients\n for (let i = 0; i < this.providers.length; i++) {\n const provider = this.providers[i].deref();\n if (provider === undefined) {\n // at the same time we need to remove providers that are no longer valid\n this.providers.splice(i, 1);\n i--;\n } else {\n provider.reset();\n }\n }\n\n if (oldConn !== undefined && oldConn.type === \"grpc\") oldConn.Transport.close();\n }\n\n private providerCleanupCounter = 0;\n\n /**\n * Creates a provider for a grpc client. Returned provider will create fresh client whenever the underlying transport is reset.\n *\n * @param clientConstructor - a factory function that creates a grpc client\n */\n public createWireClientProvider<Client>(\n clientConstructor: (transport: WireConnection) => Client,\n ): WireClientProvider<Client> {\n // We need to cleanup providers periodically to avoid memory leaks.\n // This is a simple heuristic to avoid memory leaks.\n // We could use a more sophisticated algorithm, but this is good enough for now.\n this.providerCleanupCounter++;\n if (this.providerCleanupCounter >= 16) {\n for (let i = 0; i < this.providers.length; i++) {\n const provider = this.providers[i].deref();\n if (provider === undefined) {\n this.providers.splice(i, 1);\n i--;\n }\n }\n this.providerCleanupCounter = 0;\n }\n\n const provider = new WireClientProviderImpl<Client>(() => this._wireConn, clientConstructor);\n this.providers.push(new WeakRef(provider));\n return provider;\n }\n\n public get wireConnection(): WireConnection {\n return this._wireConn;\n }\n\n public get wireProtocol(): wireProtocol | undefined {\n return this._wireProto;\n }\n\n /** Returns true if client is authenticated. Even with anonymous auth information\n * connection is considered authenticated. Unauthenticated clients are used for\n * login and similar tasks, see {@link UnauthenticatedPlClient}. */\n public get authenticated(): boolean {\n return this.authInformation !== undefined;\n }\n\n /** null means anonymous connection */\n public get authUser(): string | null {\n if (!this.authenticated) throw new Error(\"Client is not authenticated\");\n if (this.authInformation?.jwtToken) {\n if (this.hasCapability(\"auth:v2\")) {\n return parsePlJwt(this.authInformation?.jwtToken).sub;\n }\n return parsePlJwt(this.authInformation?.jwtToken).user.login;\n } else return null;\n }\n\n private updateStatus(newStatus: PlConnectionStatus) {\n process.nextTick(() => {\n if (this._status !== newStatus) {\n this._status = newStatus;\n if (this.statusListener !== undefined) this.statusListener(this._status);\n if (newStatus === \"Unauthenticated\" && this.onAuthError !== undefined) this.onAuthError();\n }\n });\n }\n\n public get status(): PlConnectionStatus {\n return this._status;\n }\n\n private authRefreshInProgress: boolean = false;\n\n private refreshAuthInformationIfNeeded(): void {\n if (\n this.refreshTimestamp === undefined ||\n Date.now() < this.refreshTimestamp ||\n this.authRefreshInProgress ||\n this._status === \"Unauthenticated\"\n )\n return;\n\n // Running refresh in background`\n this.authRefreshInProgress = true;\n void (async () => {\n try {\n const ttl = BigInt(this.conf.authTTLSeconds);\n const token = this.hasCapability(\"auth:v2\")\n ? await this.refreshToken({ ttlSeconds: ttl })\n : await this.getJwtToken(ttl);\n this.authInformation = { jwtToken: token };\n this.refreshTimestamp = inferAuthRefreshTime(\n this.authInformation,\n this.conf.authMaxRefreshSeconds,\n );\n if (this.onAuthUpdate) this.onAuthUpdate(this.authInformation);\n } catch (e: unknown) {\n if (this.onAuthRefreshProblem) this.onAuthRefreshProblem(e);\n } finally {\n this.authRefreshInProgress = false;\n }\n })();\n }\n\n /**\n * Creates middleware that parses error responses and handles them centrally.\n * This middleware runs before openapi-fetch parses the response, so we need to\n * manually parse the response body for error responses.\n */\n private createRestErrorMiddleware(): Middleware {\n return {\n onResponse: async ({ request: _request, response, options: _options }) => {\n const { body, ...resOptions } = response;\n\n if ([502, 503, 504].includes(response.status)) {\n // Service unavailable, bad gateway, gateway timeout\n this.updateStatus(\"Disconnected\");\n return new Response(body, { ...resOptions, status: response.status });\n }\n\n const respErr = await parseResponseError(response);\n if (!respErr.error) {\n // No error: nice!\n return new Response(respErr.origBody ?? body, { ...resOptions, status: response.status });\n }\n\n if (typeof respErr.error === \"string\") {\n // Non-standard error or normal response: let later middleware to deal wit it.\n return new Response(respErr.error, { ...resOptions, status: response.status });\n }\n\n if (respErr.error.code === Code.UNAUTHENTICATED) {\n this.updateStatus(\"Unauthenticated\");\n }\n\n // Let later middleware to deal with standard gRPC error.\n return new Response(respErr.origBody, { ...resOptions, status: response.status });\n },\n };\n }\n\n /** Detects certain errors and update client status accordingly when using GRPC wire connection */\n private createGrpcErrorInterceptor(): Interceptor {\n return (options, nextCall) => {\n return new InterceptingCall(nextCall(options), {\n start: (metadata, listener, next) => {\n next(metadata, {\n onReceiveStatus: (status, next) => {\n if (status.code == GrpcStatus.UNAUTHENTICATED)\n // (!!!) don't change to \"===\"\n this.updateStatus(\"Unauthenticated\");\n if (status.code == GrpcStatus.UNAVAILABLE)\n // (!!!) don't change to \"===\"\n this.updateStatus(\"Disconnected\");\n next(status);\n },\n });\n },\n });\n };\n }\n\n private createRestAuthInterceptor(): Dispatcher.DispatcherComposeInterceptor {\n return (dispatch) => {\n return (options, handler) => {\n if (this.authInformation?.jwtToken !== undefined) {\n // TODO: check this magic really works and gets called\n options.headers = {\n ...options.headers,\n authorization: \"Bearer \" + this.authInformation.jwtToken,\n };\n this.refreshAuthInformationIfNeeded();\n }\n\n return dispatch(options, handler);\n };\n };\n }\n\n /** Injects authentication information if needed */\n private createGrpcAuthInterceptor(): Interceptor {\n return (options, nextCall) => {\n return new InterceptingCall(nextCall(options), {\n start: (metadata, listener, next) => {\n if (this.authInformation?.jwtToken !== undefined) {\n metadata.set(\"authorization\", \"Bearer \" + this.authInformation.jwtToken);\n this.refreshAuthInformationIfNeeded();\n next(metadata, listener);\n } else {\n next(metadata, listener);\n }\n },\n });\n };\n }\n\n public async getJwtToken(\n ttlSeconds: bigint,\n options?: { authorization?: string; role?: AuthAPI_Role },\n ): Promise<string> {\n const cl = this.clientProvider.get();\n const role = options?.role ?? AuthAPI_Role.UNSPECIFIED;\n\n if (cl instanceof GrpcPlApiClient) {\n const meta: Record<string, string> = {};\n if (options?.authorization) meta.authorization = options.authorization;\n return (\n await cl.getJWTToken(\n {\n expiration: { seconds: ttlSeconds, nanos: 0 },\n requestedRole: role,\n },\n { meta },\n ).response\n ).token;\n } else {\n const headers: Record<string, string> = {};\n if (options?.authorization) headers.authorization = options.authorization;\n const resp = cl.POST(\"/v1/auth/jwt-token\", {\n body: { expiration: `${ttlSeconds}s`, requestedRole: role },\n headers,\n });\n return notEmpty((await resp).data, \"REST: empty response for JWT token request\").token;\n }\n }\n\n /** Login via username/password. Returns a fresh JWT. Backend creates a new session per call. */\n public async loginBasic(\n user: string,\n password: string,\n opts: { ttlSeconds?: bigint; role?: AuthAPI_Role } = {},\n ): Promise<string> {\n const cl = this.clientProvider.get();\n const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);\n const role = opts.role ?? AuthAPI_Role.UNSPECIFIED;\n\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.login({\n credentials: {\n oneofKind: \"basic\",\n basic: { login: user, password },\n },\n expiration: { seconds: ttl, nanos: 0 },\n requestedRole: role,\n }).response\n ).token;\n } else {\n const resp = cl.POST(\"/v1/auth/login\", {\n // openapi-typescript generated all body fields as required, but Login.Request\n // has a credentials oneof — only one of `basic`/`token` is sent. Cast around it.\n body: {\n basic: { login: user, password },\n expiration: `${ttl}s`,\n requestedRole: role,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any,\n });\n return notEmpty((await resp).data, \"REST: empty response for login request\").token;\n }\n }\n\n /** Login via opaque bearer token (controller pre-shared secret, OIDC id-token, etc.).\n * String input is UTF-8 encoded. Returns a fresh Platforma JWT. */\n public async loginWithToken(\n token: Uint8Array | string,\n opts: { ttlSeconds?: bigint; role?: AuthAPI_Role } = {},\n ): Promise<string> {\n const cl = this.clientProvider.get();\n const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);\n const role = opts.role ?? AuthAPI_Role.UNSPECIFIED;\n const bytes = typeof token === \"string\" ? Buffer.from(token, \"utf8\") : token;\n\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.login({\n credentials: {\n oneofKind: \"token\",\n token: { token: bytes },\n },\n expiration: { seconds: ttl, nanos: 0 },\n requestedRole: role,\n }).response\n ).token;\n } else {\n const resp = cl.POST(\"/v1/auth/login\", {\n // openapi-typescript marks all body fields as required, but Login.Request has a oneof.\n // REST encodes `bytes` as a base64 string.\n body: {\n token: { token: Buffer.from(bytes).toString(\"base64\") },\n expiration: `${ttl}s`,\n requestedRole: role,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any,\n });\n return notEmpty((await resp).data, \"REST: empty response for login request\").token;\n }\n }\n\n /** Refresh the current JWT, preserving session id and role. */\n public async refreshToken(opts: { ttlSeconds?: bigint } = {}): Promise<string> {\n const cl = this.clientProvider.get();\n const ttl = opts.ttlSeconds ?? BigInt(this.conf.authTTLSeconds);\n const currentToken = notEmpty(\n this.authInformation?.jwtToken,\n \"refreshToken called without a current JWT\",\n );\n\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.refreshToken({\n token: currentToken,\n expiration: { seconds: ttl, nanos: 0 },\n }).response\n ).token;\n } else {\n const resp = cl.POST(\"/v1/auth/refresh\", {\n body: { token: currentToken, expiration: `${ttl}s` },\n });\n return notEmpty((await resp).data, \"REST: empty response for refresh request\").token;\n }\n }\n\n public async ping(): Promise<grpcTypes.MaintenanceAPI_Ping_Response> {\n const cl = this.clientProvider.get();\n let resp: grpcTypes.MaintenanceAPI_Ping_Response;\n if (cl instanceof GrpcPlApiClient) {\n resp = (await cl.ping({})).response;\n } else {\n // The REST ping response predates the `capabilities` field (proto field 9).\n // Old servers omit it; treat absence as empty capability list.\n const pingData = notEmpty(\n (await cl.GET(\"/v1/ping\")).data,\n \"REST: empty response for ping request\",\n );\n resp = {\n ...(pingData as unknown as grpcTypes.MaintenanceAPI_Ping_Response),\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n capabilities: (pingData as any).capabilities ?? [],\n };\n }\n this._serverInfo = resp;\n return resp;\n }\n\n /** Cached Ping response. Always populated post-build(); throws if accessed earlier. */\n public get serverInfo(): grpcTypes.MaintenanceAPI_Ping_Response {\n if (!this._serverInfo) {\n throw new Error(\"LLPlClient.serverInfo accessed before build() completed\");\n }\n return this._serverInfo;\n }\n\n /** Synchronous capability check against the cached Ping response. */\n public hasCapability(capability: BackendCapability): boolean {\n return hasCapability(this.serverInfo.capabilities, capability);\n }\n\n /** True if the backend implements the setDefaultColor TX request. */\n public get supportsSetDefaultColor(): boolean {\n return isVersionAtLeast(this.serverInfo.coreVersion, [3, 3, 0]);\n }\n\n /**\n * True if the backend honors per-file `permissions` on workdir fill rules\n * (PR #1830 in milaboratory/pl). Backends before this change ignore the\n * requested mode and always land files at the canonical archive perm,\n * making `exec.builder().writeFile/addFile({ writable: true })` a no-op.\n *\n * Tagged at 3.5.0 cut without the change, so [3, 5, 0] excludes the tagged\n * release but includes dev builds past the tag (e.g. \"3.5.0-224-g0ca182\").\n */\n public get supportsWritableWorkdirFiles(): boolean {\n return isAfterVersion(this.serverInfo.coreVersion, [3, 5, 0]);\n }\n\n /**\n * Detects the best available wire protocol.\n * If wireProtocol is explicitly configured, does nothing.\n * Otherwise probes the current protocol via ping; if it fails, switches to the alternative.\n */\n private async detectOptimalWireProtocol() {\n if (this.conf.wireProtocol) {\n return;\n }\n\n // Each retry is:\n // - ping request timeout (100 to 3_000ms)\n // - backoff delay (30 to 500ms)\n //\n // 30 attempts are ~43 seconds of overall waiting time.\n // Think twice on overall time this thing takes to complete when changing these parameters.\n // It may block UI when connecting to the server and loading projects list.\n const pingTimeoutFactor = 1.3;\n const maxPingTimeoutMs = 3_000;\n const retryOptions: RetryOptions = {\n type: \"exponentialBackoff\",\n maxAttempts: 30,\n initialDelay: 30,\n backoffMultiplier: 1.3,\n jitter: 0.2,\n maxDelay: 500,\n };\n\n let attempt = 1;\n let pingTimeoutMs = 100;\n await retry(\n () => withTimeout(this.ping(), pingTimeoutMs),\n retryOptions,\n (e: unknown) => {\n if (isAbortedError(e)) {\n this.ops.logger?.info(\n `Wire proto autodetect: ping timed out after ${pingTimeoutMs}ms: attempt=${attempt}, wire=${this._wireProto}`,\n );\n\n if (attempt % 2 === 0) {\n // We have 2 wire protocols to check. Increase timeout each 2 attempts.\n pingTimeoutMs = Math.min(\n Math.round(pingTimeoutMs * pingTimeoutFactor),\n maxPingTimeoutMs,\n );\n }\n } else {\n this.ops.logger?.info(\n `Wire proto autodetect: ping failed: attempt=${attempt}, wire=${this._wireProto}, err=${String(e)}`,\n );\n }\n\n attempt++;\n const protocol = this._wireProto === \"grpc\" ? \"rest\" : \"grpc\";\n this.ops.logger?.info(\n `Wire protocol autodetect next attempt: will try wire '${protocol}' with timeout ${pingTimeoutMs}ms`,\n );\n this.initWireConnection(protocol);\n return true;\n },\n );\n }\n\n public async license(): Promise<grpcTypes.MaintenanceAPI_License_Response> {\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n return (await cl.license({})).response;\n } else {\n const resp = notEmpty(\n (await cl.GET(\"/v1/license\")).data,\n \"REST: empty response for license request\",\n );\n return {\n status: resp.status,\n isOk: resp.isOk,\n responseBody: Uint8Array.from(Buffer.from(resp.responseBody)),\n };\n }\n }\n\n public async authMethods(): Promise<grpcTypes.AuthAPI_ListMethods_Response> {\n const cl = this.clientProvider.get();\n let resp: grpcTypes.AuthAPI_ListMethods_Response;\n if (cl instanceof GrpcPlApiClient) {\n resp = (await cl.authMethods({})).response;\n } else {\n const wsResponse = notEmpty(\n (await cl.GET(\"/v1/auth/methods\")).data,\n \"REST: empty response for auth methods request\",\n );\n // OpenAPI schema flattens the protobuf oneof into `{ basic?, token? }`,\n // while protobuf-ts models it as a discriminated union. Reshape per item.\n resp = {\n methods: (wsResponse.methods ?? []).map((m): grpcTypes.AuthAPI_ListMethods_MethodInfo => {\n const base = { id: m.id, description: m.description };\n if (m.basic !== undefined) {\n return { ...base, method: { oneofKind: \"basic\", basic: m.basic } };\n }\n if (m.token !== undefined) {\n return { ...base, method: { oneofKind: \"token\", token: m.token } };\n }\n if (m.sso !== undefined) {\n return { ...base, method: { oneofKind: \"sso\", sso: m.sso } };\n }\n return { ...base, method: { oneofKind: undefined } };\n }),\n };\n }\n\n this._authMethodsSync = resp;\n return resp;\n }\n\n public get authMethodsSync(): grpcTypes.AuthAPI_ListMethods_Response {\n if (!this._authMethodsSync) {\n throw new Error(\"LLPlClient.authMethodsSync accessed before build() completed\");\n }\n return this._authMethodsSync;\n }\n\n public async getUserRoot(\n opts: { login?: string; createIfNotExists?: boolean } = {},\n ): Promise<grpcTypes.AuthAPI_GetUserRoot_Response> {\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n return (\n await cl.getUserRoot({\n login: opts.login ?? \"\",\n createIfNotExists: opts.createIfNotExists ?? false,\n })\n ).response;\n } else {\n const resp = notEmpty(\n (\n await cl.POST(\"/v1/auth/user-root\", {\n body: {\n login: opts.login ?? \"\",\n createIfNotExists: opts.createIfNotExists ?? false,\n },\n })\n ).data,\n \"REST: empty response for getUserRoot request\",\n );\n return {\n userRoot: resp.userRoot\n ? {\n resourceId: BigInt(resp.userRoot.resourceId),\n resourceSignature: Uint8Array.from(\n Buffer.from(resp.userRoot.resourceSignature, \"base64\"),\n ),\n }\n : undefined,\n };\n }\n }\n\n public async listUserResources(\n opts: { login?: string; startFrom?: bigint; limit?: number } = {},\n ): Promise<grpcTypes.AuthAPI_ListUserResources_Response[]> {\n const cl = this.clientProvider.get();\n\n if (!(cl instanceof GrpcPlApiClient)) {\n throw new Error(\"ListUserResources requires gRPC wire protocol; REST is not supported\");\n }\n\n const call = cl.listUserResources({\n login: opts.login ?? \"\",\n startFrom: opts.startFrom ?? 0n,\n limit: opts.limit ?? 0,\n });\n const responses: grpcTypes.AuthAPI_ListUserResources_Response[] = [];\n for await (const msg of call.responses) {\n responses.push(msg);\n }\n return responses;\n }\n\n public async txSync(txId: bigint): Promise<void> {\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n await cl.txSync({ txId: BigInt(txId) });\n } else {\n await cl.POST(\"/v1/tx-sync\", { body: { txId: txId.toString() } });\n }\n }\n\n createTx(rw: boolean, ops: PlCallOps = {}): LLPlTransaction {\n return new LLPlTransaction((abortSignal) => {\n let totalAbortSignal = abortSignal;\n if (ops.abortSignal) totalAbortSignal = AbortSignal.any([totalAbortSignal, ops.abortSignal]);\n\n const timeout =\n ops.timeout ??\n (rw ? this.conf.defaultRWTransactionTimeout : this.conf.defaultROTransactionTimeout);\n\n const cl = this.clientProvider.get();\n if (cl instanceof GrpcPlApiClient) {\n return cl.tx({\n abort: totalAbortSignal,\n timeout,\n });\n }\n\n const wireConn = this.wireConnection;\n if (wireConn.type === \"rest\") {\n // For REST/WebSocket protocol, timeout needs to be converted to AbortSignal\n if (timeout !== undefined) {\n totalAbortSignal = AbortSignal.any([totalAbortSignal, AbortSignal.timeout(timeout)]);\n }\n\n // The gRPC transport has the auth interceptor that already handles it, but here we need to refresh the auth information to be safe.\n this.refreshAuthInformationIfNeeded();\n\n const wsUrl = this.conf.ssl\n ? `wss://${this.conf.hostAndPort}/v1/ws/tx`\n : `ws://${this.conf.hostAndPort}/v1/ws/tx`;\n\n return new WebSocketBiDiStream(\n wsUrl,\n (msg) => TxAPI_ClientMessage.toBinary(msg),\n (data) => TxAPI_ServerMessage.fromBinary(new Uint8Array(data)),\n {\n abortSignal: totalAbortSignal,\n jwtToken: this.authInformation?.jwtToken,\n dispatcher: wireConn.Dispatcher,\n\n onComplete: async (stream) =>\n stream.requests.send({\n // Ask server to gracefully close the stream on its side, if not done yet.\n requestId: 0,\n request: { oneofKind: \"streamClose\", streamClose: {} },\n }),\n },\n );\n }\n\n throw new Error(`transactions are not supported for wire protocol ${this._wireProto}`);\n });\n }\n\n /** Closes underlying transport */\n public async close() {\n if (this.wireConnection.type === \"grpc\") {\n this.wireConnection.Transport.close();\n } else {\n // TODO: close all WS connections\n }\n await this.httpDispatcher.destroy();\n }\n}\n"],"mappings":"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"}
@@ -43,12 +43,13 @@ function plAddressToTestConfig(address) {
43
43
  plConf.defaultRequestTimeout = 500;
44
44
  return plConf;
45
45
  }
46
- function saveAuthInfoCallback(tConf) {
46
+ function saveAuthInfoCallback(tConf, instanceId) {
47
47
  return (authInformation) => {
48
48
  const dst = getFullAuthDataFilePath();
49
49
  const tmpDst = getFullAuthDataFilePath() + (0, node_crypto.randomUUID)();
50
50
  node_fs.writeFileSync(tmpDst, Buffer.from(JSON.stringify({
51
51
  conf: tConf,
52
+ instanceId,
52
53
  authInformation,
53
54
  expiration: require_auth.inferAuthRefreshTime(authInformation, 1440 * 60)
54
55
  })), "utf8");
@@ -64,28 +65,29 @@ const cleanAuthInfoCallback = () => {
64
65
  };
65
66
  async function getTestClientConf() {
66
67
  const tConf = getTestConfig();
68
+ const plConf = plAddressToTestConfig(tConf.address);
69
+ const uClient = await require_unauth_client.UnauthenticatedPlClient.build(plConf);
70
+ const instanceId = uClient.ll.serverInfo.instanceId;
67
71
  let authInformation = void 0;
68
72
  if (node_fs.existsSync(getFullAuthDataFilePath())) try {
69
73
  const cache = JSON.parse(node_fs.readFileSync(getFullAuthDataFilePath(), { encoding: "utf-8" }));
70
- if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now()) authInformation = cache.authInformation;
74
+ if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now() && cache.instanceId === instanceId) authInformation = cache.authInformation;
71
75
  } catch {
72
76
  node_fs.rmSync(getFullAuthDataFilePath());
73
77
  }
74
- const plConf = plAddressToTestConfig(tConf.address);
75
- const uClient = await require_unauth_client.UnauthenticatedPlClient.build(plConf);
76
78
  const requireAuth = await uClient.requireAuth();
77
79
  if (!requireAuth && (tConf.test_user !== void 0 || tConf.test_password !== void 0)) throw new Error(`Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
78
80
  if (requireAuth && (tConf.test_user === void 0 || tConf.test_password === void 0)) throw new Error(`No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
79
81
  if (authInformation === void 0) {
80
82
  if (requireAuth) authInformation = await uClient.login(tConf.test_user, tConf.test_password);
81
83
  else authInformation = {};
82
- saveAuthInfoCallback(tConf)(authInformation);
84
+ saveAuthInfoCallback(tConf, instanceId)(authInformation);
83
85
  }
84
86
  return {
85
87
  conf: plConf,
86
88
  auth: {
87
89
  authInformation,
88
- onUpdate: saveAuthInfoCallback(tConf),
90
+ onUpdate: saveAuthInfoCallback(tConf, instanceId),
89
91
  onAuthError: cleanAuthInfoCallback,
90
92
  onUpdateError: cleanAuthInfoCallback
91
93
  }
@@ -1 +1 @@
1
- {"version":3,"file":"test_config.cjs","names":["path","fs","plAddressToConfig","inferAuthRefreshTime","UnauthenticatedPlClient","LLPlClient","PlClient","startTcpProxy","resourceIdToString"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(tConf: TestConfig): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // try recover from cache\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now()\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"test_config.cjs","names":["path","fs","plAddressToConfig","inferAuthRefreshTime","UnauthenticatedPlClient","LLPlClient","PlClient","startTcpProxy","resourceIdToString"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"test_config.d.ts","names":[],"sources":["../../src/test/test_config.ts"],"mappings":";;;;;;;;;UAgBiB,UAAA;EACf,OAAA;EACA,UAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,iBAac,aAAA,CAAA,GAAiB,UAAA;;cAsBpB,oBAAA;;iBAGG,qBAAA,CAAsB,OAAA,WAAkB,cAAA;AAAA,iBAwClC,iBAAA,CAAA,GAAqB,OAAA;EAAU,IAAA,EAAM,cAAA;EAAgB,IAAA,EAAM,OAAA;AAAA;AAAA,iBA2D3D,eAAA,CAAgB,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,UAAA;AAAA,iBAK3D,aAAA,CACpB,eAAA,WACA,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,QAAA;AAAA,KAQjC,mBAAA;EA1JV;;EA8JI,WAAA,QA5JS;EA8JT,cAAA;AAAA;EAGA,WAAA;AAAA;AAAA,iBAGgB,YAAA,GAAA,CAAgB,IAAA,GAAO,EAAA,EAAI,QAAA,KAAa,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;AAAA,iBAE7D,YAAA,GAAA,CACpB,IAAA,GAAO,EAAA,EAAI,QAAA,EAAU,KAAA,EAAO,OAAA,CAAQ,UAAA,QAAkB,aAAA,OAAoB,OAAA,CAAQ,CAAA,GAClF,OAAA;EACE,WAAA;EACA,cAAA;AAAA,IAED,OAAA,CAAQ,CAAA"}
1
+ {"version":3,"file":"test_config.d.ts","names":[],"sources":["../../src/test/test_config.ts"],"mappings":";;;;;;;;;UAgBiB,UAAA;EACf,OAAA;EACA,UAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,iBAac,aAAA,CAAA,GAAiB,UAAA;;cAsBpB,oBAAA;;iBAGG,qBAAA,CAAsB,OAAA,WAAkB,cAAA;AAAA,iBAkDlC,iBAAA,CAAA,GAAqB,OAAA;EAAU,IAAA,EAAM,cAAA;EAAgB,IAAA,EAAM,OAAA;AAAA;AAAA,iBAmE3D,eAAA,CAAgB,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,UAAA;AAAA,iBAK3D,aAAA,CACpB,eAAA,WACA,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,QAAA;AAAA,KAQjC,mBAAA;EA5KV;;EAgLI,WAAA,QA9KS;EAgLT,cAAA;AAAA;EAGA,WAAA;AAAA;AAAA,iBAGgB,YAAA,GAAA,CAAgB,IAAA,GAAO,EAAA,EAAI,QAAA,KAAa,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;AAAA,iBAE7D,YAAA,GAAA,CACpB,IAAA,GAAO,EAAA,EAAI,QAAA,EAAU,KAAA,EAAO,OAAA,CAAQ,UAAA,QAAkB,aAAA,OAAoB,OAAA,CAAQ,CAAA,GAClF,OAAA;EACE,WAAA;EACA,cAAA;AAAA,IAED,OAAA,CAAQ,CAAA"}
@@ -41,12 +41,13 @@ function plAddressToTestConfig(address) {
41
41
  plConf.defaultRequestTimeout = 500;
42
42
  return plConf;
43
43
  }
44
- function saveAuthInfoCallback(tConf) {
44
+ function saveAuthInfoCallback(tConf, instanceId) {
45
45
  return (authInformation) => {
46
46
  const dst = getFullAuthDataFilePath();
47
47
  const tmpDst = getFullAuthDataFilePath() + randomUUID();
48
48
  fs$1.writeFileSync(tmpDst, Buffer.from(JSON.stringify({
49
49
  conf: tConf,
50
+ instanceId,
50
51
  authInformation,
51
52
  expiration: inferAuthRefreshTime(authInformation, 1440 * 60)
52
53
  })), "utf8");
@@ -62,28 +63,29 @@ const cleanAuthInfoCallback = () => {
62
63
  };
63
64
  async function getTestClientConf() {
64
65
  const tConf = getTestConfig();
66
+ const plConf = plAddressToTestConfig(tConf.address);
67
+ const uClient = await UnauthenticatedPlClient.build(plConf);
68
+ const instanceId = uClient.ll.serverInfo.instanceId;
65
69
  let authInformation = void 0;
66
70
  if (fs$1.existsSync(getFullAuthDataFilePath())) try {
67
71
  const cache = JSON.parse(fs$1.readFileSync(getFullAuthDataFilePath(), { encoding: "utf-8" }));
68
- if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now()) authInformation = cache.authInformation;
72
+ if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now() && cache.instanceId === instanceId) authInformation = cache.authInformation;
69
73
  } catch {
70
74
  fs$1.rmSync(getFullAuthDataFilePath());
71
75
  }
72
- const plConf = plAddressToTestConfig(tConf.address);
73
- const uClient = await UnauthenticatedPlClient.build(plConf);
74
76
  const requireAuth = await uClient.requireAuth();
75
77
  if (!requireAuth && (tConf.test_user !== void 0 || tConf.test_password !== void 0)) throw new Error(`Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
76
78
  if (requireAuth && (tConf.test_user === void 0 || tConf.test_password === void 0)) throw new Error(`No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
77
79
  if (authInformation === void 0) {
78
80
  if (requireAuth) authInformation = await uClient.login(tConf.test_user, tConf.test_password);
79
81
  else authInformation = {};
80
- saveAuthInfoCallback(tConf)(authInformation);
82
+ saveAuthInfoCallback(tConf, instanceId)(authInformation);
81
83
  }
82
84
  return {
83
85
  conf: plConf,
84
86
  auth: {
85
87
  authInformation,
86
- onUpdate: saveAuthInfoCallback(tConf),
88
+ onUpdate: saveAuthInfoCallback(tConf, instanceId),
87
89
  onAuthError: cleanAuthInfoCallback,
88
90
  onUpdateError: cleanAuthInfoCallback
89
91
  }
@@ -1 +1 @@
1
- {"version":3,"file":"test_config.js","names":["fs"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(tConf: TestConfig): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // try recover from cache\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now()\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"test_config.js","names":["fs"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-client",
3
- "version": "3.8.1",
3
+ "version": "3.9.1",
4
4
  "description": "New TS/JS client for Platform API",
5
5
  "files": [
6
6
  "./dist/**/*",
@@ -30,9 +30,9 @@
30
30
  "undici": "~7.16.0",
31
31
  "utility-types": "^3.11.0",
32
32
  "yaml": "^2.8.0",
33
- "@milaboratories/ts-helpers": "1.8.2",
34
33
  "@milaboratories/pl-http": "1.2.4",
35
- "@milaboratories/pl-model-common": "1.42.0"
34
+ "@milaboratories/pl-model-common": "1.42.0",
35
+ "@milaboratories/ts-helpers": "1.8.2"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@protobuf-ts/plugin": "2.11.1",
@@ -41,9 +41,9 @@
41
41
  "openapi-typescript": "^7.10.0",
42
42
  "typescript": "~5.9.3",
43
43
  "vitest": "^4.1.3",
44
- "@milaboratories/build-configs": "2.0.0",
45
44
  "@milaboratories/ts-configs": "1.2.3",
46
- "@milaboratories/ts-builder": "1.5.0"
45
+ "@milaboratories/ts-builder": "1.5.0",
46
+ "@milaboratories/build-configs": "2.0.0"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=22.19.0"
@@ -232,6 +232,16 @@ export class PlClient {
232
232
  return this._ll!.hasCapability(capability);
233
233
  }
234
234
 
235
+ /**
236
+ * True if the backend honors per-file `permissions` on workdir fill rules
237
+ * (PR #1830 in milaboratory/pl). See `LLPlClient.supportsWritableWorkdirFiles`
238
+ * for the full definition.
239
+ */
240
+ public get supportsWritableWorkdirFiles(): boolean {
241
+ this.checkInitialized();
242
+ return this._ll!.supportsWritableWorkdirFiles;
243
+ }
244
+
235
245
  /** User resources index for discovering data libraries and other shared resources. */
236
246
  public get userResources(): UserResources {
237
247
  if (!this._ll) throw new Error("Client not initialized");
@@ -61,6 +61,29 @@ function isVersionAtLeast(version: string, target: [number, number, number]): bo
61
61
  return true;
62
62
  }
63
63
 
64
+ // Returns true iff `version` is strictly after the release tag `target`. Dev
65
+ // builds (`git describe`-style, e.g. "3.5.0-224-g0ca182") are considered
66
+ // AFTER the matching release tag — they include commits past the tag and so
67
+ // have any change merged after it. Released versions with the same triplet
68
+ // return false (we want the tag itself to be excluded).
69
+ //
70
+ // Examples for target [3,5,0]:
71
+ // "3.5.0" → false (the tagged release)
72
+ // "3.5.0-224-g0ca182" → true (dev build past the tag)
73
+ // "3.5.1" → true
74
+ // "3.4.9" → false
75
+ // Returns false for unparseable versions.
76
+ function isAfterVersion(version: string, target: [number, number, number]): boolean {
77
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(.*)$/.exec(version);
78
+ if (!match) return false;
79
+ const parsed: [number, number, number] = [Number(match[1]), Number(match[2]), Number(match[3])];
80
+ const suffix = match[4];
81
+ for (let i = 0; i < 3; i++) {
82
+ if (parsed[i] !== target[i]) return parsed[i] > target[i];
83
+ }
84
+ return suffix !== "";
85
+ }
86
+
64
87
  class WireClientProviderImpl<Client> implements WireClientProvider<Client> {
65
88
  private client: Client | undefined = undefined;
66
89
 
@@ -661,6 +684,19 @@ export class LLPlClient implements WireClientProviderFactory {
661
684
  return isVersionAtLeast(this.serverInfo.coreVersion, [3, 3, 0]);
662
685
  }
663
686
 
687
+ /**
688
+ * True if the backend honors per-file `permissions` on workdir fill rules
689
+ * (PR #1830 in milaboratory/pl). Backends before this change ignore the
690
+ * requested mode and always land files at the canonical archive perm,
691
+ * making `exec.builder().writeFile/addFile({ writable: true })` a no-op.
692
+ *
693
+ * Tagged at 3.5.0 cut without the change, so [3, 5, 0] excludes the tagged
694
+ * release but includes dev builds past the tag (e.g. "3.5.0-224-g0ca182").
695
+ */
696
+ public get supportsWritableWorkdirFiles(): boolean {
697
+ return isAfterVersion(this.serverInfo.coreVersion, [3, 5, 0]);
698
+ }
699
+
664
700
  /**
665
701
  * Detects the best available wire protocol.
666
702
  * If wireProtocol is explicitly configured, does nothing.