@milaboratories/pl-client 3.9.0 → 3.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,12 +43,13 @@ function plAddressToTestConfig(address) {
43
43
  plConf.defaultRequestTimeout = 500;
44
44
  return plConf;
45
45
  }
46
- function saveAuthInfoCallback(tConf) {
46
+ function saveAuthInfoCallback(tConf, instanceId) {
47
47
  return (authInformation) => {
48
48
  const dst = getFullAuthDataFilePath();
49
49
  const tmpDst = getFullAuthDataFilePath() + (0, node_crypto.randomUUID)();
50
50
  node_fs.writeFileSync(tmpDst, Buffer.from(JSON.stringify({
51
51
  conf: tConf,
52
+ instanceId,
52
53
  authInformation,
53
54
  expiration: require_auth.inferAuthRefreshTime(authInformation, 1440 * 60)
54
55
  })), "utf8");
@@ -64,28 +65,29 @@ const cleanAuthInfoCallback = () => {
64
65
  };
65
66
  async function getTestClientConf() {
66
67
  const tConf = getTestConfig();
68
+ const plConf = plAddressToTestConfig(tConf.address);
69
+ const uClient = await require_unauth_client.UnauthenticatedPlClient.build(plConf);
70
+ const instanceId = uClient.ll.serverInfo.instanceId;
67
71
  let authInformation = void 0;
68
72
  if (node_fs.existsSync(getFullAuthDataFilePath())) try {
69
73
  const cache = JSON.parse(node_fs.readFileSync(getFullAuthDataFilePath(), { encoding: "utf-8" }));
70
- if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now()) authInformation = cache.authInformation;
74
+ if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now() && cache.instanceId === instanceId) authInformation = cache.authInformation;
71
75
  } catch {
72
76
  node_fs.rmSync(getFullAuthDataFilePath());
73
77
  }
74
- const plConf = plAddressToTestConfig(tConf.address);
75
- const uClient = await require_unauth_client.UnauthenticatedPlClient.build(plConf);
76
78
  const requireAuth = await uClient.requireAuth();
77
79
  if (!requireAuth && (tConf.test_user !== void 0 || tConf.test_password !== void 0)) throw new Error(`Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
78
80
  if (requireAuth && (tConf.test_user === void 0 || tConf.test_password === void 0)) throw new Error(`No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
79
81
  if (authInformation === void 0) {
80
82
  if (requireAuth) authInformation = await uClient.login(tConf.test_user, tConf.test_password);
81
83
  else authInformation = {};
82
- saveAuthInfoCallback(tConf)(authInformation);
84
+ saveAuthInfoCallback(tConf, instanceId)(authInformation);
83
85
  }
84
86
  return {
85
87
  conf: plConf,
86
88
  auth: {
87
89
  authInformation,
88
- onUpdate: saveAuthInfoCallback(tConf),
90
+ onUpdate: saveAuthInfoCallback(tConf, instanceId),
89
91
  onAuthError: cleanAuthInfoCallback,
90
92
  onUpdateError: cleanAuthInfoCallback
91
93
  }
@@ -1 +1 @@
1
- {"version":3,"file":"test_config.cjs","names":["path","fs","plAddressToConfig","inferAuthRefreshTime","UnauthenticatedPlClient","LLPlClient","PlClient","startTcpProxy","resourceIdToString"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(tConf: TestConfig): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // try recover from cache\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now()\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,cAAc;AAGpB,IAAI;AAEJ,SAAS,0BAA0B;AACjC,KAAI,qBAAqB,KAAA,EAAW,oBAAmBA,UAAK,QAAQ,kBAAkB;AACtF,QAAO;;AAGT,SAAgB,gBAA4B;CAC1C,IAAI,OAA4B,EAAE;AAClC,KAAIC,QAAG,WAAW,YAAY,CAC5B,QAAO,KAAK,MAAMA,QAAG,aAAa,aAAa,EAAE,UAAU,SAAS,CAAC,CAAC;AAExE,KAAI,QAAQ,IAAI,eAAe,KAAA,EAAW,MAAK,UAAU,QAAQ,IAAI;AAErE,KAAI,QAAQ,IAAI,iBAAiB,KAAA,EAAW,MAAK,YAAY,QAAQ,IAAI;AAEzE,KAAI,QAAQ,IAAI,qBAAqB,KAAA,EAAW,MAAK,gBAAgB,QAAQ,IAAI;AAEjF,KAAI,QAAQ,IAAI,kBAAkB,KAAA,EAAW,MAAK,aAAa,QAAQ,IAAI;AAE3E,KAAI,KAAK,YAAY,KAAA,EACnB,OAAM,IAAI,MACR,2CAA2C,YAAY,uCACxD;AAEH,QAAO;;;AAOT,SAAgB,sBAAsB,SAAiC;CACrE,MAAM,SAASC,eAAAA,kBAAkB,QAAQ;AACzC,QAAO,wBAAA;AACP,QAAO;;AAUT,SAAS,qBAAqB,OAA+D;AAC3F,SAAQ,oBAAoB;EAC1B,MAAM,MAAM,yBAAyB;EACrC,MAAM,SAAS,yBAAyB,IAAA,GAAA,YAAA,aAAe;AACvD,UAAG,cACD,QACA,OAAO,KACL,KAAK,UAAU;GACb,MAAM;GACN;GACA,YAAYC,aAAAA,qBAAqB,iBAAiB,OAAU,GAAG;GAChE,CAAc,CAChB,EACD,OACD;AACD,UAAG,WAAW,QAAQ,IAAI;;;AAI9B,MAAM,8BAA8B;CAClC,MAAM,IAAI,yBAAyB;AACnC,KAAIF,QAAG,WAAW,EAAE,EAAE;AACpB,UAAQ,KAAK,aAAa,IAAI;AAC9B,UAAG,OAAO,EAAE;;;AAIhB,eAAsB,oBAAsE;CAC1F,MAAM,QAAQ,eAAe;CAE7B,IAAI,kBAA+C,KAAA;AAGnD,KAAIA,QAAG,WAAW,yBAAyB,CAAC,CAC1C,KAAI;EACF,MAAM,QAAmB,KAAK,MAC5BA,QAAG,aAAa,yBAAyB,EAAE,EAAE,UAAU,SAAS,CAAC,CAClE;AACD,MACE,MAAM,KAAK,YAAY,MAAM,WAC7B,MAAM,KAAK,cAAc,MAAM,aAC/B,MAAM,KAAK,kBAAkB,MAAM,iBACnC,MAAM,aAAa,KAAK,KAAK,CAE7B,mBAAkB,MAAM;SACpB;AAEN,UAAG,OAAO,yBAAyB,CAAC;;CAIxC,MAAM,SAAS,sBAAsB,MAAM,QAAQ;CACnD,MAAM,UAAU,MAAMG,sBAAAA,wBAAwB,MAAM,OAAO;CAE3D,MAAM,cAAc,MAAM,QAAQ,aAAa;AAE/C,KAAI,CAAC,gBAAgB,MAAM,cAAc,KAAA,KAAa,MAAM,kBAAkB,KAAA,GAC5E,OAAM,IAAI,MACR,iFAAiF,YAAY,uDAC9F;AAEH,KAAI,gBAAgB,MAAM,cAAc,KAAA,KAAa,MAAM,kBAAkB,KAAA,GAC3E,OAAM,IAAI,MACR,wCAAwC,YAAY,uDACrD;AAEH,KAAI,oBAAoB,KAAA,GAAW;AACjC,MAAI,YAAa,mBAAkB,MAAM,QAAQ,MAAM,MAAM,WAAY,MAAM,cAAe;MAEzF,mBAAkB,EAAE;AAGzB,uBAAqB,MAAM,CAAC,gBAAgB;;AAG9C,QAAO;EACL,MAAM;EACN,MAAM;GACJ;GACA,UAAU,qBAAqB,MAAM;GACrC,aAAa;GACb,eAAe;GAChB;EACF;;AAGH,eAAsB,gBAAgB,gBAAyC,EAAE,EAAE;CACjF,MAAM,EAAE,MAAM,SAAS,MAAM,mBAAmB;AAChD,QAAO,MAAMC,kBAAAA,WAAW,MAAM;EAAE,GAAG;EAAM,GAAG;EAAe,EAAE,EAAE,MAAM,CAAC;;AAGxE,eAAsB,cACpB,iBACA,gBAAyC,EAAE,EAC3C;CACA,MAAM,EAAE,MAAM,SAAS,MAAM,mBAAmB;AAChD,KAAI,oBAAoB,KAAA,KAAa,KAAK,oBAAoB,KAAA,EAC5D,OAAM,IAAI,MAAM,mDAAmD;AACrE,QAAO,MAAMC,eAAAA,SAAS,KAAK;EAAE,GAAG;EAAM,GAAG;EAAe;EAAiB,EAAE,KAAK;;AAyBlF,eAAsB,aACpB,MACA,UAA+B,EAAE,EACT;CACxB,MAAM,kBAAkB,QAAQ,KAAK,KAAK,CAAC,IAAA,GAAA,YAAA,aAAe;CAC1D,IAAI,YAAA;CAEJ,IAAI;CACJ,IAAI,gBAAyC,EAAE;AAC/C,KAAI;AAEF,MAAI,QAAQ,gBAAgB,QAAQ,QAAQ,IAAI,WAC9C,KAAI;GACF,MAAM,MAAM,IAAI,IAAI,QAAQ,IAAI,WAAW;GAC3C,MAAM,SAAS,IAAI,aAAa;GAChC,MAAM,UAAU,IAAI,aAAa,eAAe,IAAI,aAAa;GACjE,MAAM,OAAO,SAAS,IAAI,KAAK;AAC/B,OAAI,UAAU,WAAW,OAAO,SAAS,KAAK,EAAE;AAC9C,YAAQ,MAAMC,kBAAAA,cAAc;KAAE,YAAY;KAAM,SAAS,QAAQ,kBAAkB;KAAG,CAAC;AAEvF,oBAAgB,EAAE,aAAa,aAAa,MAAM,QAAQ;UACrD;AACL,YAAQ,KACN,8DACA,QAAQ,IAAI,WACb;AACD;;UAEI;EAKV,MAAM,SAAS,MAAM,cAAc,iBAAiB,cAAc;AAClE,cAAY,OAAO;AACnB,MAAI;GACF,MAAM,QAAQ,MAAM,KAAK,QAAQ,MAAM;GACvC,MAAM,YAAY,MAAM,eAAe;AACvC,OAAI;AACF,UAAM,UAAU,sBAAsB,gBAAgB;YAC/C,YAAiB;AAExB,YAAQ,KAAK,uCAAuC,gBAAgB,IAAI,WAAW,QAAQ;aACnF;AAGR,UAAM,UAAU,OAAO;;AAEzB,UAAO;YACC;AAER,SAAM,OAAO,OAAO;;UAEf,KAAU;AACjB,UAAQ,IAAI,qBAAqB,gBAAgB,IAAIC,cAAAA,mBAAmB,UAAU,CAAC,GAAG;AACtF,QAAM;WAEE;AAER,MAAI,OAAO;AACT,OAAI;AACF,UAAM,MAAM,eAAe;WACrB;AAGR,OAAI;AACF,UAAM,IAAI,SAAe,YAAY,MAAO,OAAO,YAAY,SAAS,CAAC,CAAC;WACpE"}
1
+ {"version":3,"file":"test_config.cjs","names":["path","fs","plAddressToConfig","inferAuthRefreshTime","UnauthenticatedPlClient","LLPlClient","PlClient","startTcpProxy","resourceIdToString"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"test_config.d.ts","names":[],"sources":["../../src/test/test_config.ts"],"mappings":";;;;;;;;;UAgBiB,UAAA;EACf,OAAA;EACA,UAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,iBAac,aAAA,CAAA,GAAiB,UAAA;;cAsBpB,oBAAA;;iBAGG,qBAAA,CAAsB,OAAA,WAAkB,cAAA;AAAA,iBAwClC,iBAAA,CAAA,GAAqB,OAAA;EAAU,IAAA,EAAM,cAAA;EAAgB,IAAA,EAAM,OAAA;AAAA;AAAA,iBA2D3D,eAAA,CAAgB,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,UAAA;AAAA,iBAK3D,aAAA,CACpB,eAAA,WACA,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,QAAA;AAAA,KAQjC,mBAAA;EA1JV;;EA8JI,WAAA,QA5JS;EA8JT,cAAA;AAAA;EAGA,WAAA;AAAA;AAAA,iBAGgB,YAAA,GAAA,CAAgB,IAAA,GAAO,EAAA,EAAI,QAAA,KAAa,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;AAAA,iBAE7D,YAAA,GAAA,CACpB,IAAA,GAAO,EAAA,EAAI,QAAA,EAAU,KAAA,EAAO,OAAA,CAAQ,UAAA,QAAkB,aAAA,OAAoB,OAAA,CAAQ,CAAA,GAClF,OAAA;EACE,WAAA;EACA,cAAA;AAAA,IAED,OAAA,CAAQ,CAAA"}
1
+ {"version":3,"file":"test_config.d.ts","names":[],"sources":["../../src/test/test_config.ts"],"mappings":";;;;;;;;;UAgBiB,UAAA;EACf,OAAA;EACA,UAAA;EACA,SAAA;EACA,aAAA;AAAA;AAAA,iBAac,aAAA,CAAA,GAAiB,UAAA;;cAsBpB,oBAAA;;iBAGG,qBAAA,CAAsB,OAAA,WAAkB,cAAA;AAAA,iBAkDlC,iBAAA,CAAA,GAAqB,OAAA;EAAU,IAAA,EAAM,cAAA;EAAgB,IAAA,EAAM,OAAA;AAAA;AAAA,iBAmE3D,eAAA,CAAgB,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,UAAA;AAAA,iBAK3D,aAAA,CACpB,eAAA,WACA,aAAA,GAAe,OAAA,CAAQ,cAAA,IAAoB,OAAA,CAAA,QAAA;AAAA,KAQjC,mBAAA;EA5KV;;EAgLI,WAAA,QA9KS;EAgLT,cAAA;AAAA;EAGA,WAAA;AAAA;AAAA,iBAGgB,YAAA,GAAA,CAAgB,IAAA,GAAO,EAAA,EAAI,QAAA,KAAa,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;AAAA,iBAE7D,YAAA,GAAA,CACpB,IAAA,GAAO,EAAA,EAAI,QAAA,EAAU,KAAA,EAAO,OAAA,CAAQ,UAAA,QAAkB,aAAA,OAAoB,OAAA,CAAQ,CAAA,GAClF,OAAA;EACE,WAAA;EACA,cAAA;AAAA,IAED,OAAA,CAAQ,CAAA"}
@@ -41,12 +41,13 @@ function plAddressToTestConfig(address) {
41
41
  plConf.defaultRequestTimeout = 500;
42
42
  return plConf;
43
43
  }
44
- function saveAuthInfoCallback(tConf) {
44
+ function saveAuthInfoCallback(tConf, instanceId) {
45
45
  return (authInformation) => {
46
46
  const dst = getFullAuthDataFilePath();
47
47
  const tmpDst = getFullAuthDataFilePath() + randomUUID();
48
48
  fs$1.writeFileSync(tmpDst, Buffer.from(JSON.stringify({
49
49
  conf: tConf,
50
+ instanceId,
50
51
  authInformation,
51
52
  expiration: inferAuthRefreshTime(authInformation, 1440 * 60)
52
53
  })), "utf8");
@@ -62,28 +63,29 @@ const cleanAuthInfoCallback = () => {
62
63
  };
63
64
  async function getTestClientConf() {
64
65
  const tConf = getTestConfig();
66
+ const plConf = plAddressToTestConfig(tConf.address);
67
+ const uClient = await UnauthenticatedPlClient.build(plConf);
68
+ const instanceId = uClient.ll.serverInfo.instanceId;
65
69
  let authInformation = void 0;
66
70
  if (fs$1.existsSync(getFullAuthDataFilePath())) try {
67
71
  const cache = JSON.parse(fs$1.readFileSync(getFullAuthDataFilePath(), { encoding: "utf-8" }));
68
- if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now()) authInformation = cache.authInformation;
72
+ if (cache.conf.address === tConf.address && cache.conf.test_user === tConf.test_user && cache.conf.test_password === tConf.test_password && cache.expiration > Date.now() && cache.instanceId === instanceId) authInformation = cache.authInformation;
69
73
  } catch {
70
74
  fs$1.rmSync(getFullAuthDataFilePath());
71
75
  }
72
- const plConf = plAddressToTestConfig(tConf.address);
73
- const uClient = await UnauthenticatedPlClient.build(plConf);
74
76
  const requireAuth = await uClient.requireAuth();
75
77
  if (!requireAuth && (tConf.test_user !== void 0 || tConf.test_password !== void 0)) throw new Error(`Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
76
78
  if (requireAuth && (tConf.test_user === void 0 || tConf.test_password === void 0)) throw new Error(`No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`);
77
79
  if (authInformation === void 0) {
78
80
  if (requireAuth) authInformation = await uClient.login(tConf.test_user, tConf.test_password);
79
81
  else authInformation = {};
80
- saveAuthInfoCallback(tConf)(authInformation);
82
+ saveAuthInfoCallback(tConf, instanceId)(authInformation);
81
83
  }
82
84
  return {
83
85
  conf: plConf,
84
86
  auth: {
85
87
  authInformation,
86
- onUpdate: saveAuthInfoCallback(tConf),
88
+ onUpdate: saveAuthInfoCallback(tConf, instanceId),
87
89
  onAuthError: cleanAuthInfoCallback,
88
90
  onUpdateError: cleanAuthInfoCallback
89
91
  }
@@ -1 +1 @@
1
- {"version":3,"file":"test_config.js","names":["fs"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(tConf: TestConfig): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // try recover from cache\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now()\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"test_config.js","names":["fs"],"sources":["../../src/test/test_config.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport { LLPlClient } from \"../core/ll_client\";\nimport type { AuthInformation, AuthOps, PlClientConfig } from \"../core/config\";\nimport { plAddressToConfig } from \"../core/config\";\nimport { UnauthenticatedPlClient } from \"../core/unauth_client\";\nimport { PlClient } from \"../core/client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { OptionalSignedResourceId } from \"../core/types\";\nimport { NullSignedResourceId, resourceIdToString } from \"../core/types\";\nimport { inferAuthRefreshTime } from \"../core/auth\";\nimport * as path from \"node:path\";\nimport type { TestTcpProxy } from \"./tcp-proxy\";\nimport { startTcpProxy } from \"./tcp-proxy\";\n\nexport { TestTcpProxy };\n\nexport interface TestConfig {\n address: string;\n test_proxy?: string;\n test_user?: string;\n test_password?: string;\n}\n\nconst CONFIG_FILE = \"test_config.json\";\n// const AUTH_DATA_FILE = '.test_auth.json';\n\nlet authDataFilePath: string | undefined;\n\nfunction getFullAuthDataFilePath() {\n if (authDataFilePath === undefined) authDataFilePath = path.resolve(\".test_auth.json\");\n return authDataFilePath;\n}\n\nexport function getTestConfig(): TestConfig {\n let conf: Partial<TestConfig> = {};\n if (fs.existsSync(CONFIG_FILE))\n conf = JSON.parse(fs.readFileSync(CONFIG_FILE, { encoding: \"utf-8\" })) as TestConfig;\n\n if (process.env.PL_ADDRESS !== undefined) conf.address = process.env.PL_ADDRESS;\n\n if (process.env.PL_TEST_USER !== undefined) conf.test_user = process.env.PL_TEST_USER;\n\n if (process.env.PL_TEST_PASSWORD !== undefined) conf.test_password = process.env.PL_TEST_PASSWORD;\n\n if (process.env.PL_TEST_PROXY !== undefined) conf.test_proxy = process.env.PL_TEST_PROXY;\n\n if (conf.address === undefined)\n throw new Error(\n `can't resolve platform address (checked ${CONFIG_FILE} file and PL_ADDRESS environment var)`,\n );\n\n return conf as TestConfig;\n}\n\n/** Default request timeout for tests (ms) */\nexport const TEST_REQUEST_TIMEOUT = 500;\n\n/** Returns PlClientConfig with reduced timeout for tests */\nexport function plAddressToTestConfig(address: string): PlClientConfig {\n const plConf = plAddressToConfig(address);\n plConf.defaultRequestTimeout = TEST_REQUEST_TIMEOUT;\n return plConf;\n}\n\ninterface AuthCache {\n /** To check if config changed */\n conf: TestConfig;\n /** Backend's instance ID at the moment the JWT was issued. Restarts that\n * reset state rotate this; the JWT's `iss` claim is bound to it and the\n * backend rejects tokens minted by a different instance. Without this\n * check, a cached JWT from a previous backend run would silently fail\n * the first authenticated call after restart. */\n instanceId: string;\n expiration: number;\n authInformation: AuthInformation;\n}\n\nfunction saveAuthInfoCallback(\n tConf: TestConfig,\n instanceId: string,\n): (authInformation: AuthInformation) => void {\n return (authInformation) => {\n const dst = getFullAuthDataFilePath();\n const tmpDst = getFullAuthDataFilePath() + randomUUID();\n fs.writeFileSync(\n tmpDst,\n Buffer.from(\n JSON.stringify({\n conf: tConf,\n instanceId,\n authInformation,\n expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),\n } as AuthCache),\n ),\n \"utf8\",\n );\n fs.renameSync(tmpDst, dst);\n };\n}\n\nconst cleanAuthInfoCallback = () => {\n const p = getFullAuthDataFilePath();\n if (fs.existsSync(p)) {\n console.warn(`Removing: ${p}`);\n fs.rmSync(p);\n }\n};\n\nexport async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {\n const tConf = getTestConfig();\n\n const plConf = plAddressToTestConfig(tConf.address);\n const uClient = await UnauthenticatedPlClient.build(plConf);\n // ll.serverInfo is populated by build()'s ping; instanceId rotates on a\n // backend restart that drops the persisted state.\n const instanceId = uClient.ll.serverInfo.instanceId;\n\n let authInformation: AuthInformation | undefined = undefined;\n\n // Try recover from cache. The cache is keyed by config AND backend\n // instanceId — a backend restart that rotates instanceId invalidates the\n // cached JWT (the token's `iss` claim no longer matches the live\n // instance), so we re-login instead of carrying the dead token through\n // to the first authenticated call.\n if (fs.existsSync(getFullAuthDataFilePath())) {\n try {\n const cache: AuthCache = JSON.parse(\n fs.readFileSync(getFullAuthDataFilePath(), { encoding: \"utf-8\" }),\n ) as AuthCache; // TODO runtime validation\n if (\n cache.conf.address === tConf.address &&\n cache.conf.test_user === tConf.test_user &&\n cache.conf.test_password === tConf.test_password &&\n cache.expiration > Date.now() &&\n cache.instanceId === instanceId\n )\n authInformation = cache.authInformation;\n } catch {\n // removing cache file on any error\n fs.rmSync(getFullAuthDataFilePath());\n }\n }\n\n const requireAuth = await uClient.requireAuth();\n\n if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))\n throw new Error(\n `Server require no auth, but test user name or test password are provided via (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (requireAuth && (tConf.test_user === undefined || tConf.test_password === undefined))\n throw new Error(\n `No auth information found in config (${CONFIG_FILE}) or env variables: PL_TEST_USER and PL_TEST_PASSWORD`,\n );\n\n if (authInformation === undefined) {\n if (requireAuth) authInformation = await uClient.login(tConf.test_user!, tConf.test_password!);\n // No authorization is required\n else authInformation = {};\n\n // saving cache\n saveAuthInfoCallback(tConf, instanceId)(authInformation);\n }\n\n return {\n conf: plConf,\n auth: {\n authInformation,\n onUpdate: saveAuthInfoCallback(tConf, instanceId),\n onAuthError: cleanAuthInfoCallback,\n onUpdateError: cleanAuthInfoCallback,\n },\n };\n}\n\nexport async function getTestLLClient(confOverrides: Partial<PlClientConfig> = {}) {\n const { conf, auth } = await getTestClientConf();\n return await LLPlClient.build({ ...conf, ...confOverrides }, { auth });\n}\n\nexport async function getTestClient(\n alternativeRoot?: string,\n confOverrides: Partial<PlClientConfig> = {},\n) {\n const { conf, auth } = await getTestClientConf();\n if (alternativeRoot !== undefined && conf.alternativeRoot !== undefined)\n throw new Error(\"test pl address configured with alternative root\");\n return await PlClient.init({ ...conf, ...confOverrides, alternativeRoot }, auth);\n}\n\nexport type WithTempRootOptions =\n | {\n /** If true and PL_ADDRESS is http://localhost or http://127.0.0.1:<port>,\n * a TCP proxy will be started and PL client will connect through it. */\n viaTcpProxy: true;\n /** Artificial latency for proxy (ms). Default 0 */\n proxyLatencyMs?: number;\n }\n | {\n viaTcpProxy?: undefined;\n };\n\nexport async function withTempRoot<T>(body: (pl: PlClient) => Promise<T>): Promise<T | void>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: Awaited<ReturnType<typeof startTcpProxy>>) => Promise<T>,\n options: {\n viaTcpProxy: true;\n proxyLatencyMs?: number;\n },\n): Promise<T>;\n\nexport async function withTempRoot<T>(\n body: (pl: PlClient, proxy: any) => Promise<T>,\n options: WithTempRootOptions = {},\n): Promise<T | undefined> {\n const alternativeRoot = `test_${Date.now()}_${randomUUID()}`;\n let altRootId: OptionalSignedResourceId = NullSignedResourceId;\n // Proxy management\n let proxy: Awaited<ReturnType<typeof startTcpProxy>> | undefined;\n let confOverrides: Partial<PlClientConfig> = {};\n try {\n // Optionally start TCP proxy and rewrite PL_ADDRESS to point to proxy\n if (options.viaTcpProxy === true && process.env.PL_ADDRESS) {\n try {\n const url = new URL(process.env.PL_ADDRESS);\n const isHttp = url.protocol === \"http:\";\n const isLocal = url.hostname === \"127.0.0.1\" || url.hostname === \"localhost\";\n const port = parseInt(url.port);\n if (isHttp && isLocal && Number.isFinite(port)) {\n proxy = await startTcpProxy({ targetPort: port, latency: options.proxyLatencyMs ?? 0 });\n // Override client connection host:port to proxy\n confOverrides = { hostAndPort: `127.0.0.1:${proxy.port}` } as Partial<PlClientConfig>;\n } else {\n console.warn(\n \"*** skipping proxy-based test, PL_ADDRESS is not localhost\",\n process.env.PL_ADDRESS,\n );\n return;\n }\n } catch {\n // ignore proxy setup errors; tests will run against original address\n }\n }\n\n const client = await getTestClient(alternativeRoot, confOverrides);\n altRootId = client.clientRoot;\n try {\n const value = await body(client, proxy);\n const rawClient = await getTestClient();\n try {\n await rawClient.deleteAlternativeRoot(alternativeRoot);\n } catch (cleanupErr: any) {\n // Cleanup may fail if test intentionally deleted resources\n console.warn(`Failed to clean up alternative root ${alternativeRoot}:`, cleanupErr.message);\n } finally {\n // Close the cleanup client to avoid dangling gRPC channels that can cause\n // segfaults during process exit\n await rawClient.close();\n }\n return value;\n } finally {\n // Close the test client to avoid dangling gRPC channels\n await client.close();\n }\n } catch (err: any) {\n console.log(`ALTERNATIVE ROOT: ${alternativeRoot} (${resourceIdToString(altRootId)})`);\n throw err;\n // throw new Error('withTempRoot error: ' + err.message, { cause: err });\n } finally {\n // Stop proxy if started\n if (proxy) {\n try {\n await proxy.disconnectAll();\n } catch {\n /* ignore */\n }\n try {\n await new Promise<void>((resolve) => proxy!.server.close(() => resolve()));\n } catch {\n /* ignore */\n }\n }\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-client",
3
- "version": "3.9.0",
3
+ "version": "3.9.1",
4
4
  "description": "New TS/JS client for Platform API",
5
5
  "files": [
6
6
  "./dist/**/*",
@@ -31,8 +31,8 @@
31
31
  "utility-types": "^3.11.0",
32
32
  "yaml": "^2.8.0",
33
33
  "@milaboratories/pl-http": "1.2.4",
34
- "@milaboratories/ts-helpers": "1.8.2",
35
- "@milaboratories/pl-model-common": "1.42.0"
34
+ "@milaboratories/pl-model-common": "1.42.0",
35
+ "@milaboratories/ts-helpers": "1.8.2"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@protobuf-ts/plugin": "2.11.1",
@@ -41,9 +41,9 @@
41
41
  "openapi-typescript": "^7.10.0",
42
42
  "typescript": "~5.9.3",
43
43
  "vitest": "^4.1.3",
44
+ "@milaboratories/ts-configs": "1.2.3",
44
45
  "@milaboratories/ts-builder": "1.5.0",
45
- "@milaboratories/build-configs": "2.0.0",
46
- "@milaboratories/ts-configs": "1.2.3"
46
+ "@milaboratories/build-configs": "2.0.0"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=22.19.0"
@@ -65,11 +65,20 @@ export function plAddressToTestConfig(address: string): PlClientConfig {
65
65
  interface AuthCache {
66
66
  /** To check if config changed */
67
67
  conf: TestConfig;
68
+ /** Backend's instance ID at the moment the JWT was issued. Restarts that
69
+ * reset state rotate this; the JWT's `iss` claim is bound to it and the
70
+ * backend rejects tokens minted by a different instance. Without this
71
+ * check, a cached JWT from a previous backend run would silently fail
72
+ * the first authenticated call after restart. */
73
+ instanceId: string;
68
74
  expiration: number;
69
75
  authInformation: AuthInformation;
70
76
  }
71
77
 
72
- function saveAuthInfoCallback(tConf: TestConfig): (authInformation: AuthInformation) => void {
78
+ function saveAuthInfoCallback(
79
+ tConf: TestConfig,
80
+ instanceId: string,
81
+ ): (authInformation: AuthInformation) => void {
73
82
  return (authInformation) => {
74
83
  const dst = getFullAuthDataFilePath();
75
84
  const tmpDst = getFullAuthDataFilePath() + randomUUID();
@@ -78,6 +87,7 @@ function saveAuthInfoCallback(tConf: TestConfig): (authInformation: AuthInformat
78
87
  Buffer.from(
79
88
  JSON.stringify({
80
89
  conf: tConf,
90
+ instanceId,
81
91
  authInformation,
82
92
  expiration: inferAuthRefreshTime(authInformation, 24 * 60 * 60),
83
93
  } as AuthCache),
@@ -99,9 +109,19 @@ const cleanAuthInfoCallback = () => {
99
109
  export async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth: AuthOps }> {
100
110
  const tConf = getTestConfig();
101
111
 
112
+ const plConf = plAddressToTestConfig(tConf.address);
113
+ const uClient = await UnauthenticatedPlClient.build(plConf);
114
+ // ll.serverInfo is populated by build()'s ping; instanceId rotates on a
115
+ // backend restart that drops the persisted state.
116
+ const instanceId = uClient.ll.serverInfo.instanceId;
117
+
102
118
  let authInformation: AuthInformation | undefined = undefined;
103
119
 
104
- // try recover from cache
120
+ // Try recover from cache. The cache is keyed by config AND backend
121
+ // instanceId — a backend restart that rotates instanceId invalidates the
122
+ // cached JWT (the token's `iss` claim no longer matches the live
123
+ // instance), so we re-login instead of carrying the dead token through
124
+ // to the first authenticated call.
105
125
  if (fs.existsSync(getFullAuthDataFilePath())) {
106
126
  try {
107
127
  const cache: AuthCache = JSON.parse(
@@ -111,7 +131,8 @@ export async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth:
111
131
  cache.conf.address === tConf.address &&
112
132
  cache.conf.test_user === tConf.test_user &&
113
133
  cache.conf.test_password === tConf.test_password &&
114
- cache.expiration > Date.now()
134
+ cache.expiration > Date.now() &&
135
+ cache.instanceId === instanceId
115
136
  )
116
137
  authInformation = cache.authInformation;
117
138
  } catch {
@@ -120,9 +141,6 @@ export async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth:
120
141
  }
121
142
  }
122
143
 
123
- const plConf = plAddressToTestConfig(tConf.address);
124
- const uClient = await UnauthenticatedPlClient.build(plConf);
125
-
126
144
  const requireAuth = await uClient.requireAuth();
127
145
 
128
146
  if (!requireAuth && (tConf.test_user !== undefined || tConf.test_password !== undefined))
@@ -141,14 +159,14 @@ export async function getTestClientConf(): Promise<{ conf: PlClientConfig; auth:
141
159
  else authInformation = {};
142
160
 
143
161
  // saving cache
144
- saveAuthInfoCallback(tConf)(authInformation);
162
+ saveAuthInfoCallback(tConf, instanceId)(authInformation);
145
163
  }
146
164
 
147
165
  return {
148
166
  conf: plConf,
149
167
  auth: {
150
168
  authInformation,
151
- onUpdate: saveAuthInfoCallback(tConf),
169
+ onUpdate: saveAuthInfoCallback(tConf, instanceId),
152
170
  onAuthError: cleanAuthInfoCallback,
153
171
  onUpdateError: cleanAuthInfoCallback,
154
172
  },