@things-factory/integration-base 10.0.0-beta.90 → 10.0.0-beta.95

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 (48) hide show
  1. package/dist-server/engine/connection-manager.d.ts +19 -0
  2. package/dist-server/engine/connection-manager.js +148 -16
  3. package/dist-server/engine/connection-manager.js.map +1 -1
  4. package/dist-server/engine/connector/headless-connector.d.ts +12 -0
  5. package/dist-server/engine/connector/headless-connector.js +74 -31
  6. package/dist-server/engine/connector/headless-connector.js.map +1 -1
  7. package/dist-server/engine/evaluate-template.d.ts +22 -3
  8. package/dist-server/engine/evaluate-template.js +43 -4
  9. package/dist-server/engine/evaluate-template.js.map +1 -1
  10. package/dist-server/engine/task/script.js +25 -17
  11. package/dist-server/engine/task/script.js.map +1 -1
  12. package/dist-server/engine/types.d.ts +32 -0
  13. package/dist-server/engine/types.js.map +1 -1
  14. package/dist-server/service/connection/connection-mutation.d.ts +3 -0
  15. package/dist-server/service/connection/connection-type.d.ts +4 -1
  16. package/dist-server/service/connection/connection-type.js +21 -0
  17. package/dist-server/service/connection/connection-type.js.map +1 -1
  18. package/dist-server/service/connection/connection.d.ts +80 -2
  19. package/dist-server/service/connection/connection.js +59 -15
  20. package/dist-server/service/connection/connection.js.map +1 -1
  21. package/dist-server/service/domain-attribute/domain-attribute-query.d.ts +17 -0
  22. package/dist-server/service/domain-attribute/domain-attribute-query.js +76 -0
  23. package/dist-server/service/domain-attribute/domain-attribute-query.js.map +1 -0
  24. package/dist-server/service/domain-attribute/domain-attribute-type.d.ts +15 -0
  25. package/dist-server/service/domain-attribute/domain-attribute-type.js +46 -0
  26. package/dist-server/service/domain-attribute/domain-attribute-type.js.map +1 -0
  27. package/dist-server/service/domain-attribute/index.d.ts +3 -0
  28. package/dist-server/service/domain-attribute/index.js +8 -0
  29. package/dist-server/service/domain-attribute/index.js.map +1 -0
  30. package/dist-server/service/index.d.ts +1 -1
  31. package/dist-server/service/index.js +3 -1
  32. package/dist-server/service/index.js.map +1 -1
  33. package/dist-server/service/scenario/scenario-mutation.js +10 -0
  34. package/dist-server/service/scenario/scenario-mutation.js.map +1 -1
  35. package/dist-server/service/scenario/scenario-query.d.ts +3 -3
  36. package/dist-server/service/scenario/scenario-query.js +9 -9
  37. package/dist-server/service/scenario/scenario-query.js.map +1 -1
  38. package/dist-server/service/scenario-instance/scenario-instance-type.js +79 -0
  39. package/dist-server/service/scenario-instance/scenario-instance-type.js.map +1 -1
  40. package/dist-server/service/step/step-mutation.js +15 -0
  41. package/dist-server/service/step/step-mutation.js.map +1 -1
  42. package/dist-server/service/step/step-query.js +11 -2
  43. package/dist-server/service/step/step-query.js.map +1 -1
  44. package/dist-server/tsconfig.tsbuildinfo +1 -1
  45. package/dist-server/utils/domain-inheritance.d.ts +27 -0
  46. package/dist-server/utils/domain-inheritance.js +67 -0
  47. package/dist-server/utils/domain-inheritance.js.map +1 -1
  48. package/package.json +10 -10
@@ -5,6 +5,12 @@ export declare class ConnectionManager {
5
5
  private static connectors;
6
6
  private static connections;
7
7
  private static entities;
8
+ /**
9
+ * Session 직렬화 mutex — connection name 단위 (도메인 무관 글로벌).
10
+ * sessionExclusive connector 의 acquireSessionPage 동시 호출이 외부 세션을 서로
11
+ * invalidate 하지 않도록 큐잉. release 호출 시 다음 대기자가 진행.
12
+ */
13
+ private static sessionLocks;
8
14
  private static logFormat;
9
15
  static logger: import("winston").Logger;
10
16
  static ready(): Promise<void>;
@@ -21,6 +27,19 @@ export declare class ConnectionManager {
21
27
  };
22
28
  static getEntities(): {};
23
29
  static getConnectionInstance(connection: Connection): any;
30
+ /**
31
+ * 세션 사용 패턴 (RAII).
32
+ *
33
+ * connector 가 sessionExclusive=true 로 선언한 경우 connection.name 단위 mutex 로
34
+ * 직렬화. 호출자는 acquireSessionPage / releasePage 의 lifecycle 을 신경 쓸 필요 없이
35
+ * `withSession(connection, async page => { ... })` 형태로 사용하면 됨.
36
+ *
37
+ * stateless connector (sessionExclusive=false) 는 mutex 없이 즉시 진행.
38
+ *
39
+ * @param connection 도메인 instance (connection.type 에서 connector 조회)
40
+ * @param fn page 를 받아 데이터 작업하는 async 함수
41
+ */
42
+ static withSession<T>(connection: any, fn: (page: any) => Promise<T>): Promise<T>;
24
43
  static getConnectionInstanceByName(domain: Domain, name: string): Promise<any>;
25
44
  static getConnectionEntityByName(domain: Domain, name: string): Promise<Connection | null>;
26
45
  static getConnectionInstanceEntityByName(domain: Domain, name: string): any;
@@ -6,6 +6,7 @@ const moment_timezone_1 = tslib_1.__importDefault(require("moment-timezone"));
6
6
  const winston_1 = require("winston");
7
7
  const shell_1 = require("@things-factory/shell");
8
8
  const service_1 = require("../service");
9
+ const domain_inheritance_1 = require("../utils/domain-inheritance");
9
10
  const proxy_connector_1 = require("./connector/proxy-connector");
10
11
  const { combine, splat, printf, errors } = winston_1.format;
11
12
  const debug = require('debug')('things-factory:integration-base:connections');
@@ -32,6 +33,12 @@ class ConnectionManager {
32
33
  static { this.connectors = {}; }
33
34
  static { this.connections = {}; }
34
35
  static { this.entities = {}; }
36
+ /**
37
+ * Session 직렬화 mutex — connection name 단위 (도메인 무관 글로벌).
38
+ * sessionExclusive connector 의 acquireSessionPage 동시 호출이 외부 세션을 서로
39
+ * invalidate 하지 않도록 큐잉. release 호출 시 다음 대기자가 진행.
40
+ */
41
+ static { this.sessionLocks = new Map(); }
35
42
  static { this.logFormat = printf(({ level, message, timestamp, stack }) => {
36
43
  return `${timestamp} ${level}: ${stack || message}`;
37
44
  }); }
@@ -114,20 +121,106 @@ class ConnectionManager {
114
121
  const { domain, name } = connection;
115
122
  return ConnectionManager.connections[domain.id]?.[name];
116
123
  }
124
+ /**
125
+ * 세션 사용 패턴 (RAII).
126
+ *
127
+ * connector 가 sessionExclusive=true 로 선언한 경우 connection.name 단위 mutex 로
128
+ * 직렬화. 호출자는 acquireSessionPage / releasePage 의 lifecycle 을 신경 쓸 필요 없이
129
+ * `withSession(connection, async page => { ... })` 형태로 사용하면 됨.
130
+ *
131
+ * stateless connector (sessionExclusive=false) 는 mutex 없이 즉시 진행.
132
+ *
133
+ * @param connection 도메인 instance (connection.type 에서 connector 조회)
134
+ * @param fn page 를 받아 데이터 작업하는 async 함수
135
+ */
136
+ static async withSession(connection, fn) {
137
+ const connector = ConnectionManager.getConnector(connection.type);
138
+ const exclusive = !!connector?.sessionExclusive;
139
+ const run = async () => {
140
+ const page = await connection.acquireSessionPage();
141
+ try {
142
+ return await fn(page);
143
+ }
144
+ finally {
145
+ // releasePage 가 page 인자를 받음 — 누락 시 내부의 page.close() 와
146
+ // browser 풀 반환이 스킵되어 브라우저가 leak. 풀 고갈 → 정상 종료 hang 으로 이어짐.
147
+ try {
148
+ await connection.releasePage?.(page);
149
+ }
150
+ catch (e) {
151
+ ConnectionManager.logger.warn(`releasePage failed for '${connection.name}':`, e);
152
+ }
153
+ }
154
+ };
155
+ if (!exclusive)
156
+ return await run();
157
+ // mutex: connection name 단위로 직렬화 (도메인 무관, 같은 외부 계정 중복 로그인 방지)
158
+ const lockKey = connection.name;
159
+ const wasQueued = ConnectionManager.sessionLocks.has(lockKey);
160
+ const prev = ConnectionManager.sessionLocks.get(lockKey) || Promise.resolve();
161
+ let release;
162
+ const next = new Promise(r => (release = r));
163
+ // chain: 이전 작업이 끝난 뒤 next 가 끝날 때까지 pending — 다음 대기자가 이 chain 으로 await
164
+ const chain = prev.then(() => next);
165
+ ConnectionManager.sessionLocks.set(lockKey, chain);
166
+ // 큐 대기 진단 — 운영 중 "왜 작업이 stall 되는가" 추적용
167
+ const waitStartedAt = wasQueued ? Date.now() : 0;
168
+ if (wasQueued) {
169
+ ConnectionManager.logger.info(`[withSession] '${lockKey}' queued behind existing lock — waiting (domain='${connection.domain?.subdomain || connection.domainId}')`);
170
+ }
171
+ await prev;
172
+ if (wasQueued) {
173
+ const waited = Date.now() - waitStartedAt;
174
+ ConnectionManager.logger.info(`[withSession] '${lockKey}' lock acquired after ${waited}ms wait`);
175
+ }
176
+ try {
177
+ return await run();
178
+ }
179
+ finally {
180
+ release();
181
+ // 내가 latest 면 (이 chain 뒤로 새로 들어온 대기자 없음) cleanup
182
+ if (ConnectionManager.sessionLocks.get(lockKey) === chain) {
183
+ ConnectionManager.sessionLocks.delete(lockKey);
184
+ }
185
+ }
186
+ }
117
187
  static async getConnectionInstanceByName(domain, name) {
118
188
  const connections = ConnectionManager.connections[domain.id];
119
189
  const connection = connections?.[name];
120
190
  if (!connection) {
121
- // on-demand 커넥션인지 확인하고 생성
191
+ // 자식 도메인에 instance 가 없을 때의 세 가지 자동 처리 분기:
192
+ //
193
+ // (a) entity.__sharedFrom 있음 — SHARE 모드 inheritance
194
+ // → 부모 도메인의 instance 를 그대로 lookup 해서 반환. 자식 도메인에는 instance 등록 안 함.
195
+ // 같은 instance 가 부모·자식 모두에게 서비스 됨.
196
+ //
197
+ // (b) entity.__inheritedFrom 있음 — ISOLATE 모드 inheritance
198
+ // → 자식 도메인 키 아래 새 instance 자동 생성. cookies/세션 격리.
199
+ //
200
+ // (c) entity.onDemand=true — 명시적 lazy 생성 (자식 자체 record 인 경우에도 적용)
201
+ // → 자식 도메인 키 아래 새 instance 생성.
202
+ //
203
+ // 나머지: 자식 record 도 없고 inherit 도 안 되는 경우 → throw.
122
204
  try {
123
205
  const connectionEntity = await ConnectionManager.getConnectionEntityByName(domain, name);
124
- if (connectionEntity?.onDemand) {
125
- // on-demand 커넥션 생성
126
- const onDemandInstance = await ConnectionManager.createOnDemandConnection(connectionEntity);
127
- return onDemandInstance;
206
+ if (connectionEntity?.__sharedFrom) {
207
+ // SHARE: 부모 instance 직접 사용
208
+ const parentInstance = ConnectionManager.connections[connectionEntity.__sharedFrom]?.[name];
209
+ if (parentInstance)
210
+ return parentInstance;
211
+ throw `SHARE mode: parent instance for '${name}' (domain id '${connectionEntity.__sharedFrom}') not registered yet — startup ready() may have failed for parent`;
212
+ }
213
+ if (connectionEntity?.onDemand || connectionEntity?.__inheritedFrom) {
214
+ const instance = await ConnectionManager.createOnDemandConnection(connectionEntity);
215
+ return instance;
216
+ }
217
+ else if (connectionEntity) {
218
+ // entity 는 찾았는데 instance 생성 trigger (onDemand/inherit) 없음 — 운영자가 record 자체 점검 필요
219
+ throw `Connection '${name}' record exists but instance is not registered and no auto-create trigger (onDemand=false, no inheritance marker). Check whether parent record's active=true and ready() succeeded at startup.`;
128
220
  }
129
221
  else {
130
- throw `The connection with the given name(${name}) cannot be found`;
222
+ // entity 자체 찾음 도메인 트리 어디에도 record 없음
223
+ throw `Connection '${name}' not found in domain '${domain.subdomain || domain.id}' or any ancestor — verify the Connection record exists at the running domain or any of its ancestors.`;
131
224
  }
132
225
  }
133
226
  catch (error) {
@@ -163,22 +256,61 @@ class ConnectionManager {
163
256
  // return parentCachedEntity
164
257
  // }
165
258
  // }
166
- // 4. 부모 도메인에서 데이터베이스 조회 (fallback)
167
- if (domain.parentId) {
168
- connection = await (0, shell_1.getRepository)(service_1.Connection).findOne({
259
+ // 4. ancestor 도메인 트리 fallback (closest-first walk)
260
+ //
261
+ // EnvVar inheritance 일관되게 모든 조상 도메인을 closest-first 로 탐색.
262
+ // 예: SYSTEM → 시공사 → 프로젝트 트리에서 시나리오가 프로젝트 도메인에서 실행될 때
263
+ // Connection record 가 SYSTEM 또는 시공사 어디에 있어도 찾아냄.
264
+ //
265
+ // 매칭된 ancestor 의 inheritanceMode 에 따라 두 갈래:
266
+ // ISOLATE (default): 자식 도메인용 clone 생성. cookies/세션 격리, 자식 컨텍스트에서
267
+ // EnvVar override 해소. __inheritedFrom marker 로 후속 자동 instance.
268
+ // SHARE: 부모 entity 그대로 반환. params·세션·인스턴스 모두 부모 것 공유.
269
+ // __sharedFrom marker 로 instance lookup 이 부모 도메인을 가리키게 함.
270
+ //
271
+ // 호환성: inheritanceMode 미지정(NULL) → connector default → 폴백 ISOLATE.
272
+ // 기존 모든 Connection 은 이 column 추가 전 만들어졌으므로 NULL → ISOLATE = 기존 거동.
273
+ const ancestorIds = await (0, domain_inheritance_1.getDomainIdsWithAncestors)(domain);
274
+ // ancestorIds[0] = domain.id (이미 위 분기에서 시도), skip
275
+ const searchedAncestors = [];
276
+ for (let i = 1; i < ancestorIds.length; i++) {
277
+ const ancestorId = ancestorIds[i];
278
+ searchedAncestors.push(ancestorId);
279
+ const parentConn = await (0, shell_1.getRepository)(service_1.Connection).findOne({
169
280
  where: {
170
- domain: { id: domain.parentId },
281
+ domain: { id: ancestorId },
171
282
  name: name
172
283
  },
173
284
  relations: ['domain', 'edge', 'creator', 'updater']
174
285
  });
175
- if (connection) {
176
- const resolvedParams = await connection.getResolvedParameters();
177
- connection.params = resolvedParams;
178
- connection.domain = domain;
179
- connection.domainId = domain.id;
180
- return connection;
286
+ if (!parentConn)
287
+ continue; // ancestor 에 없으면 다음 ancestor
288
+ ConnectionManager.logger.info(`[inheritance] '${name}' inherited by domain '${domain.subdomain || domain.id}' ` +
289
+ `from ancestor '${parentConn.domain?.subdomain || ancestorId}' ` +
290
+ `(walked ${i} ancestor(s))`);
291
+ const connectorDefault = ConnectionManager.connectors[parentConn.type]?.inheritanceMode;
292
+ const effectiveMode = parentConn.inheritanceMode || connectorDefault || service_1.ConnectionInheritanceMode.ISOLATE;
293
+ if (effectiveMode === service_1.ConnectionInheritanceMode.SHARE) {
294
+ // SHARE — 부모 entity 그대로 반환 (mutation 최소화).
295
+ const shared = parentConn;
296
+ shared.__sharedFrom = parentConn.domainId;
297
+ shared.params = await parentConn.getResolvedParameters();
298
+ return shared;
181
299
  }
300
+ // ISOLATE (default) — clone + marker
301
+ const inherited = Object.assign(Object.create(Object.getPrototypeOf(parentConn)), parentConn, {
302
+ domain,
303
+ domainId: domain.id,
304
+ cookies: undefined,
305
+ __inheritedFrom: parentConn.domainId
306
+ });
307
+ inherited.params = await inherited.getResolvedParameters();
308
+ return inherited;
309
+ }
310
+ // 어느 ancestor 에서도 못 찾음 — 운영자가 어디까지 찾았는지 알 수 있도록 진단 로그
311
+ if (searchedAncestors.length > 0) {
312
+ ConnectionManager.logger.warn(`[inheritance] '${name}' not found at any ancestor of domain '${domain.subdomain || domain.id}' ` +
313
+ `— searched: [${searchedAncestors.join(', ')}]`);
182
314
  }
183
315
  return null;
184
316
  }
@@ -1 +1 @@
1
- {"version":3,"file":"connection-manager.js","sourceRoot":"","sources":["../../server/engine/connection-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import moment from 'moment-timezone'\nimport { createLogger, format, transports } from 'winston'\n\nimport { Domain, getRepository, pubsub, PubSubLogTransport } from '@things-factory/shell'\n\nimport { Connection, ConnectionStatus } from '../service'\nimport { Connector } from './types'\nimport { ProxyConnector } from './connector/proxy-connector'\n\nconst { combine, splat, printf, errors } = format\nconst debug = require('debug')('things-factory:integration-base:connections')\n\nfunction getSystemTimeZone() {\n try {\n const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone\n if (!timeZone) {\n throw new Error('Unable to resolve timeZone')\n }\n return timeZone\n } catch (e) {\n console.warn('Failed to get system timeZone, falling back to UTC.', e)\n return 'UTC'\n }\n}\n\nconst SYSTEM_TZ = getSystemTimeZone()\nconst systemTimestamp = format((info, opts: { tz?: string }) => {\n if (opts.tz) info.timestamp = moment().tz(opts.tz).format()\n return info\n})\n\nexport class ConnectionManager {\n private static connectors: { [propName: string]: Connector } = {}\n private static connections: { [domainId: string]: { [name: string]: any } } = {}\n private static entities = {}\n private static logFormat = printf(({ level, message, timestamp, stack }) => {\n return `${timestamp} ${level}: ${stack || message}`\n })\n\n public static logger = createLogger({\n format: combine(errors({ stack: true }), systemTimestamp({ tz: SYSTEM_TZ }), splat(), ConnectionManager.logFormat),\n transports: [\n new (transports as any).DailyRotateFile({\n filename: `logs/connections-%DATE%.log`,\n datePattern: 'YYYY-MM-DD-HH',\n zippedArchive: false,\n maxSize: '20m',\n maxFiles: '14d',\n level: 'info'\n }),\n new PubSubLogTransport({\n topic: 'connection-log'\n })\n ]\n })\n\n static async ready() {\n const CONNECTIONS = await Promise.all(\n (\n await getRepository(Connection).find({\n where: { active: true },\n relations: ['domain', 'edge', 'creator', 'updater']\n })\n ).map(async connection => {\n // 🔐 해결된 파라미터 사용\n const resolvedParams = await connection.getResolvedParameters()\n\n return {\n ...connection,\n params: resolvedParams\n }\n })\n )\n\n ConnectionManager.logger.info('Initializing ConnectionManager...')\n\n return await Promise.all(\n [...Object.keys(ConnectionManager.connectors), 'proxy-connector'].map(type => {\n const connector = type == 'proxy-connector' ? ProxyConnector.instance : ConnectionManager.getConnector(type)\n\n ConnectionManager.logger.info(`Connector '${type}' started to ready`)\n\n return connector\n .ready(\n CONNECTIONS.filter(connection => {\n if (type == 'proxy-connector') {\n return !!connection.edge\n } else {\n return !connection.edge && connection.type == type\n }\n }) as any\n )\n .catch(error => {\n ConnectionManager.logger.error(error)\n })\n .then(() => {\n ConnectionManager.logger.info(`All connector for '${type}' ready`)\n })\n })\n ).then(() => {\n ConnectionManager.logger.info('ConnectionManager initialization done:')\n Object.keys(ConnectionManager.connections).forEach(key => {\n var connections = ConnectionManager.connections[key]\n ConnectionManager.logger.info('For domain(%s) : %s', key, JSON.stringify(Object.keys(connections)))\n })\n })\n }\n\n static registerConnector(type: string, connector: Connector) {\n ConnectionManager.connectors[type] = connector\n }\n\n static getConnector(type: string): Connector {\n return ConnectionManager.connectors[type]\n }\n\n static getConnectors(): { [connectorName: string]: Connector } {\n return {\n ...ConnectionManager.connectors\n }\n }\n\n static unregisterConnector(type: string) {\n delete ConnectionManager.connectors[type]\n }\n\n static getConnections() {\n return ConnectionManager.connections\n }\n\n static getEntities() {\n return ConnectionManager.entities\n }\n\n static getConnectionInstance(connection: Connection): any {\n const { domain, name } = connection\n return ConnectionManager.connections[domain.id]?.[name]\n }\n\n static async getConnectionInstanceByName(domain: Domain, name: string) {\n const connections = ConnectionManager.connections[domain.id]\n const connection = connections?.[name]\n\n if (!connection) {\n // on-demand 커넥션인지 확인하고 생성\n try {\n const connectionEntity = await ConnectionManager.getConnectionEntityByName(domain, name)\n\n if (connectionEntity?.onDemand) {\n // on-demand 커넥션 생성\n const onDemandInstance = await ConnectionManager.createOnDemandConnection(connectionEntity)\n return onDemandInstance\n } else {\n throw `The connection with the given name(${name}) cannot be found`\n }\n } catch (error) {\n throw `The connection with the given name(${name}) cannot be found: ${error}`\n }\n }\n\n return connection\n }\n\n static async getConnectionEntityByName(domain: Domain, name: string): Promise<Connection | null> {\n // 1. 현재 도메인에서 메모리 조회 (우선)\n const cachedEntity = ConnectionManager.entities[domain.id]?.[name]\n if (cachedEntity) {\n return cachedEntity\n }\n\n // 2. 현재 도메인에서 데이터베이스 조회 (우선)\n try {\n let connection = await getRepository(Connection).findOne({\n where: {\n domain: { id: domain.id },\n name: name\n },\n relations: ['domain', 'edge', 'creator', 'updater']\n })\n\n if (connection) {\n const resolvedParams = await connection.getResolvedParameters()\n connection.params = resolvedParams\n return connection\n }\n\n // 3. 현재 도메인에서 못 찾으면 부모 도메인에서 메모리 조회 - 안된다. connection정보를 수정해야 하므로 사용할 수 없다.\n // if (domain.parentId) {\n // const parentCachedEntity = ConnectionManager.entities[domain.parentId]?.[name]\n // if (parentCachedEntity) {\n // return parentCachedEntity\n // }\n // }\n\n // 4. 부모 도메인에서 데이터베이스 조회 (fallback)\n if (domain.parentId) {\n connection = await getRepository(Connection).findOne({\n where: {\n domain: { id: domain.parentId },\n name: name\n },\n relations: ['domain', 'edge', 'creator', 'updater']\n })\n\n if (connection) {\n const resolvedParams = await connection.getResolvedParameters()\n connection.params = resolvedParams\n connection.domain = domain\n connection.domainId = domain.id\n\n return connection\n }\n }\n\n return null\n } catch (error) {\n ConnectionManager.logger.error(`Failed to get connection entity '${name}' from database:`, error)\n return null\n }\n }\n\n static getConnectionInstanceEntityByName(domain: Domain, name: string): any {\n const connection = ConnectionManager.entities[domain.id]?.[name]\n if (connection) {\n return connection\n }\n\n if (domain.parentId) {\n return ConnectionManager.entities[domain.parentId]?.[name]\n }\n }\n\n /**\n * Creates a connection on-demand and returns the instance.\n * @param connection - The connection entity to create\n * @returns The connection instance\n */\n static async createOnDemandConnection(connection: Connection): Promise<any> {\n try {\n // 커넥션 생성\n await connection.connect()\n\n ConnectionManager.logger.info(`On-demand connection '${connection.name}' created successfully`)\n return ConnectionManager.getConnectionInstance(connection)\n } catch (error) {\n ConnectionManager.logger.error(`Failed to create on-demand connection '${connection.name}':`, error)\n throw error\n }\n }\n\n /**\n * Disconnects an on-demand connection.\n * @param connection - The connection entity to disconnect\n */\n static async disconnectOnDemandConnection(connection: Connection): Promise<void> {\n try {\n await connection.disconnect()\n // ConnectionManager.removeConnectionInstance(connection)\n ConnectionManager.logger.info(`On-demand connection '${connection.name}' disconnected successfully`)\n } catch (error) {\n ConnectionManager.logger.error(`Failed to disconnect on-demand connection '${connection.name}':`, error)\n }\n }\n\n static getConnectionInstances(domain: Domain): { [connectionName: string]: any } {\n const connections = ConnectionManager.connections[domain.id]\n const parentConnections = domain.parentId && ConnectionManager.connections[domain.parentId]\n\n return {\n ...parentConnections,\n ...connections\n }\n }\n\n static getConnectionInstanceEntities(domain: Domain): { [connectionName: string]: any } {\n const connections = ConnectionManager.entities[domain.id]\n\n return {\n ...connections\n }\n }\n\n static addConnectionInstance(connection: Connection, instance: any) {\n const { domain, name } = connection\n\n var connections = ConnectionManager.connections[domain.id]\n if (!connections) {\n connections = ConnectionManager.connections[domain.id] = {}\n }\n\n var entities = ConnectionManager.entities[domain.id]\n if (!entities) {\n entities = ConnectionManager.entities[domain.id] = {}\n }\n\n connections[name] = instance\n entities[name] = connection\n\n ConnectionManager.publishState(connection, ConnectionStatus.CONNECTED)\n debug('add-connection', domain.subdomain, name)\n }\n\n static removeConnectionInstance(connection: Connection): any {\n const { domain, name } = connection\n var connections = ConnectionManager.connections[domain.id]\n var entities = ConnectionManager.entities[domain.id]\n\n var instance = connections?.[name]\n\n if (!connections || !instance) {\n debug('remove-connection', `'${name}' connection not found in domain '${domain.subdomain}'`)\n return\n }\n\n delete connections[name]\n delete entities[name]\n\n ConnectionManager.publishState(connection, ConnectionStatus.DISCONNECTED)\n debug('remove-connection', `'${name}' connection is removed from domain '${domain.subdomain}'`)\n\n return instance\n }\n\n private static async publishState(connection: Connection, state) {\n const { domain, id, name, description, type, edge } = connection\n\n pubsub.publish('connection-state', {\n connectionState: {\n domain,\n id,\n name,\n description,\n type,\n edge,\n state,\n timestamp: new Date()\n }\n })\n }\n}\n"]}
1
+ {"version":3,"file":"connection-manager.js","sourceRoot":"","sources":["../../server/engine/connection-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import moment from 'moment-timezone'\nimport { createLogger, format, transports } from 'winston'\n\nimport { Domain, getRepository, pubsub, PubSubLogTransport } from '@things-factory/shell'\n\nimport { Connection, ConnectionInheritanceMode, ConnectionStatus } from '../service'\nimport { getDomainIdsWithAncestors } from '../utils/domain-inheritance'\nimport { Connector } from './types'\nimport { ProxyConnector } from './connector/proxy-connector'\n\nconst { combine, splat, printf, errors } = format\nconst debug = require('debug')('things-factory:integration-base:connections')\n\nfunction getSystemTimeZone() {\n try {\n const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone\n if (!timeZone) {\n throw new Error('Unable to resolve timeZone')\n }\n return timeZone\n } catch (e) {\n console.warn('Failed to get system timeZone, falling back to UTC.', e)\n return 'UTC'\n }\n}\n\nconst SYSTEM_TZ = getSystemTimeZone()\nconst systemTimestamp = format((info, opts: { tz?: string }) => {\n if (opts.tz) info.timestamp = moment().tz(opts.tz).format()\n return info\n})\n\nexport class ConnectionManager {\n private static connectors: { [propName: string]: Connector } = {}\n private static connections: { [domainId: string]: { [name: string]: any } } = {}\n private static entities = {}\n /**\n * Session 직렬화 mutex — connection name 단위 (도메인 무관 글로벌).\n * sessionExclusive connector 의 acquireSessionPage 동시 호출이 외부 세션을 서로\n * invalidate 하지 않도록 큐잉. release 호출 시 다음 대기자가 진행.\n */\n private static sessionLocks: Map<string, Promise<void>> = new Map()\n private static logFormat = printf(({ level, message, timestamp, stack }) => {\n return `${timestamp} ${level}: ${stack || message}`\n })\n\n public static logger = createLogger({\n format: combine(errors({ stack: true }), systemTimestamp({ tz: SYSTEM_TZ }), splat(), ConnectionManager.logFormat),\n transports: [\n new (transports as any).DailyRotateFile({\n filename: `logs/connections-%DATE%.log`,\n datePattern: 'YYYY-MM-DD-HH',\n zippedArchive: false,\n maxSize: '20m',\n maxFiles: '14d',\n level: 'info'\n }),\n new PubSubLogTransport({\n topic: 'connection-log'\n })\n ]\n })\n\n static async ready() {\n const CONNECTIONS = await Promise.all(\n (\n await getRepository(Connection).find({\n where: { active: true },\n relations: ['domain', 'edge', 'creator', 'updater']\n })\n ).map(async connection => {\n // 🔐 해결된 파라미터 사용\n const resolvedParams = await connection.getResolvedParameters()\n\n return {\n ...connection,\n params: resolvedParams\n }\n })\n )\n\n ConnectionManager.logger.info('Initializing ConnectionManager...')\n\n return await Promise.all(\n [...Object.keys(ConnectionManager.connectors), 'proxy-connector'].map(type => {\n const connector = type == 'proxy-connector' ? ProxyConnector.instance : ConnectionManager.getConnector(type)\n\n ConnectionManager.logger.info(`Connector '${type}' started to ready`)\n\n return connector\n .ready(\n CONNECTIONS.filter(connection => {\n if (type == 'proxy-connector') {\n return !!connection.edge\n } else {\n return !connection.edge && connection.type == type\n }\n }) as any\n )\n .catch(error => {\n ConnectionManager.logger.error(error)\n })\n .then(() => {\n ConnectionManager.logger.info(`All connector for '${type}' ready`)\n })\n })\n ).then(() => {\n ConnectionManager.logger.info('ConnectionManager initialization done:')\n Object.keys(ConnectionManager.connections).forEach(key => {\n var connections = ConnectionManager.connections[key]\n ConnectionManager.logger.info('For domain(%s) : %s', key, JSON.stringify(Object.keys(connections)))\n })\n })\n }\n\n static registerConnector(type: string, connector: Connector) {\n ConnectionManager.connectors[type] = connector\n }\n\n static getConnector(type: string): Connector {\n return ConnectionManager.connectors[type]\n }\n\n static getConnectors(): { [connectorName: string]: Connector } {\n return {\n ...ConnectionManager.connectors\n }\n }\n\n static unregisterConnector(type: string) {\n delete ConnectionManager.connectors[type]\n }\n\n static getConnections() {\n return ConnectionManager.connections\n }\n\n static getEntities() {\n return ConnectionManager.entities\n }\n\n static getConnectionInstance(connection: Connection): any {\n const { domain, name } = connection\n return ConnectionManager.connections[domain.id]?.[name]\n }\n\n /**\n * 세션 사용 패턴 (RAII).\n *\n * connector 가 sessionExclusive=true 로 선언한 경우 connection.name 단위 mutex 로\n * 직렬화. 호출자는 acquireSessionPage / releasePage 의 lifecycle 을 신경 쓸 필요 없이\n * `withSession(connection, async page => { ... })` 형태로 사용하면 됨.\n *\n * stateless connector (sessionExclusive=false) 는 mutex 없이 즉시 진행.\n *\n * @param connection 도메인 instance (connection.type 에서 connector 조회)\n * @param fn page 를 받아 데이터 작업하는 async 함수\n */\n static async withSession<T>(connection: any, fn: (page: any) => Promise<T>): Promise<T> {\n const connector = ConnectionManager.getConnector(connection.type)\n const exclusive = !!connector?.sessionExclusive\n\n const run = async (): Promise<T> => {\n const page = await connection.acquireSessionPage()\n try {\n return await fn(page)\n } finally {\n // releasePage 가 page 인자를 받음 — 누락 시 내부의 page.close() 와\n // browser 풀 반환이 스킵되어 브라우저가 leak. 풀 고갈 → 정상 종료 hang 으로 이어짐.\n try {\n await connection.releasePage?.(page)\n } catch (e) {\n ConnectionManager.logger.warn(`releasePage failed for '${connection.name}':`, e as any)\n }\n }\n }\n\n if (!exclusive) return await run()\n\n // mutex: connection name 단위로 직렬화 (도메인 무관, 같은 외부 계정 중복 로그인 방지)\n const lockKey = connection.name\n const wasQueued = ConnectionManager.sessionLocks.has(lockKey)\n const prev = ConnectionManager.sessionLocks.get(lockKey) || Promise.resolve()\n let release!: () => void\n const next = new Promise<void>(r => (release = r))\n // chain: 이전 작업이 끝난 뒤 next 가 끝날 때까지 pending — 다음 대기자가 이 chain 으로 await\n const chain = prev.then(() => next)\n ConnectionManager.sessionLocks.set(lockKey, chain)\n\n // 큐 대기 진단 — 운영 중 \"왜 작업이 stall 되는가\" 추적용\n const waitStartedAt = wasQueued ? Date.now() : 0\n if (wasQueued) {\n ConnectionManager.logger.info(\n `[withSession] '${lockKey}' queued behind existing lock — waiting (domain='${connection.domain?.subdomain || connection.domainId}')`\n )\n }\n await prev\n if (wasQueued) {\n const waited = Date.now() - waitStartedAt\n ConnectionManager.logger.info(\n `[withSession] '${lockKey}' lock acquired after ${waited}ms wait`\n )\n }\n\n try {\n return await run()\n } finally {\n release()\n // 내가 latest 면 (이 chain 뒤로 새로 들어온 대기자 없음) cleanup\n if (ConnectionManager.sessionLocks.get(lockKey) === chain) {\n ConnectionManager.sessionLocks.delete(lockKey)\n }\n }\n }\n\n static async getConnectionInstanceByName(domain: Domain, name: string) {\n const connections = ConnectionManager.connections[domain.id]\n const connection = connections?.[name]\n\n if (!connection) {\n // 자식 도메인에 instance 가 없을 때의 세 가지 자동 처리 분기:\n //\n // (a) entity.__sharedFrom 있음 — SHARE 모드 inheritance\n // → 부모 도메인의 instance 를 그대로 lookup 해서 반환. 자식 도메인에는 instance 등록 안 함.\n // 같은 instance 가 부모·자식 모두에게 서비스 됨.\n //\n // (b) entity.__inheritedFrom 있음 — ISOLATE 모드 inheritance\n // → 자식 도메인 키 아래 새 instance 자동 생성. cookies/세션 격리.\n //\n // (c) entity.onDemand=true — 명시적 lazy 생성 (자식 자체 record 인 경우에도 적용)\n // → 자식 도메인 키 아래 새 instance 생성.\n //\n // 나머지: 자식 record 도 없고 inherit 도 안 되는 경우 → throw.\n try {\n const connectionEntity = await ConnectionManager.getConnectionEntityByName(domain, name)\n\n if (connectionEntity?.__sharedFrom) {\n // SHARE: 부모 instance 직접 사용\n const parentInstance = ConnectionManager.connections[connectionEntity.__sharedFrom]?.[name]\n if (parentInstance) return parentInstance\n throw `SHARE mode: parent instance for '${name}' (domain id '${connectionEntity.__sharedFrom}') not registered yet — startup ready() may have failed for parent`\n }\n\n if (connectionEntity?.onDemand || connectionEntity?.__inheritedFrom) {\n const instance = await ConnectionManager.createOnDemandConnection(connectionEntity!)\n return instance\n } else if (connectionEntity) {\n // entity 는 찾았는데 instance 생성 trigger (onDemand/inherit) 없음 — 운영자가 record 자체 점검 필요\n throw `Connection '${name}' record exists but instance is not registered and no auto-create trigger (onDemand=false, no inheritance marker). Check whether parent record's active=true and ready() succeeded at startup.`\n } else {\n // entity 자체 못 찾음 — 도메인 트리 어디에도 record 없음\n throw `Connection '${name}' not found in domain '${domain.subdomain || domain.id}' or any ancestor — verify the Connection record exists at the running domain or any of its ancestors.`\n }\n } catch (error) {\n throw `The connection with the given name(${name}) cannot be found: ${error}`\n }\n }\n\n return connection\n }\n\n static async getConnectionEntityByName(domain: Domain, name: string): Promise<Connection | null> {\n // 1. 현재 도메인에서 메모리 조회 (우선)\n const cachedEntity = ConnectionManager.entities[domain.id]?.[name]\n if (cachedEntity) {\n return cachedEntity\n }\n\n // 2. 현재 도메인에서 데이터베이스 조회 (우선)\n try {\n let connection = await getRepository(Connection).findOne({\n where: {\n domain: { id: domain.id },\n name: name\n },\n relations: ['domain', 'edge', 'creator', 'updater']\n })\n\n if (connection) {\n const resolvedParams = await connection.getResolvedParameters()\n connection.params = resolvedParams\n return connection\n }\n\n // 3. 현재 도메인에서 못 찾으면 부모 도메인에서 메모리 조회 - 안된다. connection정보를 수정해야 하므로 사용할 수 없다.\n // if (domain.parentId) {\n // const parentCachedEntity = ConnectionManager.entities[domain.parentId]?.[name]\n // if (parentCachedEntity) {\n // return parentCachedEntity\n // }\n // }\n\n // 4. ancestor 도메인 트리 fallback (closest-first walk)\n //\n // EnvVar inheritance 와 일관되게 모든 조상 도메인을 closest-first 로 탐색.\n // 예: SYSTEM → 시공사 → 프로젝트 트리에서 시나리오가 프로젝트 도메인에서 실행될 때\n // Connection record 가 SYSTEM 또는 시공사 어디에 있어도 찾아냄.\n //\n // 매칭된 ancestor 의 inheritanceMode 에 따라 두 갈래:\n // ISOLATE (default): 자식 도메인용 clone 생성. cookies/세션 격리, 자식 컨텍스트에서\n // EnvVar override 해소. __inheritedFrom marker 로 후속 자동 instance.\n // SHARE: 부모 entity 그대로 반환. params·세션·인스턴스 모두 부모 것 공유.\n // __sharedFrom marker 로 instance lookup 이 부모 도메인을 가리키게 함.\n //\n // 호환성: inheritanceMode 미지정(NULL) → connector default → 폴백 ISOLATE.\n // 기존 모든 Connection 은 이 column 추가 전 만들어졌으므로 NULL → ISOLATE = 기존 거동.\n const ancestorIds = await getDomainIdsWithAncestors(domain)\n // ancestorIds[0] = domain.id (이미 위 분기에서 시도), skip\n const searchedAncestors: string[] = []\n for (let i = 1; i < ancestorIds.length; i++) {\n const ancestorId = ancestorIds[i]\n searchedAncestors.push(ancestorId)\n const parentConn = await getRepository(Connection).findOne({\n where: {\n domain: { id: ancestorId },\n name: name\n },\n relations: ['domain', 'edge', 'creator', 'updater']\n })\n\n if (!parentConn) continue // 이 ancestor 에 없으면 다음 ancestor\n\n ConnectionManager.logger.info(\n `[inheritance] '${name}' inherited by domain '${domain.subdomain || domain.id}' ` +\n `from ancestor '${parentConn.domain?.subdomain || ancestorId}' ` +\n `(walked ${i} ancestor(s))`\n )\n\n const connectorDefault = ConnectionManager.connectors[parentConn.type]?.inheritanceMode\n const effectiveMode =\n parentConn.inheritanceMode || connectorDefault || ConnectionInheritanceMode.ISOLATE\n\n if (effectiveMode === ConnectionInheritanceMode.SHARE) {\n // SHARE — 부모 entity 그대로 반환 (mutation 최소화).\n const shared: any = parentConn\n shared.__sharedFrom = parentConn.domainId\n shared.params = await parentConn.getResolvedParameters()\n return shared\n }\n\n // ISOLATE (default) — clone + marker\n const inherited: any = Object.assign(\n Object.create(Object.getPrototypeOf(parentConn)),\n parentConn,\n {\n domain,\n domainId: domain.id,\n cookies: undefined,\n __inheritedFrom: parentConn.domainId\n }\n )\n inherited.params = await inherited.getResolvedParameters()\n return inherited\n }\n\n // 어느 ancestor 에서도 못 찾음 — 운영자가 어디까지 찾았는지 알 수 있도록 진단 로그\n if (searchedAncestors.length > 0) {\n ConnectionManager.logger.warn(\n `[inheritance] '${name}' not found at any ancestor of domain '${domain.subdomain || domain.id}' ` +\n `— searched: [${searchedAncestors.join(', ')}]`\n )\n }\n return null\n } catch (error) {\n ConnectionManager.logger.error(`Failed to get connection entity '${name}' from database:`, error)\n return null\n }\n }\n\n static getConnectionInstanceEntityByName(domain: Domain, name: string): any {\n const connection = ConnectionManager.entities[domain.id]?.[name]\n if (connection) {\n return connection\n }\n\n if (domain.parentId) {\n return ConnectionManager.entities[domain.parentId]?.[name]\n }\n }\n\n /**\n * Creates a connection on-demand and returns the instance.\n * @param connection - The connection entity to create\n * @returns The connection instance\n */\n static async createOnDemandConnection(connection: Connection): Promise<any> {\n try {\n // 커넥션 생성\n await connection.connect()\n\n ConnectionManager.logger.info(`On-demand connection '${connection.name}' created successfully`)\n return ConnectionManager.getConnectionInstance(connection)\n } catch (error) {\n ConnectionManager.logger.error(`Failed to create on-demand connection '${connection.name}':`, error)\n throw error\n }\n }\n\n /**\n * Disconnects an on-demand connection.\n * @param connection - The connection entity to disconnect\n */\n static async disconnectOnDemandConnection(connection: Connection): Promise<void> {\n try {\n await connection.disconnect()\n // ConnectionManager.removeConnectionInstance(connection)\n ConnectionManager.logger.info(`On-demand connection '${connection.name}' disconnected successfully`)\n } catch (error) {\n ConnectionManager.logger.error(`Failed to disconnect on-demand connection '${connection.name}':`, error)\n }\n }\n\n static getConnectionInstances(domain: Domain): { [connectionName: string]: any } {\n const connections = ConnectionManager.connections[domain.id]\n const parentConnections = domain.parentId && ConnectionManager.connections[domain.parentId]\n\n return {\n ...parentConnections,\n ...connections\n }\n }\n\n static getConnectionInstanceEntities(domain: Domain): { [connectionName: string]: any } {\n const connections = ConnectionManager.entities[domain.id]\n\n return {\n ...connections\n }\n }\n\n static addConnectionInstance(connection: Connection, instance: any) {\n const { domain, name } = connection\n\n var connections = ConnectionManager.connections[domain.id]\n if (!connections) {\n connections = ConnectionManager.connections[domain.id] = {}\n }\n\n var entities = ConnectionManager.entities[domain.id]\n if (!entities) {\n entities = ConnectionManager.entities[domain.id] = {}\n }\n\n connections[name] = instance\n entities[name] = connection\n\n ConnectionManager.publishState(connection, ConnectionStatus.CONNECTED)\n debug('add-connection', domain.subdomain, name)\n }\n\n static removeConnectionInstance(connection: Connection): any {\n const { domain, name } = connection\n var connections = ConnectionManager.connections[domain.id]\n var entities = ConnectionManager.entities[domain.id]\n\n var instance = connections?.[name]\n\n if (!connections || !instance) {\n debug('remove-connection', `'${name}' connection not found in domain '${domain.subdomain}'`)\n return\n }\n\n delete connections[name]\n delete entities[name]\n\n ConnectionManager.publishState(connection, ConnectionStatus.DISCONNECTED)\n debug('remove-connection', `'${name}' connection is removed from domain '${domain.subdomain}'`)\n\n return instance\n }\n\n private static async publishState(connection: Connection, state) {\n const { domain, id, name, description, type, edge } = connection\n\n pubsub.publish('connection-state', {\n connectionState: {\n domain,\n id,\n name,\n description,\n type,\n edge,\n state,\n timestamp: new Date()\n }\n })\n }\n}\n"]}
@@ -1,5 +1,17 @@
1
1
  import { Connector } from '../types';
2
2
  export declare class HeadlessConnector implements Connector {
3
+ /**
4
+ * 외부 사이트 로그인 세션 보유 — 같은 connection name 의 동시 acquireSessionPage
5
+ * 호출이 같은 외부 계정 세션을 서로 invalidate 하지 않도록 ConnectionManager 가
6
+ * mutex 직렬화. 상속 connector (allbaro, kiscon 등) 자동 적용.
7
+ */
8
+ sessionExclusive: boolean;
9
+ /**
10
+ * 자식 도메인이 부모의 Connection 정의를 inherit 할 때 자식별 새 인스턴스 생성.
11
+ * cookies/세션·자격증명 격리. 헤드리스 사이트 connector 는 외부 자격증명이
12
+ * 자식 (테넌트) 마다 다를 수 있으므로 ISOLATE 가 자연 default.
13
+ */
14
+ inheritanceMode: "isolate";
3
15
  ready(connectionConfigs: any): Promise<void>;
4
16
  connect(connection: any): Promise<void>;
5
17
  setupPage(page: any, uri: any, timeout: any): Promise<void>;
@@ -15,6 +15,20 @@ const headless_pool_1 = require("../resource-pool/headless-pool");
15
15
  - Released pages are returned to the `headlessPool` for reuse.
16
16
  */
17
17
  class HeadlessConnector {
18
+ constructor() {
19
+ /**
20
+ * 외부 사이트 로그인 세션 보유 — 같은 connection name 의 동시 acquireSessionPage
21
+ * 호출이 같은 외부 계정 세션을 서로 invalidate 하지 않도록 ConnectionManager 가
22
+ * mutex 직렬화. 상속 connector (allbaro, kiscon 등) 자동 적용.
23
+ */
24
+ this.sessionExclusive = true;
25
+ /**
26
+ * 자식 도메인이 부모의 Connection 정의를 inherit 할 때 자식별 새 인스턴스 생성.
27
+ * cookies/세션·자격증명 격리. 헤드리스 사이트 connector 는 외부 자격증명이
28
+ * 자식 (테넌트) 마다 다를 수 있으므로 ISOLATE 가 자연 default.
29
+ */
30
+ this.inheritanceMode = 'isolate';
31
+ }
18
32
  async ready(connectionConfigs) {
19
33
  await Promise.all(connectionConfigs.map(this.connect.bind(this)));
20
34
  connection_manager_1.ConnectionManager.logger.info('headless-connector connections are ready');
@@ -22,10 +36,13 @@ class HeadlessConnector {
22
36
  async connect(connection) {
23
37
  const { endpoint: uri = '1', params: { username = '', password = '', loginPagePath = '/login', loginApiUrl = null, usernameSelector = '#username', passwordSelector = '#password', submitSelector = '#submit', successSelector = null, shadowDomSelectors = '', // Comma separated shadow DOM selectors
24
38
  timeout = 15000, // Default timeout for operations
25
- retries = 3 // Default number of retries for login or page actions
26
- } = {} } = connection;
39
+ retries = 3, // Default number of retries for login or page actions
40
+ // 쿠키 캐시 재사용 건너뛰고 매 acquireSessionPage 마다 로그인하도록 강제.
41
+ // 서버 세션 만료가 잦거나 검증이 비싼 사이트(예: 올바로) 에서 사용.
42
+ forceLogin = false } = {} } = connection;
27
43
  const loginInfo = {
28
44
  loginRequired: Boolean(username), // Determine if login is required
45
+ forceLogin: Boolean(forceLogin),
29
46
  username,
30
47
  password,
31
48
  loginPagePath,
@@ -135,25 +152,36 @@ class HeadlessConnector {
135
152
  page = await browser.newPage();
136
153
  await this.setupPage(page, uri, timeout);
137
154
  if (loginInfo.loginRequired) {
138
- // 먼저 기본 페이지로 이동
139
- await page.goto(uri, { waitUntil: 'networkidle2', timeout });
140
- // 현재 세션의 Authorization 헤더 확인
141
- const headers = await page.evaluate(() => {
142
- return fetch(window.location.href, { method: 'GET' })
143
- .then(response => response.headers.get('Authorization'))
144
- .catch(() => null);
145
- });
146
- if (headers) {
147
- connection_manager_1.ConnectionManager.logger.info('User is already logged in, skipping login process.');
148
- return page;
155
+ if (loginInfo.forceLogin) {
156
+ // 매번 새로 로그인 쿠키·세션 재사용 검증 모두 건너뜀.
157
+ // 세션이 서버측에서 자주 만료되는 사이트 용도.
158
+ connection_manager_1.ConnectionManager.logger.info('forceLogin=true skipping cookie reuse, performing fresh login');
159
+ connection.cookies = undefined;
160
+ await this.performLogin(page, uri, loginInfo);
161
+ cookies = await page.cookies();
162
+ connection.cookies = cookies;
149
163
  }
150
- if (cookies && isCookieValid(cookies)) {
151
- await this.applyCookiesAndVerifySession(page, cookies, loginInfo);
152
- return page;
164
+ else {
165
+ // 먼저 기본 페이지로 이동
166
+ await page.goto(uri, { waitUntil: 'networkidle2', timeout });
167
+ // 현재 세션의 Authorization 헤더 확인
168
+ const headers = await page.evaluate(() => {
169
+ return fetch(window.location.href, { method: 'GET' })
170
+ .then(response => response.headers.get('Authorization'))
171
+ .catch(() => null);
172
+ });
173
+ if (headers) {
174
+ connection_manager_1.ConnectionManager.logger.info('User is already logged in, skipping login process.');
175
+ return page;
176
+ }
177
+ if (cookies && isCookieValid(cookies)) {
178
+ await this.applyCookiesAndVerifySession(page, cookies, loginInfo);
179
+ return page;
180
+ }
181
+ await this.performLogin(page, uri, loginInfo);
182
+ cookies = await page.cookies();
183
+ connection.cookies = cookies;
153
184
  }
154
- await this.performLogin(page, uri, loginInfo);
155
- cookies = await page.cookies();
156
- connection.cookies = cookies;
157
185
  }
158
186
  else {
159
187
  // 로그인이 필요하지 않은 경우에도 기본 페이지로 이동
@@ -417,14 +445,17 @@ class HeadlessConnector {
417
445
  });
418
446
  page.on('requestfailed', request => {
419
447
  try {
420
- console.log('Request failed:');
421
- console.log(`- URL: ${request.url()}`);
422
- console.log(`- Method: ${request.method()}`);
423
- console.log(`- Failure Text: ${request.failure()?.errorText}`);
424
- console.log(`- Headers:`, request.headers());
425
- if (request.postData()) {
426
- console.log(`- Post Data: ${request.postData()}`);
427
- }
448
+ // 정적 리소스(이미지·폰트·CSS ) 의 실패는 로그인 흐름과 무관한 소음이므로 무시.
449
+ // document / xhr / fetch 같은 의미있는 요청 실패만 출력.
450
+ const type = request.resourceType();
451
+ const NOISY_TYPES = new Set(['image', 'font', 'stylesheet', 'media', 'manifest', 'other']);
452
+ if (NOISY_TYPES.has(type))
453
+ return;
454
+ // 일부 사이트의 정적 자원이 다른 type 으로 잡힐 수 있어 확장자 기반 보강 필터.
455
+ const url = request.url();
456
+ if (/\.(png|jpe?g|gif|webp|svg|ico|woff2?|ttf|otf|eot|css|map)(\?|$)/i.test(url))
457
+ return;
458
+ console.warn(`[headless] request failed (type=${type}): ${request.method()} ${url} — ${request.failure()?.errorText}`);
428
459
  }
429
460
  catch (error) {
430
461
  console.error('Error in requestfailed handler:', error);
@@ -442,6 +473,7 @@ class HeadlessConnector {
442
473
  }
443
474
  }
444
475
  async performLogin(page, uri, loginInfo) {
476
+ let lastError = null;
445
477
  for (let attempt = 1; attempt <= loginInfo.retries; attempt++) {
446
478
  try {
447
479
  await page.goto(`${uri}${loginInfo.loginPagePath}`, { waitUntil: 'networkidle2', timeout: loginInfo.timeout });
@@ -449,7 +481,12 @@ class HeadlessConnector {
449
481
  const passwordInput = await this.resolveShadowDom(page, loginInfo.loginSelectors.shadowDomSelectors, loginInfo.loginSelectors.passwordSelector);
450
482
  const submitButton = await this.resolveShadowDom(page, loginInfo.loginSelectors.shadowDomSelectors, loginInfo.loginSelectors.submitSelector);
451
483
  if (!usernameInput || !passwordInput || !submitButton) {
452
- throw new Error('Failed to locate input elements in shadow DOM');
484
+ throw new Error(`Failed to locate login form elements ` +
485
+ `(username=${!!usernameInput}, password=${!!passwordInput}, submit=${!!submitButton}). ` +
486
+ `Selectors: username='${loginInfo.loginSelectors.usernameSelector}', ` +
487
+ `password='${loginInfo.loginSelectors.passwordSelector}', ` +
488
+ `submit='${loginInfo.loginSelectors.submitSelector}'. ` +
489
+ `사이트 폼 변경 가능성 — Connection params 의 *Selector 값을 실제 폼에 맞춰 덮어쓰세요.`);
453
490
  }
454
491
  await usernameInput.type(loginInfo.username);
455
492
  await passwordInput.type(loginInfo.password);
@@ -495,6 +532,7 @@ class HeadlessConnector {
495
532
  }
496
533
  }
497
534
  catch (error) {
535
+ lastError = error;
498
536
  connection_manager_1.ConnectionManager.logger.warn(`Login attempt ${attempt} failed:`, error);
499
537
  try {
500
538
  await page.screenshot({ path: `logs/login-failure-attempt-${attempt}.png` });
@@ -503,7 +541,7 @@ class HeadlessConnector {
503
541
  connection_manager_1.ConnectionManager.logger.error('Failed to capture screenshot:', error);
504
542
  }
505
543
  if (attempt === loginInfo.retries) {
506
- throw new Error(`Login failed after ${loginInfo.retries} attempts`);
544
+ throw new Error(`Login failed after ${loginInfo.retries} attempts — last cause: ${lastError?.message || lastError}`);
507
545
  }
508
546
  }
509
547
  }
@@ -511,9 +549,14 @@ class HeadlessConnector {
511
549
  async resolveShadowDom(page, shadowSelectors, targetSelector) {
512
550
  let context;
513
551
  if (!shadowSelectors || shadowSelectors.length === 0) {
514
- // No Shadow DOM path; use document root as the context
552
+ // No Shadow DOM path; use document root as the context.
553
+ // XPath 지원 — puppeteer 22+ 의 통합 선택자: `xpath/<expression>` prefix.
554
+ // 셀렉터가 `//`, `(//`, `.//` 로 시작하면 xpath/ 로 감싸 page.$ 에 전달.
555
+ if (typeof targetSelector === 'string' && /^\s*(\(|\/\/|\.\/\/)/.test(targetSelector)) {
556
+ return await page.$('xpath/' + targetSelector.trim());
557
+ }
515
558
  context = page.mainFrame(); // Puppeteer uses frames to represent document
516
- return context.$(targetSelector); // Search directly in the document root
559
+ return context.$(targetSelector); // CSS selector — Search directly in the document root
517
560
  }
518
561
  context = page; // Start with the page as the context
519
562
  for (const selector of shadowSelectors) {