@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.
Files changed (206) hide show
  1. package/dist/core/PromiseTracker.cjs.map +1 -1
  2. package/dist/core/PromiseTracker.d.ts.map +1 -1
  3. package/dist/core/PromiseTracker.js.map +1 -1
  4. package/dist/core/StatefulPromise.cjs +2 -0
  5. package/dist/core/StatefulPromise.cjs.map +1 -1
  6. package/dist/core/StatefulPromise.js +2 -0
  7. package/dist/core/StatefulPromise.js.map +1 -1
  8. package/dist/core/abstract_stream.d.ts +1 -1
  9. package/dist/core/advisory_locks.cjs.map +1 -1
  10. package/dist/core/advisory_locks.js.map +1 -1
  11. package/dist/core/auth.cjs.map +1 -1
  12. package/dist/core/auth.d.ts.map +1 -1
  13. package/dist/core/auth.js.map +1 -1
  14. package/dist/core/cache.d.ts.map +1 -1
  15. package/dist/core/capabilities.cjs.map +1 -1
  16. package/dist/core/capabilities.d.ts +2 -3
  17. package/dist/core/capabilities.d.ts.map +1 -1
  18. package/dist/core/capabilities.js.map +1 -1
  19. package/dist/core/client.cjs +23 -6
  20. package/dist/core/client.cjs.map +1 -1
  21. package/dist/core/client.d.ts +14 -4
  22. package/dist/core/client.d.ts.map +1 -1
  23. package/dist/core/client.js +22 -5
  24. package/dist/core/client.js.map +1 -1
  25. package/dist/core/config.cjs.map +1 -1
  26. package/dist/core/config.d.ts.map +1 -1
  27. package/dist/core/config.js.map +1 -1
  28. package/dist/core/default_client.cjs +5 -5
  29. package/dist/core/default_client.cjs.map +1 -1
  30. package/dist/core/default_client.d.ts.map +1 -1
  31. package/dist/core/default_client.js.map +1 -1
  32. package/dist/core/driver.cjs.map +1 -1
  33. package/dist/core/driver.d.ts.map +1 -1
  34. package/dist/core/driver.js.map +1 -1
  35. package/dist/core/error_resource.cjs.map +1 -1
  36. package/dist/core/error_resource.js.map +1 -1
  37. package/dist/core/errors.cjs +11 -11
  38. package/dist/core/errors.cjs.map +1 -1
  39. package/dist/core/errors.d.ts.map +1 -1
  40. package/dist/core/errors.js +11 -10
  41. package/dist/core/errors.js.map +1 -1
  42. package/dist/core/final.cjs +6 -2
  43. package/dist/core/final.cjs.map +1 -1
  44. package/dist/core/final.d.ts.map +1 -1
  45. package/dist/core/final.js +4 -1
  46. package/dist/core/final.js.map +1 -1
  47. package/dist/core/ll_client.cjs +60 -16
  48. package/dist/core/ll_client.cjs.map +1 -1
  49. package/dist/core/ll_client.d.ts +23 -7
  50. package/dist/core/ll_client.d.ts.map +1 -1
  51. package/dist/core/ll_client.js +62 -17
  52. package/dist/core/ll_client.js.map +1 -1
  53. package/dist/core/ll_transaction.cjs +3 -1
  54. package/dist/core/ll_transaction.cjs.map +1 -1
  55. package/dist/core/ll_transaction.d.ts +1 -1
  56. package/dist/core/ll_transaction.d.ts.map +1 -1
  57. package/dist/core/ll_transaction.js +2 -1
  58. package/dist/core/ll_transaction.js.map +1 -1
  59. package/dist/core/stat.cjs.map +1 -1
  60. package/dist/core/stat.js.map +1 -1
  61. package/dist/core/transaction.cjs +91 -4
  62. package/dist/core/transaction.cjs.map +1 -1
  63. package/dist/core/transaction.d.ts +52 -2
  64. package/dist/core/transaction.d.ts.map +1 -1
  65. package/dist/core/transaction.js +90 -5
  66. package/dist/core/transaction.js.map +1 -1
  67. package/dist/core/tree_filter.cjs +73 -18
  68. package/dist/core/tree_filter.cjs.map +1 -1
  69. package/dist/core/tree_filter.d.ts +1 -1
  70. package/dist/core/tree_filter.d.ts.map +1 -1
  71. package/dist/core/tree_filter.js +72 -19
  72. package/dist/core/tree_filter.js.map +1 -1
  73. package/dist/core/type_conversion.cjs +19 -20
  74. package/dist/core/type_conversion.cjs.map +1 -1
  75. package/dist/core/type_conversion.js +19 -19
  76. package/dist/core/type_conversion.js.map +1 -1
  77. package/dist/core/types.cjs +2 -3
  78. package/dist/core/types.cjs.map +1 -1
  79. package/dist/core/types.d.ts.map +1 -1
  80. package/dist/core/types.js +2 -2
  81. package/dist/core/types.js.map +1 -1
  82. package/dist/core/unauth_client.cjs +7 -6
  83. package/dist/core/unauth_client.cjs.map +1 -1
  84. package/dist/core/unauth_client.d.ts +9 -0
  85. package/dist/core/unauth_client.d.ts.map +1 -1
  86. package/dist/core/unauth_client.js +7 -5
  87. package/dist/core/unauth_client.js.map +1 -1
  88. package/dist/core/user_resources.cjs +57 -1
  89. package/dist/core/user_resources.cjs.map +1 -1
  90. package/dist/core/user_resources.d.ts +55 -1
  91. package/dist/core/user_resources.d.ts.map +1 -1
  92. package/dist/core/user_resources.js +57 -0
  93. package/dist/core/user_resources.js.map +1 -1
  94. package/dist/core/websocket_stream.cjs +17 -20
  95. package/dist/core/websocket_stream.cjs.map +1 -1
  96. package/dist/core/websocket_stream.js +16 -19
  97. package/dist/core/websocket_stream.js.map +1 -1
  98. package/dist/core/wire.d.ts.map +1 -1
  99. package/dist/helpers/pl.cjs +25 -0
  100. package/dist/helpers/pl.cjs.map +1 -1
  101. package/dist/helpers/pl.d.ts +1 -1
  102. package/dist/helpers/pl.d.ts.map +1 -1
  103. package/dist/helpers/pl.js +1 -1
  104. package/dist/helpers/pl.js.map +1 -1
  105. package/dist/helpers/poll.cjs +5 -1
  106. package/dist/helpers/poll.cjs.map +1 -1
  107. package/dist/helpers/poll.d.ts.map +1 -1
  108. package/dist/helpers/poll.js +4 -0
  109. package/dist/helpers/poll.js.map +1 -1
  110. package/dist/helpers/retry_strategy.cjs +1 -0
  111. package/dist/helpers/retry_strategy.cjs.map +1 -1
  112. package/dist/helpers/retry_strategy.js +1 -1
  113. package/dist/helpers/retry_strategy.js.map +1 -1
  114. package/dist/helpers/tx_helpers.cjs +0 -1
  115. package/dist/helpers/tx_helpers.cjs.map +1 -1
  116. package/dist/helpers/tx_helpers.d.ts.map +1 -1
  117. package/dist/helpers/tx_helpers.js.map +1 -1
  118. package/dist/index.cjs +3 -1
  119. package/dist/index.d.ts +3 -3
  120. package/dist/index.js +2 -2
  121. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.cjs +0 -1
  122. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.cjs.map +1 -1
  123. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.d.ts.map +1 -1
  124. package/dist/proto-grpc/github.com/googleapis/googleapis/google/rpc/status.js.map +1 -1
  125. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +537 -83
  126. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
  127. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs +11 -4
  128. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs.map +1 -1
  129. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts +12 -6
  130. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts.map +1 -1
  131. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js +11 -3
  132. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js.map +1 -1
  133. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +112 -7
  134. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
  135. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +336 -83
  136. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
  137. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs +23 -7
  138. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs.map +1 -1
  139. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts +10 -3
  140. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts.map +1 -1
  141. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js +18 -7
  142. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js.map +1 -1
  143. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs +1 -2
  144. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs.map +1 -1
  145. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts.map +1 -1
  146. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js +1 -1
  147. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js.map +1 -1
  148. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.cjs +2 -2
  149. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.cjs.map +1 -1
  150. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts +1 -1
  151. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.d.ts.map +1 -1
  152. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.js +2 -2
  153. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/resource_types.js.map +1 -1
  154. package/dist/proto-grpc/google/protobuf/any.cjs +1 -2
  155. package/dist/proto-grpc/google/protobuf/any.cjs.map +1 -1
  156. package/dist/proto-grpc/google/protobuf/any.d.ts.map +1 -1
  157. package/dist/proto-grpc/google/protobuf/any.js +1 -1
  158. package/dist/proto-grpc/google/protobuf/any.js.map +1 -1
  159. package/dist/proto-grpc/google/protobuf/duration.cjs +0 -1
  160. package/dist/proto-grpc/google/protobuf/duration.cjs.map +1 -1
  161. package/dist/proto-grpc/google/protobuf/duration.d.ts.map +1 -1
  162. package/dist/proto-grpc/google/protobuf/duration.js.map +1 -1
  163. package/dist/proto-grpc/google/protobuf/timestamp.cjs +0 -1
  164. package/dist/proto-grpc/google/protobuf/timestamp.cjs.map +1 -1
  165. package/dist/proto-grpc/google/protobuf/timestamp.d.ts.map +1 -1
  166. package/dist/proto-grpc/google/protobuf/timestamp.js.map +1 -1
  167. package/dist/proto-grpc/google/rpc/code.d.ts.map +1 -1
  168. package/dist/proto-rest/index.cjs +1 -1
  169. package/dist/proto-rest/index.cjs.map +1 -1
  170. package/dist/proto-rest/index.d.ts +1 -1
  171. package/dist/proto-rest/index.d.ts.map +1 -1
  172. package/dist/proto-rest/index.js.map +1 -1
  173. package/dist/proto-rest/plapi.d.ts +30 -9
  174. package/dist/proto-rest/plapi.d.ts.map +1 -1
  175. package/dist/test/tcp-proxy.cjs +2 -2
  176. package/dist/test/tcp-proxy.cjs.map +1 -1
  177. package/dist/test/tcp-proxy.d.ts +1 -1
  178. package/dist/test/tcp-proxy.js.map +1 -1
  179. package/dist/test/test_config.cjs +8 -2
  180. package/dist/test/test_config.cjs.map +1 -1
  181. package/dist/test/test_config.d.ts +1 -1
  182. package/dist/test/test_config.d.ts.map +1 -1
  183. package/dist/test/test_config.js +1 -1
  184. package/dist/test/test_config.js.map +1 -1
  185. package/dist/util/pl.cjs.map +1 -1
  186. package/dist/util/pl.js.map +1 -1
  187. package/dist/util/util.cjs.map +1 -1
  188. package/dist/util/util.js.map +1 -1
  189. package/package.json +4 -4
  190. package/src/core/capabilities.ts +10 -3
  191. package/src/core/client.ts +30 -3
  192. package/src/core/errors.ts +2 -0
  193. package/src/core/final.ts +4 -0
  194. package/src/core/ll_client.test.ts +15 -2
  195. package/src/core/ll_client.ts +66 -11
  196. package/src/core/transaction.ts +103 -3
  197. package/src/core/unauth_client.ts +17 -2
  198. package/src/core/user_resources.ts +85 -0
  199. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +16 -3
  200. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +335 -12
  201. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.ts +21 -7
  202. package/src/proto-rest/plapi.ts +29 -8
  203. package/dist/proto-grpc/google/rpc/code.cjs +0 -224
  204. package/dist/proto-grpc/google/rpc/code.cjs.map +0 -1
  205. package/dist/proto-grpc/google/rpc/code.js +0 -224
  206. 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,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"}
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"}
@@ -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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"}
@@ -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;AACtD,QAAO,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC"}
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"}
@@ -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;AACtD,QAAO,KAAK,MAAM,OAAO,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC"}
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"}
@@ -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;AACrE,QAAO,iBAAiB,eAAe,YAAY,OAAO,MAAM;;AAGlE,SAAgB,QAAQ,OAAwC;AAC9D,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK,MAAM;UAC/C,oBAAoB,MAAM,CAAE,QAAO;KAEvC,OAAM,IAAI,MAAM,oBAAoB,QAAQ"}
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"}
@@ -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;AACrE,QAAO,iBAAiB,eAAe,YAAY,OAAO,MAAM;;AAGlE,SAAgB,QAAQ,OAAwC;AAC9D,KAAI,OAAO,UAAU,SAAU,QAAO,OAAO,KAAK,MAAM;UAC/C,oBAAoB,MAAM,CAAE,QAAO;KAEvC,OAAM,IAAI,MAAM,oBAAoB,QAAQ"}
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.11.5",
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.5.2",
46
- "@milaboratories/ts-configs": "1.2.3"
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"
@@ -5,8 +5,7 @@
5
5
  * to detect incompatibilities between blocks and backend via
6
6
  * via `TemplateDataV3.requiredCapabilities`.
7
7
  *
8
- * Mirror of `pl/platform/api/plapiserver/server_capabilities.go` — keep
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 = "auth:v2" | "treeFilter:v2" | "wasm:v1";
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(
@@ -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
- * (PR #1830 in milaboratory/pl). See `LLPlClient.supportsWritableWorkdirFiles`
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 {
@@ -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
- const client = await LLPlClient.build(plAddressToTestConfig(cfg.address));
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 tp.setImmediate();
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
  });
@@ -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.updateStatus("Unauthenticated");
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.updateStatus("Unauthenticated");
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 { nonce: pkce.nonce, expiresAt: new Date(pkce.expiresAt) };
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
- * (PR #1830 in milaboratory/pl). Backends before this change ignore the
742
- * requested mode and always land files at the canonical archive perm,
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
- * Tagged at 3.5.0 cut without the change, so [3, 5, 0] excludes the tagged
746
- * release but includes dev builds past the tag (e.g. "3.5.0-224-g0ca182").
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) {