@milaboratories/pl-client 3.11.5 → 3.12.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.
- package/dist/core/PromiseTracker.cjs.map +1 -1
- package/dist/core/PromiseTracker.d.ts.map +1 -1
- package/dist/core/PromiseTracker.js.map +1 -1
- package/dist/core/StatefulPromise.cjs +2 -0
- package/dist/core/StatefulPromise.cjs.map +1 -1
- package/dist/core/StatefulPromise.js +2 -0
- package/dist/core/StatefulPromise.js.map +1 -1
- package/dist/core/abstract_stream.d.ts +1 -1
- package/dist/core/advisory_locks.cjs.map +1 -1
- package/dist/core/advisory_locks.js.map +1 -1
- package/dist/core/auth.cjs.map +1 -1
- package/dist/core/auth.d.ts.map +1 -1
- package/dist/core/auth.js.map +1 -1
- package/dist/core/cache.d.ts.map +1 -1
- package/dist/core/capabilities.cjs.map +1 -1
- package/dist/core/capabilities.d.ts +2 -3
- package/dist/core/capabilities.d.ts.map +1 -1
- package/dist/core/capabilities.js.map +1 -1
- package/dist/core/client.cjs +23 -6
- package/dist/core/client.cjs.map +1 -1
- package/dist/core/client.d.ts +14 -4
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +22 -5
- package/dist/core/client.js.map +1 -1
- package/dist/core/config.cjs.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/default_client.cjs +5 -5
- package/dist/core/default_client.cjs.map +1 -1
- package/dist/core/default_client.d.ts.map +1 -1
- package/dist/core/default_client.js.map +1 -1
- package/dist/core/driver.cjs.map +1 -1
- package/dist/core/driver.d.ts.map +1 -1
- package/dist/core/driver.js.map +1 -1
- package/dist/core/error_resource.cjs.map +1 -1
- package/dist/core/error_resource.js.map +1 -1
- package/dist/core/errors.cjs +11 -11
- package/dist/core/errors.cjs.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +11 -10
- package/dist/core/errors.js.map +1 -1
- package/dist/core/final.cjs +6 -2
- package/dist/core/final.cjs.map +1 -1
- package/dist/core/final.d.ts.map +1 -1
- package/dist/core/final.js +4 -1
- package/dist/core/final.js.map +1 -1
- package/dist/core/ll_client.cjs +60 -16
- package/dist/core/ll_client.cjs.map +1 -1
- package/dist/core/ll_client.d.ts +23 -7
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/ll_client.js +62 -17
- package/dist/core/ll_client.js.map +1 -1
- package/dist/core/ll_transaction.cjs +3 -1
- package/dist/core/ll_transaction.cjs.map +1 -1
- package/dist/core/ll_transaction.d.ts +1 -1
- package/dist/core/ll_transaction.d.ts.map +1 -1
- package/dist/core/ll_transaction.js +2 -1
- package/dist/core/ll_transaction.js.map +1 -1
- package/dist/core/stat.cjs.map +1 -1
- package/dist/core/stat.js.map +1 -1
- package/dist/core/transaction.cjs +91 -4
- package/dist/core/transaction.cjs.map +1 -1
- package/dist/core/transaction.d.ts +52 -2
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +90 -5
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/tree_filter.cjs +73 -18
- package/dist/core/tree_filter.cjs.map +1 -1
- package/dist/core/tree_filter.d.ts +1 -1
- package/dist/core/tree_filter.d.ts.map +1 -1
- package/dist/core/tree_filter.js +72 -19
- package/dist/core/tree_filter.js.map +1 -1
- package/dist/core/type_conversion.cjs +19 -20
- package/dist/core/type_conversion.cjs.map +1 -1
- package/dist/core/type_conversion.js +19 -19
- package/dist/core/type_conversion.js.map +1 -1
- package/dist/core/types.cjs +2 -3
- package/dist/core/types.cjs.map +1 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +2 -2
- package/dist/core/types.js.map +1 -1
- package/dist/core/unauth_client.cjs +7 -6
- package/dist/core/unauth_client.cjs.map +1 -1
- package/dist/core/unauth_client.d.ts +9 -0
- package/dist/core/unauth_client.d.ts.map +1 -1
- package/dist/core/unauth_client.js +7 -5
- package/dist/core/unauth_client.js.map +1 -1
- package/dist/core/user_resources.cjs +57 -1
- package/dist/core/user_resources.cjs.map +1 -1
- package/dist/core/user_resources.d.ts +55 -1
- package/dist/core/user_resources.d.ts.map +1 -1
- package/dist/core/user_resources.js +57 -0
- package/dist/core/user_resources.js.map +1 -1
- package/dist/core/websocket_stream.cjs +17 -20
- package/dist/core/websocket_stream.cjs.map +1 -1
- package/dist/core/websocket_stream.js +16 -19
- package/dist/core/websocket_stream.js.map +1 -1
- package/dist/core/wire.d.ts.map +1 -1
- package/dist/helpers/pl.cjs +25 -0
- package/dist/helpers/pl.cjs.map +1 -1
- package/dist/helpers/pl.d.ts +1 -1
- package/dist/helpers/pl.d.ts.map +1 -1
- package/dist/helpers/pl.js +1 -1
- package/dist/helpers/pl.js.map +1 -1
- package/dist/helpers/poll.cjs +5 -1
- package/dist/helpers/poll.cjs.map +1 -1
- package/dist/helpers/poll.d.ts.map +1 -1
- package/dist/helpers/poll.js +4 -0
- package/dist/helpers/poll.js.map +1 -1
- package/dist/helpers/retry_strategy.cjs +1 -0
- package/dist/helpers/retry_strategy.cjs.map +1 -1
- package/dist/helpers/retry_strategy.js +1 -1
- package/dist/helpers/retry_strategy.js.map +1 -1
- package/dist/helpers/tx_helpers.cjs +0 -1
- package/dist/helpers/tx_helpers.cjs.map +1 -1
- package/dist/helpers/tx_helpers.d.ts.map +1 -1
- package/dist/helpers/tx_helpers.js.map +1 -1
- package/dist/index.cjs +3 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.cjs +0 -1
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.cjs.map +1 -1
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +537 -83
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs +11 -4
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts +12 -6
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js +11 -3
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +112 -7
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +336 -83
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs +23 -7
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts +10 -3
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js +18 -7
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs +1 -2
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.cjs +2 -2
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.js +2 -2
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.js.map +1 -1
- package/dist/proto-grpc/google/protobuf/any.cjs +1 -2
- package/dist/proto-grpc/google/protobuf/any.cjs.map +1 -1
- package/dist/proto-grpc/google/protobuf/any.d.ts.map +1 -1
- package/dist/proto-grpc/google/protobuf/any.js +1 -1
- package/dist/proto-grpc/google/protobuf/any.js.map +1 -1
- package/dist/proto-grpc/google/protobuf/duration.cjs +0 -1
- package/dist/proto-grpc/google/protobuf/duration.cjs.map +1 -1
- package/dist/proto-grpc/google/protobuf/duration.d.ts.map +1 -1
- package/dist/proto-grpc/google/protobuf/duration.js.map +1 -1
- package/dist/proto-grpc/google/protobuf/timestamp.cjs +0 -1
- package/dist/proto-grpc/google/protobuf/timestamp.cjs.map +1 -1
- package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
- package/dist/proto-grpc/google/protobuf/timestamp.js.map +1 -1
- package/dist/proto-grpc/google/rpc/code.d.ts.map +1 -1
- package/dist/proto-rest/index.cjs +1 -1
- package/dist/proto-rest/index.cjs.map +1 -1
- package/dist/proto-rest/index.d.ts +1 -1
- package/dist/proto-rest/index.d.ts.map +1 -1
- package/dist/proto-rest/index.js.map +1 -1
- package/dist/proto-rest/plapi.d.ts +30 -9
- package/dist/proto-rest/plapi.d.ts.map +1 -1
- package/dist/test/tcp-proxy.cjs +2 -2
- package/dist/test/tcp-proxy.cjs.map +1 -1
- package/dist/test/tcp-proxy.d.ts +1 -1
- package/dist/test/tcp-proxy.js.map +1 -1
- package/dist/test/test_config.cjs +8 -2
- package/dist/test/test_config.cjs.map +1 -1
- package/dist/test/test_config.d.ts +1 -1
- package/dist/test/test_config.d.ts.map +1 -1
- package/dist/test/test_config.js +1 -1
- package/dist/test/test_config.js.map +1 -1
- package/dist/util/pl.cjs.map +1 -1
- package/dist/util/pl.js.map +1 -1
- package/dist/util/util.cjs.map +1 -1
- package/dist/util/util.js.map +1 -1
- package/package.json +4 -4
- package/src/core/capabilities.ts +10 -3
- package/src/core/client.ts +30 -3
- package/src/core/errors.ts +2 -0
- package/src/core/final.ts +4 -0
- package/src/core/ll_client.test.ts +15 -2
- package/src/core/ll_client.ts +66 -11
- package/src/core/transaction.ts +103 -3
- package/src/core/unauth_client.ts +17 -2
- package/src/core/user_resources.ts +85 -0
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +16 -3
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +335 -12
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.ts +21 -7
- package/src/proto-rest/plapi.ts +29 -8
- package/dist/proto-grpc/google/rpc/code.cjs +0 -224
- package/dist/proto-grpc/google/rpc/code.cjs.map +0 -1
- package/dist/proto-grpc/google/rpc/code.js +0 -224
- package/dist/proto-grpc/google/rpc/code.js.map +0 -1
|
@@ -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 /** 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
|
+
{"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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"}
|
|
@@ -38,5 +38,5 @@ declare function withTempRoot<T>(body: (pl: PlClient, proxy: Awaited<ReturnType<
|
|
|
38
38
|
proxyLatencyMs?: number;
|
|
39
39
|
}): Promise<T>;
|
|
40
40
|
//#endregion
|
|
41
|
-
export { test_config_d_exports };
|
|
41
|
+
export { TEST_REQUEST_TIMEOUT, TestConfig, type TestTcpProxy, WithTempRootOptions, getTestClient, getTestClientConf, getTestConfig, getTestLLClient, plAddressToTestConfig, test_config_d_exports, withTempRoot };
|
|
42
42
|
//# sourceMappingURL=test_config.d.ts.map
|
|
@@ -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,
|
|
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,IAAiB,UAAU;;cAsB9B,oBAAA;;iBAGG,qBAAA,CAAsB,OAAA,WAAkB,cAAc;AAAA,iBAkDhD,iBAAA,IAAqB,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,IAAgB,IAAA,GAAO,EAAA,EAAI,QAAA,KAAa,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;AAAA,iBAE7D,YAAA,IACpB,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"}
|
package/dist/test/test_config.js
CHANGED
|
@@ -160,6 +160,6 @@ async function withTempRoot(body, options = {}) {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
//#endregion
|
|
163
|
-
export { test_config_exports };
|
|
163
|
+
export { getTestClient, getTestClientConf, getTestConfig, getTestLLClient, plAddressToTestConfig, test_config_exports, withTempRoot };
|
|
164
164
|
|
|
165
165
|
//# sourceMappingURL=test_config.js.map
|
|
@@ -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 /** 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
|
+
{"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/dist/util/pl.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pl.cjs","names":[],"sources":["../../src/util/pl.ts"],"sourcesContent":["export type PlJWTPayload = {\n sub: string; // user ID\n iss: string; // backend instance ID\n\n // deprecated. Prior backend capability auth:v2. Use uid instead.\n user: {\n login: string;\n };\n\n exp: number;\n iat: number;\n};\n\nexport function parsePlJwt(token: string): PlJWTPayload {\n return JSON.parse(Buffer.from(token.split(\".\")[1], \"base64\").toString());\n}\n"],"mappings":";AAaA,SAAgB,WAAW,OAA6B;
|
|
1
|
+
{"version":3,"file":"pl.cjs","names":[],"sources":["../../src/util/pl.ts"],"sourcesContent":["export type PlJWTPayload = {\n sub: string; // user ID\n iss: string; // backend instance ID\n\n // deprecated. Prior backend capability auth:v2. Use uid instead.\n user: {\n login: string;\n };\n\n exp: number;\n iat: number;\n};\n\nexport function parsePlJwt(token: string): PlJWTPayload {\n return JSON.parse(Buffer.from(token.split(\".\")[1], \"base64\").toString());\n}\n"],"mappings":";AAaA,SAAgB,WAAW,OAA6B;CACtD,OAAO,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,SAAS,CAAC;AACzE"}
|
package/dist/util/pl.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pl.js","names":[],"sources":["../../src/util/pl.ts"],"sourcesContent":["export type PlJWTPayload = {\n sub: string; // user ID\n iss: string; // backend instance ID\n\n // deprecated. Prior backend capability auth:v2. Use uid instead.\n user: {\n login: string;\n };\n\n exp: number;\n iat: number;\n};\n\nexport function parsePlJwt(token: string): PlJWTPayload {\n return JSON.parse(Buffer.from(token.split(\".\")[1], \"base64\").toString());\n}\n"],"mappings":";AAaA,SAAgB,WAAW,OAA6B;
|
|
1
|
+
{"version":3,"file":"pl.js","names":[],"sources":["../../src/util/pl.ts"],"sourcesContent":["export type PlJWTPayload = {\n sub: string; // user ID\n iss: string; // backend instance ID\n\n // deprecated. Prior backend capability auth:v2. Use uid instead.\n user: {\n login: string;\n };\n\n exp: number;\n iat: number;\n};\n\nexport function parsePlJwt(token: string): PlJWTPayload {\n return JSON.parse(Buffer.from(token.split(\".\")[1], \"base64\").toString());\n}\n"],"mappings":";AAaA,SAAgB,WAAW,OAA6B;CACtD,OAAO,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,SAAS,CAAC;AACzE"}
|
package/dist/util/util.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.cjs","names":[],"sources":["../../src/util/util.ts"],"sourcesContent":["function isArrayBufferOrView(value: unknown): value is ArrayBufferLike {\n return value instanceof ArrayBuffer || ArrayBuffer.isView(value);\n}\n\nexport function toBytes(value: string | Uint8Array): Uint8Array {\n if (typeof value === \"string\") return Buffer.from(value);\n else if (isArrayBufferOrView(value)) return value;\n // eslint-disable-next-line @typescript-eslint/restrict-template-expressions\n else throw new Error(`Unexpected type: ${value}`);\n}\n"],"mappings":";AAAA,SAAS,oBAAoB,OAA0C;
|
|
1
|
+
{"version":3,"file":"util.cjs","names":[],"sources":["../../src/util/util.ts"],"sourcesContent":["function isArrayBufferOrView(value: unknown): value is ArrayBufferLike {\n return value instanceof ArrayBuffer || ArrayBuffer.isView(value);\n}\n\nexport function toBytes(value: string | Uint8Array): Uint8Array {\n if (typeof value === \"string\") return Buffer.from(value);\n else if (isArrayBufferOrView(value)) return value;\n // eslint-disable-next-line @typescript-eslint/restrict-template-expressions\n else throw new Error(`Unexpected type: ${value}`);\n}\n"],"mappings":";AAAA,SAAS,oBAAoB,OAA0C;CACrE,OAAO,iBAAiB,eAAe,YAAY,OAAO,KAAK;AACjE;AAEA,SAAgB,QAAQ,OAAwC;CAC9D,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK,KAAK;MAClD,IAAI,oBAAoB,KAAK,GAAG,OAAO;MAEvC,MAAM,IAAI,MAAM,oBAAoB,OAAO;AAClD"}
|
package/dist/util/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","names":[],"sources":["../../src/util/util.ts"],"sourcesContent":["function isArrayBufferOrView(value: unknown): value is ArrayBufferLike {\n return value instanceof ArrayBuffer || ArrayBuffer.isView(value);\n}\n\nexport function toBytes(value: string | Uint8Array): Uint8Array {\n if (typeof value === \"string\") return Buffer.from(value);\n else if (isArrayBufferOrView(value)) return value;\n // eslint-disable-next-line @typescript-eslint/restrict-template-expressions\n else throw new Error(`Unexpected type: ${value}`);\n}\n"],"mappings":";AAAA,SAAS,oBAAoB,OAA0C;
|
|
1
|
+
{"version":3,"file":"util.js","names":[],"sources":["../../src/util/util.ts"],"sourcesContent":["function isArrayBufferOrView(value: unknown): value is ArrayBufferLike {\n return value instanceof ArrayBuffer || ArrayBuffer.isView(value);\n}\n\nexport function toBytes(value: string | Uint8Array): Uint8Array {\n if (typeof value === \"string\") return Buffer.from(value);\n else if (isArrayBufferOrView(value)) return value;\n // eslint-disable-next-line @typescript-eslint/restrict-template-expressions\n else throw new Error(`Unexpected type: ${value}`);\n}\n"],"mappings":";AAAA,SAAS,oBAAoB,OAA0C;CACrE,OAAO,iBAAiB,eAAe,YAAY,OAAO,KAAK;AACjE;AAEA,SAAgB,QAAQ,OAAwC;CAC9D,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK,KAAK;MAClD,IAAI,oBAAoB,KAAK,GAAG,OAAO;MAEvC,MAAM,IAAI,MAAM,oBAAoB,OAAO;AAClD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-client",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.1",
|
|
4
4
|
"description": "New TS/JS client for Platform API",
|
|
5
5
|
"files": [
|
|
6
6
|
"./dist/**/*",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"undici": "~7.16.0",
|
|
31
31
|
"utility-types": "^3.11.0",
|
|
32
32
|
"yaml": "^2.8.0",
|
|
33
|
+
"@milaboratories/pl-model-common": "1.46.3",
|
|
33
34
|
"@milaboratories/pl-http": "1.2.4",
|
|
34
|
-
"@milaboratories/pl-model-common": "1.46.2",
|
|
35
35
|
"@milaboratories/ts-helpers": "1.8.3"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"typescript": "~5.9.3",
|
|
43
43
|
"vitest": "^4.1.3",
|
|
44
44
|
"@milaboratories/build-configs": "2.0.0",
|
|
45
|
-
"@milaboratories/ts-builder": "1.
|
|
46
|
-
"@milaboratories/ts-configs": "1.
|
|
45
|
+
"@milaboratories/ts-builder": "1.6.0",
|
|
46
|
+
"@milaboratories/ts-configs": "1.3.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=22.19.0"
|
package/src/core/capabilities.ts
CHANGED
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* to detect incompatibilities between blocks and backend via
|
|
6
6
|
* via `TemplateDataV3.requiredCapabilities`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* tokens in sync with the backend's advertised list. Format is
|
|
8
|
+
* Keep tokens in sync with the backend's advertised list. Format is
|
|
10
9
|
* `<feature>:<version>`; bump the version when wire semantics change.
|
|
11
10
|
*
|
|
12
11
|
* Shared across:
|
|
@@ -15,7 +14,15 @@
|
|
|
15
14
|
* - `@platforma-sdk/block-tools` — copies it onto the published manifest
|
|
16
15
|
* - `@milaboratories/pl-middle-layer` — checks it at install time
|
|
17
16
|
*/
|
|
18
|
-
export type BackendCapability =
|
|
17
|
+
export type BackendCapability =
|
|
18
|
+
| "auth:v2"
|
|
19
|
+
| "treeFilter:v2"
|
|
20
|
+
| "wasm:v1"
|
|
21
|
+
// Project-sharing capability tokens.
|
|
22
|
+
| "crossTreeRefs:v1" // cross-color field attach (accept a foreign-colored shared envelope)
|
|
23
|
+
| "userListing:v1" // list users for the recipient picker
|
|
24
|
+
| "publicGrants:v1" // public (everyone) grants allowed for any role
|
|
25
|
+
| "txListGrants:v1"; // list grants inside a transaction (batched recipient reads)
|
|
19
26
|
|
|
20
27
|
/** True iff `capabilities` advertises the requested token. */
|
|
21
28
|
export function hasCapability(
|
package/src/core/client.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AuthOps, PlClientConfig, PlConnectionStatusListener, wireProtocol
|
|
|
2
2
|
import type { PlCallOps } from "./ll_client";
|
|
3
3
|
import { LLPlClient } from "./ll_client";
|
|
4
4
|
import { PlTransaction, TxCommitConflict } from "./transaction";
|
|
5
|
+
import type { Role } from "./transaction";
|
|
5
6
|
import type { OptionalSignedResourceId, SignedResourceId } from "./types";
|
|
6
7
|
import {
|
|
7
8
|
ensureSignedResourceIdNotNull,
|
|
@@ -80,6 +81,11 @@ export class PlClient {
|
|
|
80
81
|
|
|
81
82
|
private _userResources?: UserResources;
|
|
82
83
|
|
|
84
|
+
/** Cached effective role of the authenticated session, fetched once during
|
|
85
|
+
* init via the GetSessionInfo RPC. `null` for an anonymous (no-auth)
|
|
86
|
+
* connection — mirrors {@link authUser}. Read through {@link currentUserRole}. */
|
|
87
|
+
private _currentUserRole: Role | null = null;
|
|
88
|
+
|
|
83
89
|
private _txCommittedStat: TxStat = initialTxStat();
|
|
84
90
|
private _txConflictStat: TxStat = initialTxStat();
|
|
85
91
|
private _txErrorStat: TxStat = initialTxStat();
|
|
@@ -232,10 +238,24 @@ export class PlClient {
|
|
|
232
238
|
return this._ll!.hasCapability(capability);
|
|
233
239
|
}
|
|
234
240
|
|
|
241
|
+
/** Effective role of the authenticated session, resolved once during init via
|
|
242
|
+
* the GetSessionInfo RPC and cached. `null` for an anonymous (no-auth)
|
|
243
|
+
* connection, or when the backend predates GetSessionInfo. Mirrors
|
|
244
|
+
* {@link authUser}'s null contract. */
|
|
245
|
+
public get currentUserRole(): Role | null {
|
|
246
|
+
this.checkInitialized();
|
|
247
|
+
return this._currentUserRole;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Login of the authenticated user, or `null` for an anonymous connection. */
|
|
251
|
+
public get authUser(): string | null {
|
|
252
|
+
this.checkInitialized();
|
|
253
|
+
return this.userResources.authUser;
|
|
254
|
+
}
|
|
255
|
+
|
|
235
256
|
/**
|
|
236
|
-
* True if the backend honors per-file `permissions` on workdir fill rules
|
|
237
|
-
*
|
|
238
|
-
* for the full definition.
|
|
257
|
+
* True if the backend honors per-file `permissions` on workdir fill rules.
|
|
258
|
+
* See {@link LLPlClient.supportsWritableWorkdirFiles} for the full definition.
|
|
239
259
|
*/
|
|
240
260
|
public get supportsWritableWorkdirFiles(): boolean {
|
|
241
261
|
this.checkInitialized();
|
|
@@ -276,6 +296,13 @@ export class PlClient {
|
|
|
276
296
|
|
|
277
297
|
const userRoot = await this.userResources.getUserRoot({ createIfNotExists: true });
|
|
278
298
|
|
|
299
|
+
// Resolve the caller's role once; only the publicGrants-gated share-with-everybody check uses it,
|
|
300
|
+
// so skip it for anonymous connections or when publicGrants:v1 is absent. Use _ll.hasCapability,
|
|
301
|
+
// not this.hasCapability: we are mid-init, so this.checkInitialized() would throw.
|
|
302
|
+
if (this.userResources.authUser !== null && this._ll.hasCapability("publicGrants:v1")) {
|
|
303
|
+
this._currentUserRole = (await this._ll.getSessionInfo()).role;
|
|
304
|
+
}
|
|
305
|
+
|
|
279
306
|
if (this.conf.alternativeRoot === undefined) {
|
|
280
307
|
this._clientRoot = userRoot;
|
|
281
308
|
} else {
|
package/src/core/errors.ts
CHANGED
|
@@ -20,6 +20,8 @@ export function isUnauthenticated(err: unknown, nested: boolean = false): boolea
|
|
|
20
20
|
if ((err as any).name == "RpcError" && (err as any).code == "UNAUTHENTICATED") return true;
|
|
21
21
|
if ((err as any).name == "RESTError" && (err as any).status.code == Code.UNAUTHENTICATED)
|
|
22
22
|
return true;
|
|
23
|
+
// PlError (e.g. UnrecoverablePlError from a failed streaming tx) carries the status code numerically.
|
|
24
|
+
if (err instanceof PlError && err.status?.code === Code.UNAUTHENTICATED) return true;
|
|
23
25
|
if ((err as any).cause !== undefined && !nested)
|
|
24
26
|
return isUnauthenticated((err as any).cause, true);
|
|
25
27
|
return false;
|
package/src/core/final.ts
CHANGED
|
@@ -87,6 +87,10 @@ export const DefaultFinalResourceDataPredicate: FinalResourceDataPredicate = (r)
|
|
|
87
87
|
case ResourceTypeName.UserProject:
|
|
88
88
|
case ResourceTypeName.Projects:
|
|
89
89
|
case ResourceTypeName.ClientRoot:
|
|
90
|
+
// Never final — these sharing resources gain and lose dynamic child fields over their lifetime.
|
|
91
|
+
case ResourceTypeName.SharingOutbox:
|
|
92
|
+
case ResourceTypeName.SharingState:
|
|
93
|
+
case ResourceTypeName.SharedEnvelope:
|
|
90
94
|
return false;
|
|
91
95
|
default:
|
|
92
96
|
if (
|
|
@@ -50,7 +50,15 @@ test("unauthenticated status change", async () => {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// The flip to Unauthenticated is async, so await it via the status listener.
|
|
54
|
+
let onUnauthenticated!: () => void;
|
|
55
|
+
const unauthenticated = new Promise<void>((resolve) => (onUnauthenticated = resolve));
|
|
56
|
+
|
|
57
|
+
const client = await LLPlClient.build(plAddressToTestConfig(cfg.address), {
|
|
58
|
+
statusListener: (s) => {
|
|
59
|
+
if (s === "Unauthenticated") onUnauthenticated();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
54
62
|
expect(client.status).toBe("OK");
|
|
55
63
|
|
|
56
64
|
const tx = client.createTx(true);
|
|
@@ -73,7 +81,12 @@ test("unauthenticated status change", async () => {
|
|
|
73
81
|
await tx.await();
|
|
74
82
|
}).rejects.toThrow(UnauthenticatedError);
|
|
75
83
|
|
|
76
|
-
await
|
|
84
|
+
await Promise.race([
|
|
85
|
+
unauthenticated,
|
|
86
|
+
tp
|
|
87
|
+
.setTimeout(5000)
|
|
88
|
+
.then(() => Promise.reject(new Error("status never became Unauthenticated"))),
|
|
89
|
+
]);
|
|
77
90
|
|
|
78
91
|
expect(client.status).toEqual("Unauthenticated");
|
|
79
92
|
});
|
package/src/core/ll_client.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
TxAPI_ServerMessage,
|
|
43
43
|
} from "../proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api";
|
|
44
44
|
import type { MiLogger } from "@milaboratories/ts-helpers";
|
|
45
|
-
import { isAbortedError } from "./errors";
|
|
45
|
+
import { isAbortedError, isUnauthenticated } from "./errors";
|
|
46
46
|
import { Timestamp } from "../proto-grpc/google/protobuf/timestamp";
|
|
47
47
|
|
|
48
48
|
export interface PlCallOps {
|
|
@@ -122,6 +122,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
122
122
|
private _authMethodsSync?: grpcTypes.AuthAPI_ListMethods_Response;
|
|
123
123
|
|
|
124
124
|
private _status: PlConnectionStatus = "OK";
|
|
125
|
+
private authProbeInFlight = false;
|
|
125
126
|
private readonly statusListener?: PlConnectionStatusListener;
|
|
126
127
|
|
|
127
128
|
private _wireProto: wireProtocol = "grpc";
|
|
@@ -393,6 +394,25 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
393
394
|
});
|
|
394
395
|
}
|
|
395
396
|
|
|
397
|
+
/**
|
|
398
|
+
* UNAUTHENTICATED covers both a dead session (must reset) and a per-request authorization failure
|
|
399
|
+
* such as a revoked resource signature (must not). Disambiguate by probing getSessionInfo — an
|
|
400
|
+
* authenticated call with no resource signature: success means the session is alive and the failure
|
|
401
|
+
* was resource-level; a probe that is itself Unauthenticated means real auth loss. Coalesced via
|
|
402
|
+
* authProbeInFlight so a burst of failures costs one probe.
|
|
403
|
+
*/
|
|
404
|
+
private async verifyThenMaybeEscalateUnauthenticated(): Promise<void> {
|
|
405
|
+
if (this._status !== "OK" || this.authProbeInFlight) return;
|
|
406
|
+
this.authProbeInFlight = true;
|
|
407
|
+
try {
|
|
408
|
+
await this.getSessionInfo();
|
|
409
|
+
} catch (e) {
|
|
410
|
+
if (isUnauthenticated(e)) this.updateStatus("Unauthenticated");
|
|
411
|
+
} finally {
|
|
412
|
+
this.authProbeInFlight = false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
396
416
|
public get status(): PlConnectionStatus {
|
|
397
417
|
return this._status;
|
|
398
418
|
}
|
|
@@ -458,7 +478,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
458
478
|
}
|
|
459
479
|
|
|
460
480
|
if (respErr.error.code === Code.UNAUTHENTICATED) {
|
|
461
|
-
this.
|
|
481
|
+
void this.verifyThenMaybeEscalateUnauthenticated();
|
|
462
482
|
}
|
|
463
483
|
|
|
464
484
|
// Let later middleware to deal with standard gRPC error.
|
|
@@ -476,7 +496,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
476
496
|
onReceiveStatus: (status, next) => {
|
|
477
497
|
if (status.code == GrpcStatus.UNAUTHENTICATED)
|
|
478
498
|
// (!!!) don't change to "==="
|
|
479
|
-
this.
|
|
499
|
+
void this.verifyThenMaybeEscalateUnauthenticated();
|
|
480
500
|
if (status.code == GrpcStatus.UNAVAILABLE)
|
|
481
501
|
// (!!!) don't change to "==="
|
|
482
502
|
this.updateStatus("Disconnected");
|
|
@@ -649,7 +669,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
649
669
|
/** Request a one-time PKCE nonce + its expiry from the backend. The desktop is
|
|
650
670
|
* expected to place the nonce verbatim into the OIDC auth-request before
|
|
651
671
|
* redirecting to the IdP; the backend enforces the expiry on {@link loginSSO}. */
|
|
652
|
-
public async beginSSOLogin(): Promise<{ nonce: string; expiresAt: Date }> {
|
|
672
|
+
public async beginSSOLogin(): Promise<{ nonce: string; expiresAt: Date; clientSecret?: string }> {
|
|
653
673
|
const cl = this.clientProvider.get();
|
|
654
674
|
if (cl instanceof GrpcPlApiClient) {
|
|
655
675
|
const resp = (await cl.beginSSOLogin({}).response).flow;
|
|
@@ -660,6 +680,7 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
660
680
|
return {
|
|
661
681
|
nonce: resp.publicPkce.nonce,
|
|
662
682
|
expiresAt: Timestamp.toDate(exp),
|
|
683
|
+
clientSecret: resp.publicPkce.clientSecret,
|
|
663
684
|
};
|
|
664
685
|
} else {
|
|
665
686
|
const wsResponse = notEmpty(
|
|
@@ -667,7 +688,11 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
667
688
|
"REST: empty response for beginSSOLogin request",
|
|
668
689
|
);
|
|
669
690
|
const pkce = notEmpty(wsResponse.publicPkce, "REST: missing publicPkce in beginSSOLogin");
|
|
670
|
-
return {
|
|
691
|
+
return {
|
|
692
|
+
nonce: pkce.nonce,
|
|
693
|
+
expiresAt: new Date(pkce.expiresAt),
|
|
694
|
+
clientSecret: pkce.clientSecret,
|
|
695
|
+
};
|
|
671
696
|
}
|
|
672
697
|
}
|
|
673
698
|
|
|
@@ -737,13 +762,12 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
737
762
|
}
|
|
738
763
|
|
|
739
764
|
/**
|
|
740
|
-
* True if the backend honors per-file `permissions` on workdir fill rules
|
|
741
|
-
* (
|
|
742
|
-
* requested mode and
|
|
743
|
-
* making `exec.builder().writeFile/addFile({ writable: true })` a no-op.
|
|
765
|
+
* True if the backend honors per-file `permissions` on workdir fill rules, so
|
|
766
|
+
* `exec.builder().writeFile/addFile({ writable: true })` takes effect. When false
|
|
767
|
+
* the requested mode is ignored and files land at the canonical archive perm.
|
|
744
768
|
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
769
|
+
* [3, 5, 0] excludes the tagged 3.5.0 release but includes dev builds past the
|
|
770
|
+
* tag (e.g. "3.5.0-224-g0ca182"), which is where the support lands.
|
|
747
771
|
*/
|
|
748
772
|
public get supportsWritableWorkdirFiles(): boolean {
|
|
749
773
|
return isAfterVersion(this.serverInfo.coreVersion, [3, 5, 0]);
|
|
@@ -926,6 +950,37 @@ export class LLPlClient implements WireClientProviderFactory {
|
|
|
926
950
|
return responses;
|
|
927
951
|
}
|
|
928
952
|
|
|
953
|
+
/** Returns the authenticated caller's session id and effective role.
|
|
954
|
+
* Models the {@link getUserRoot} wrapper: gRPC unary with a REST fallback. */
|
|
955
|
+
public async getSessionInfo(): Promise<grpcTypes.AuthAPI_GetSessionInfo_Response> {
|
|
956
|
+
const cl = this.clientProvider.get();
|
|
957
|
+
if (cl instanceof GrpcPlApiClient) {
|
|
958
|
+
return (await cl.getSessionInfo({})).response;
|
|
959
|
+
} else {
|
|
960
|
+
const resp = notEmpty(
|
|
961
|
+
(await cl.POST("/v1/auth/session-info", { body: {} })).data,
|
|
962
|
+
"REST: empty response for getSessionInfo request",
|
|
963
|
+
);
|
|
964
|
+
return {
|
|
965
|
+
sessionId: Uint8Array.from(Buffer.from(resp.sessionId, "base64")),
|
|
966
|
+
role: resp.role as AuthAPI_Role,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Lists the users known to the server — the recipient picker's source. A user
|
|
972
|
+
* becomes known on first login; provisioned users who have never logged in do not
|
|
973
|
+
* appear. gRPC-only (unary, no REST binding). */
|
|
974
|
+
public async listUsers(): Promise<grpcTypes.AuthAPI_User[]> {
|
|
975
|
+
const cl = this.clientProvider.get();
|
|
976
|
+
|
|
977
|
+
if (!(cl instanceof GrpcPlApiClient)) {
|
|
978
|
+
throw new Error("ListUsers requires gRPC wire protocol; REST is not supported");
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return (await cl.listUsers({})).response.users;
|
|
982
|
+
}
|
|
983
|
+
|
|
929
984
|
public async txSync(txId: bigint): Promise<void> {
|
|
930
985
|
const cl = this.clientProvider.get();
|
|
931
986
|
if (cl instanceof GrpcPlApiClient) {
|