@open-kingdom/shared-backend-integration-test-doubles 0.0.2-17 → 0.0.2-18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -5
- package/dist/gcs/gcs-error-mode.d.ts +161 -0
- package/dist/gcs/gcs-error-mode.d.ts.map +1 -0
- package/dist/gcs/gcs-error-mode.js +256 -0
- package/dist/gcs/gcs-error-mode.js.map +1 -0
- package/dist/gcs/gcs-error-simulation.interceptor.d.ts +21 -0
- package/dist/gcs/gcs-error-simulation.interceptor.d.ts.map +1 -0
- package/dist/gcs/gcs-error-simulation.interceptor.js +40 -0
- package/dist/gcs/gcs-error-simulation.interceptor.js.map +1 -0
- package/dist/gcs/gcs-twin.d.ts +1 -0
- package/dist/gcs/gcs-twin.d.ts.map +1 -1
- package/dist/gcs/gcs-twin.js +19 -1
- package/dist/gcs/gcs-twin.js.map +1 -1
- package/dist/gcs/index.d.ts +3 -0
- package/dist/gcs/index.d.ts.map +1 -1
- package/dist/gcs/index.js +2 -0
- package/dist/gcs/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptor/http-helper.d.ts +37 -0
- package/dist/interceptor/http-helper.d.ts.map +1 -0
- package/dist/interceptor/http-helper.js +109 -0
- package/dist/interceptor/http-helper.js.map +1 -0
- package/dist/interceptor/index.d.ts +4 -0
- package/dist/interceptor/index.d.ts.map +1 -0
- package/dist/interceptor/index.js +5 -0
- package/dist/interceptor/index.js.map +1 -0
- package/dist/interceptor/node-interceptor.d.ts +40 -0
- package/dist/interceptor/node-interceptor.d.ts.map +1 -0
- package/dist/interceptor/node-interceptor.js +188 -0
- package/dist/interceptor/node-interceptor.js.map +1 -0
- package/dist/interceptor/routing-table.config.d.ts +3 -0
- package/dist/interceptor/routing-table.config.d.ts.map +1 -0
- package/dist/interceptor/routing-table.config.js +25 -0
- package/dist/interceptor/routing-table.config.js.map +1 -0
- package/dist/interceptor/routing-table.d.ts +15 -0
- package/dist/interceptor/routing-table.d.ts.map +1 -0
- package/dist/interceptor/routing-table.js +44 -0
- package/dist/interceptor/routing-table.js.map +1 -0
- package/dist/shared/constants.d.ts +4 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +3 -1
- package/dist/shared/constants.js.map +1 -1
- package/dist/youtube/index.d.ts +15 -0
- package/dist/youtube/index.d.ts.map +1 -0
- package/dist/youtube/index.js +11 -0
- package/dist/youtube/index.js.map +1 -0
- package/dist/youtube/msw-handlers.d.ts +40 -0
- package/dist/youtube/msw-handlers.d.ts.map +1 -0
- package/dist/youtube/msw-handlers.js +45 -0
- package/dist/youtube/msw-handlers.js.map +1 -0
- package/dist/youtube/player-shim.d.ts +3 -0
- package/dist/youtube/player-shim.d.ts.map +1 -0
- package/dist/youtube/player-shim.js +296 -0
- package/dist/youtube/player-shim.js.map +1 -0
- package/dist/youtube/search-fixtures.d.ts +29 -0
- package/dist/youtube/search-fixtures.d.ts.map +1 -0
- package/dist/youtube/search-fixtures.js +392 -0
- package/dist/youtube/search-fixtures.js.map +1 -0
- package/dist/youtube/search-response.d.ts +51 -0
- package/dist/youtube/search-response.d.ts.map +1 -0
- package/dist/youtube/search-response.js +53 -0
- package/dist/youtube/search-response.js.map +1 -0
- package/dist/youtube/youtube-error-mode.d.ts +36 -0
- package/dist/youtube/youtube-error-mode.d.ts.map +1 -0
- package/dist/youtube/youtube-error-mode.js +87 -0
- package/dist/youtube/youtube-error-mode.js.map +1 -0
- package/dist/youtube/youtube-twin.config.d.ts +13 -0
- package/dist/youtube/youtube-twin.config.d.ts.map +1 -0
- package/dist/youtube/youtube-twin.config.js +32 -0
- package/dist/youtube/youtube-twin.config.js.map +1 -0
- package/dist/youtube/youtube-twin.d.ts +14 -0
- package/dist/youtube/youtube-twin.d.ts.map +1 -0
- package/dist/youtube/youtube-twin.js +162 -0
- package/dist/youtube/youtube-twin.js.map +1 -0
- package/package.json +15 -2
- package/test-assets/youtube/sample.mp4 +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-01.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-02.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-03.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-04.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-05.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-06.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-07.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-08.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-09.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-10.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-11.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-12.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-13.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-14.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-15.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-16.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-17.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-18.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-19.jpg +0 -0
- package/test-assets/youtube/thumbnails/thumbnail-20.jpg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/interceptor/node-interceptor.ts"],"sourcesContent":["import http from 'http';\nimport https from 'https';\nimport { RoutingTable } from './routing-table.js';\nimport {\n parseHttpArguments,\n injectOriginalHostHeader,\n rewriteRequestOptions,\n callRequestWithOptions,\n callRequestWithUrlAndOptions,\n callRequestWithUrl,\n forwardToOriginalRequest,\n} from './http-helper.js';\n\nexport class NodeInterceptor {\n private originalFetch: typeof globalThis.fetch | null = null;\n private originalHttpRequest: typeof http.request | null = null;\n private originalHttpGet: typeof http.get | null = null;\n private originalHttpsRequest: typeof https.request | null = null;\n private originalHttpsGet: typeof https.get | null = null;\n private readonly routingTable: RoutingTable;\n private active = false;\n\n constructor(routingTable: RoutingTable) {\n this.routingTable = routingTable;\n }\n\n /**\n * Installs interception hooks for both global fetch and node http/https modules.\n */\n install(): void {\n if (this.active) {\n return; // Idempotency guard\n }\n\n this.interceptFetch();\n this.interceptHttpAndHttpsRequests();\n\n this.active = true;\n }\n\n /**\n * Restores fetch and http/https modules to their original native states.\n */\n uninstall(): void {\n if (!this.active) {\n return;\n }\n\n this.restoreFetch();\n this.restoreHttpAndHttpsRequests();\n\n this.active = false;\n }\n\n /**\n * Checks if interception is currently active.\n */\n isActive(): boolean {\n return this.active;\n }\n\n /**\n * Monkey-patches the global fetch API to redirect requests based on the RoutingTable.\n */\n private interceptFetch(): void {\n this.originalFetch = globalThis.fetch;\n const originalFetch = this.originalFetch;\n const routingTable = this.routingTable;\n\n globalThis.fetch = async (\n input: Parameters<typeof globalThis.fetch>[0],\n init?: Parameters<typeof globalThis.fetch>[1]\n ): Promise<Response> => {\n if (!originalFetch) {\n throw new Error('NodeInterceptor: original fetch is not cached');\n }\n\n let urlStr = '';\n\n if (typeof input === 'string') {\n urlStr = input;\n } else if (input instanceof URL) {\n urlStr = input.toString();\n } else if (input instanceof Request) {\n urlStr = input.url;\n }\n\n const rewritten = routingTable.resolve(urlStr);\n\n if (rewritten) {\n const originalHost = new URL(urlStr).host;\n const headers = new Headers();\n\n // 1. Copy headers from input Request if it exists\n if (input instanceof Request) {\n input.headers.forEach((value, key) => {\n headers.set(key, value);\n });\n }\n\n // 2. Overwrite with any headers passed in init\n if (init?.headers) {\n const initHeaders = new Headers(init.headers);\n initHeaders.forEach((value, key) => {\n headers.set(key, value);\n });\n }\n\n // 3. Inject X-Original-Host tracking header\n headers.set('X-Original-Host', originalHost);\n\n // Handle native Request object conversion cleanly\n if (input instanceof Request) {\n const newInit: Record<string, unknown> = {\n method: input.method,\n headers: headers,\n body: input.body,\n signal: input.signal,\n ...init,\n };\n if (input.body) {\n newInit.duplex = 'half';\n }\n if ('credentials' in input) newInit.credentials = input.credentials;\n if ('mode' in input) newInit.mode = input.mode;\n if ('referrer' in input) newInit.referrer = input.referrer;\n if ('cache' in input) newInit.cache = input.cache;\n if ('redirect' in input) newInit.redirect = input.redirect;\n\n const newRequest = new Request(rewritten, newInit as RequestInit);\n return originalFetch(newRequest);\n }\n\n // Handle string/URL input\n return originalFetch(rewritten, {\n ...init,\n headers,\n });\n }\n\n // No match - forward request untouched\n return originalFetch(input, init);\n };\n }\n\n /**\n * Monkey-patches Node's native http/https request methods.\n */\n private interceptHttpAndHttpsRequests(): void {\n this.originalHttpRequest = http.request;\n this.originalHttpGet = http.get;\n this.originalHttpsRequest = https.request;\n this.originalHttpsGet = https.get;\n\n // Helper to generate a patched request method for http or https\n const createPatchedRequest = (\n moduleProtocol: string,\n originalRequest: typeof http.request\n ) => {\n return (\n arg1: string | URL | http.RequestOptions,\n arg2?: http.RequestOptions | ((res: http.IncomingMessage) => void),\n arg3?: (res: http.IncomingMessage) => void\n ): http.ClientRequest => {\n // Parse polymorphic arguments into standardized request variables\n const { urlObj, options, callback } = parseHttpArguments(\n moduleProtocol,\n arg1,\n arg2,\n arg3\n );\n\n const rewrittenStr = this.routingTable.resolve(urlObj.toString());\n if (rewrittenStr) {\n const rewritten = new URL(rewrittenStr);\n\n // 1. Route to local digital twin emulator target\n let newOptions = rewriteRequestOptions(options, rewritten);\n\n // 2. Add verification header to store original destination\n newOptions = injectOriginalHostHeader(newOptions, urlObj.host);\n\n const targetRequest =\n rewritten.protocol === 'https:'\n ? this.originalHttpsRequest\n : this.originalHttpRequest;\n if (!targetRequest) {\n throw new Error(\n 'NodeInterceptor: Cannot call targetRequest because the interceptor has not been fully installed.'\n );\n }\n return callRequestWithOptions(targetRequest, newOptions, callback);\n }\n\n // Pass-through to original network endpoint if no matching redirection rule is found\n return forwardToOriginalRequest(originalRequest, arg1, arg2, callback);\n };\n };\n\n // Helper to generate a patched get method (wraps the request method and auto-ends the request stream)\n const createPatchedGet = (patchedRequest: typeof http.request) => {\n return (\n arg1: string | URL | http.RequestOptions,\n arg2?: http.RequestOptions | ((res: http.IncomingMessage) => void),\n arg3?: (res: http.IncomingMessage) => void\n ): http.ClientRequest => {\n let req: http.ClientRequest;\n if (typeof arg1 === 'string' || arg1 instanceof URL) {\n if (typeof arg2 === 'object' && arg2 !== null) {\n req = callRequestWithUrlAndOptions(\n patchedRequest,\n arg1,\n arg2,\n arg3\n );\n } else {\n const callback = typeof arg2 === 'function' ? arg2 : arg3;\n req = callRequestWithUrl(patchedRequest, arg1, callback);\n }\n } else {\n const callback = typeof arg2 === 'function' ? arg2 : arg3;\n req = callRequestWithOptions(patchedRequest, arg1, callback);\n }\n req.end();\n return req;\n };\n };\n\n http.request = createPatchedRequest(\n 'http:',\n this.originalHttpRequest\n ) as typeof http.request;\n https.request = createPatchedRequest(\n 'https:',\n this.originalHttpsRequest as typeof http.request\n ) as typeof https.request;\n\n http.get = createPatchedGet(http.request) as typeof http.get;\n https.get = createPatchedGet(\n https.request as typeof http.request\n ) as typeof https.get;\n }\n\n /**\n * Restores global fetch to its original implementation.\n */\n private restoreFetch(): void {\n if (this.originalFetch) {\n globalThis.fetch = this.originalFetch;\n this.originalFetch = null;\n }\n }\n\n /**\n * Restores Node's http/https methods to their original implementations.\n */\n private restoreHttpAndHttpsRequests(): void {\n if (this.originalHttpRequest) {\n http.request = this.originalHttpRequest;\n this.originalHttpRequest = null;\n }\n\n if (this.originalHttpGet) {\n http.get = this.originalHttpGet;\n this.originalHttpGet = null;\n }\n\n if (this.originalHttpsRequest) {\n https.request = this.originalHttpsRequest;\n this.originalHttpsRequest = null;\n }\n\n if (this.originalHttpsGet) {\n https.get = this.originalHttpsGet;\n this.originalHttpsGet = null;\n }\n }\n}\n"],"names":["http","https","parseHttpArguments","injectOriginalHostHeader","rewriteRequestOptions","callRequestWithOptions","callRequestWithUrlAndOptions","callRequestWithUrl","forwardToOriginalRequest","NodeInterceptor","install","active","interceptFetch","interceptHttpAndHttpsRequests","uninstall","restoreFetch","restoreHttpAndHttpsRequests","isActive","originalFetch","globalThis","fetch","routingTable","input","init","Error","urlStr","URL","toString","Request","url","rewritten","resolve","originalHost","host","headers","Headers","forEach","value","key","set","initHeaders","newInit","method","body","signal","duplex","credentials","mode","referrer","cache","redirect","newRequest","originalHttpRequest","request","originalHttpGet","get","originalHttpsRequest","originalHttpsGet","createPatchedRequest","moduleProtocol","originalRequest","arg1","arg2","arg3","urlObj","options","callback","rewrittenStr","newOptions","targetRequest","protocol","createPatchedGet","patchedRequest","req","end","constructor"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":";AAAA,OAAOA,UAAU,OAAO;AACxB,OAAOC,WAAW,QAAQ;AAE1B,SACEC,kBAAkB,EAClBC,wBAAwB,EACxBC,qBAAqB,EACrBC,sBAAsB,EACtBC,4BAA4B,EAC5BC,kBAAkB,EAClBC,wBAAwB,QACnB,mBAAmB;AAE1B,OAAO,MAAMC;IAaX;;GAEC,GACDC,UAAgB;QACd,IAAI,IAAI,CAACC,MAAM,EAAE;YACf,QAAQ,oBAAoB;QAC9B;QAEA,IAAI,CAACC,cAAc;QACnB,IAAI,CAACC,6BAA6B;QAElC,IAAI,CAACF,MAAM,GAAG;IAChB;IAEA;;GAEC,GACDG,YAAkB;QAChB,IAAI,CAAC,IAAI,CAACH,MAAM,EAAE;YAChB;QACF;QAEA,IAAI,CAACI,YAAY;QACjB,IAAI,CAACC,2BAA2B;QAEhC,IAAI,CAACL,MAAM,GAAG;IAChB;IAEA;;GAEC,GACDM,WAAoB;QAClB,OAAO,IAAI,CAACN,MAAM;IACpB;IAEA;;GAEC,GACD,AAAQC,iBAAuB;QAC7B,IAAI,CAACM,aAAa,GAAGC,WAAWC,KAAK;QACrC,MAAMF,gBAAgB,IAAI,CAACA,aAAa;QACxC,MAAMG,eAAe,IAAI,CAACA,YAAY;QAEtCF,WAAWC,KAAK,GAAG,OACjBE,OACAC;YAEA,IAAI,CAACL,eAAe;gBAClB,MAAM,IAAIM,MAAM;YAClB;YAEA,IAAIC,SAAS;YAEb,IAAI,OAAOH,UAAU,UAAU;gBAC7BG,SAASH;YACX,OAAO,IAAIA,iBAAiBI,KAAK;gBAC/BD,SAASH,MAAMK,QAAQ;YACzB,OAAO,IAAIL,iBAAiBM,SAAS;gBACnCH,SAASH,MAAMO,GAAG;YACpB;YAEA,MAAMC,YAAYT,aAAaU,OAAO,CAACN;YAEvC,IAAIK,WAAW;gBACb,MAAME,eAAe,IAAIN,IAAID,QAAQQ,IAAI;gBACzC,MAAMC,UAAU,IAAIC;gBAEpB,kDAAkD;gBAClD,IAAIb,iBAAiBM,SAAS;oBAC5BN,MAAMY,OAAO,CAACE,OAAO,CAAC,CAACC,OAAOC;wBAC5BJ,QAAQK,GAAG,CAACD,KAAKD;oBACnB;gBACF;gBAEA,+CAA+C;gBAC/C,IAAId,wBAAAA,KAAMW,OAAO,EAAE;oBACjB,MAAMM,cAAc,IAAIL,QAAQZ,KAAKW,OAAO;oBAC5CM,YAAYJ,OAAO,CAAC,CAACC,OAAOC;wBAC1BJ,QAAQK,GAAG,CAACD,KAAKD;oBACnB;gBACF;gBAEA,4CAA4C;gBAC5CH,QAAQK,GAAG,CAAC,mBAAmBP;gBAE/B,kDAAkD;gBAClD,IAAIV,iBAAiBM,SAAS;oBAC5B,MAAMa,UAAmC;wBACvCC,QAAQpB,MAAMoB,MAAM;wBACpBR,SAASA;wBACTS,MAAMrB,MAAMqB,IAAI;wBAChBC,QAAQtB,MAAMsB,MAAM;uBACjBrB;oBAEL,IAAID,MAAMqB,IAAI,EAAE;wBACdF,QAAQI,MAAM,GAAG;oBACnB;oBACA,IAAI,iBAAiBvB,OAAOmB,QAAQK,WAAW,GAAGxB,MAAMwB,WAAW;oBACnE,IAAI,UAAUxB,OAAOmB,QAAQM,IAAI,GAAGzB,MAAMyB,IAAI;oBAC9C,IAAI,cAAczB,OAAOmB,QAAQO,QAAQ,GAAG1B,MAAM0B,QAAQ;oBAC1D,IAAI,WAAW1B,OAAOmB,QAAQQ,KAAK,GAAG3B,MAAM2B,KAAK;oBACjD,IAAI,cAAc3B,OAAOmB,QAAQS,QAAQ,GAAG5B,MAAM4B,QAAQ;oBAE1D,MAAMC,aAAa,IAAIvB,QAAQE,WAAWW;oBAC1C,OAAOvB,cAAciC;gBACvB;gBAEA,0BAA0B;gBAC1B,OAAOjC,cAAcY,WAAW,aAC3BP;oBACHW;;YAEJ;YAEA,uCAAuC;YACvC,OAAOhB,cAAcI,OAAOC;QAC9B;IACF;IAEA;;GAEC,GACD,AAAQV,gCAAsC;QAC5C,IAAI,CAACuC,mBAAmB,GAAGpD,KAAKqD,OAAO;QACvC,IAAI,CAACC,eAAe,GAAGtD,KAAKuD,GAAG;QAC/B,IAAI,CAACC,oBAAoB,GAAGvD,MAAMoD,OAAO;QACzC,IAAI,CAACI,gBAAgB,GAAGxD,MAAMsD,GAAG;QAEjC,gEAAgE;QAChE,MAAMG,uBAAuB,CAC3BC,gBACAC;YAEA,OAAO,CACLC,MACAC,MACAC;gBAEA,kEAAkE;gBAClE,MAAM,EAAEC,MAAM,EAAEC,OAAO,EAAEC,QAAQ,EAAE,GAAGhE,mBACpCyD,gBACAE,MACAC,MACAC;gBAGF,MAAMI,eAAe,IAAI,CAAC9C,YAAY,CAACU,OAAO,CAACiC,OAAOrC,QAAQ;gBAC9D,IAAIwC,cAAc;oBAChB,MAAMrC,YAAY,IAAIJ,IAAIyC;oBAE1B,iDAAiD;oBACjD,IAAIC,aAAahE,sBAAsB6D,SAASnC;oBAEhD,2DAA2D;oBAC3DsC,aAAajE,yBAAyBiE,YAAYJ,OAAO/B,IAAI;oBAE7D,MAAMoC,gBACJvC,UAAUwC,QAAQ,KAAK,WACnB,IAAI,CAACd,oBAAoB,GACzB,IAAI,CAACJ,mBAAmB;oBAC9B,IAAI,CAACiB,eAAe;wBAClB,MAAM,IAAI7C,MACR;oBAEJ;oBACA,OAAOnB,uBAAuBgE,eAAeD,YAAYF;gBAC3D;gBAEA,qFAAqF;gBACrF,OAAO1D,yBAAyBoD,iBAAiBC,MAAMC,MAAMI;YAC/D;QACF;QAEA,sGAAsG;QACtG,MAAMK,mBAAmB,CAACC;YACxB,OAAO,CACLX,MACAC,MACAC;gBAEA,IAAIU;gBACJ,IAAI,OAAOZ,SAAS,YAAYA,gBAAgBnC,KAAK;oBACnD,IAAI,OAAOoC,SAAS,YAAYA,SAAS,MAAM;wBAC7CW,MAAMnE,6BACJkE,gBACAX,MACAC,MACAC;oBAEJ,OAAO;wBACL,MAAMG,WAAW,OAAOJ,SAAS,aAAaA,OAAOC;wBACrDU,MAAMlE,mBAAmBiE,gBAAgBX,MAAMK;oBACjD;gBACF,OAAO;oBACL,MAAMA,WAAW,OAAOJ,SAAS,aAAaA,OAAOC;oBACrDU,MAAMpE,uBAAuBmE,gBAAgBX,MAAMK;gBACrD;gBACAO,IAAIC,GAAG;gBACP,OAAOD;YACT;QACF;QAEAzE,KAAKqD,OAAO,GAAGK,qBACb,SACA,IAAI,CAACN,mBAAmB;QAE1BnD,MAAMoD,OAAO,GAAGK,qBACd,UACA,IAAI,CAACF,oBAAoB;QAG3BxD,KAAKuD,GAAG,GAAGgB,iBAAiBvE,KAAKqD,OAAO;QACxCpD,MAAMsD,GAAG,GAAGgB,iBACVtE,MAAMoD,OAAO;IAEjB;IAEA;;GAEC,GACD,AAAQtC,eAAqB;QAC3B,IAAI,IAAI,CAACG,aAAa,EAAE;YACtBC,WAAWC,KAAK,GAAG,IAAI,CAACF,aAAa;YACrC,IAAI,CAACA,aAAa,GAAG;QACvB;IACF;IAEA;;GAEC,GACD,AAAQF,8BAAoC;QAC1C,IAAI,IAAI,CAACoC,mBAAmB,EAAE;YAC5BpD,KAAKqD,OAAO,GAAG,IAAI,CAACD,mBAAmB;YACvC,IAAI,CAACA,mBAAmB,GAAG;QAC7B;QAEA,IAAI,IAAI,CAACE,eAAe,EAAE;YACxBtD,KAAKuD,GAAG,GAAG,IAAI,CAACD,eAAe;YAC/B,IAAI,CAACA,eAAe,GAAG;QACzB;QAEA,IAAI,IAAI,CAACE,oBAAoB,EAAE;YAC7BvD,MAAMoD,OAAO,GAAG,IAAI,CAACG,oBAAoB;YACzC,IAAI,CAACA,oBAAoB,GAAG;QAC9B;QAEA,IAAI,IAAI,CAACC,gBAAgB,EAAE;YACzBxD,MAAMsD,GAAG,GAAG,IAAI,CAACE,gBAAgB;YACjC,IAAI,CAACA,gBAAgB,GAAG;QAC1B;IACF;IA9PAkB,YAAYtD,YAA0B,CAAE;aARhCH,gBAAgD;aAChDkC,sBAAkD;aAClDE,kBAA0C;aAC1CE,uBAAoD;aACpDC,mBAA4C;aAE5C9C,SAAS;QAGf,IAAI,CAACU,YAAY,GAAGA;IACtB;AA6PF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing-table.config.d.ts","sourceRoot":"","sources":["../../src/interceptor/routing-table.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,eAAO,MAAM,qBAAqB,EAAE,YAAY,EAqB/C,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULT_PORTS } from '../shared/constants.js';
|
|
2
|
+
export const defaultRoutingEntries = [
|
|
3
|
+
// Gmail twin
|
|
4
|
+
{
|
|
5
|
+
hostname: 'gmail.googleapis.com',
|
|
6
|
+
target: `http://localhost:${DEFAULT_PORTS.GMAIL}`
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
hostname: 'oauth2.googleapis.com',
|
|
10
|
+
target: `http://localhost:${DEFAULT_PORTS.GMAIL}`
|
|
11
|
+
},
|
|
12
|
+
// YouTube twin — Data API
|
|
13
|
+
{
|
|
14
|
+
hostname: 'www.googleapis.com',
|
|
15
|
+
target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,
|
|
16
|
+
pathPrefix: '/youtube/'
|
|
17
|
+
},
|
|
18
|
+
// YouTube twin — IFrame API (browser CDN)
|
|
19
|
+
{
|
|
20
|
+
hostname: 'www.youtube.com',
|
|
21
|
+
target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=routing-table.config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/interceptor/routing-table.config.ts"],"sourcesContent":["import { RoutingEntry } from './routing-table.js';\nimport { DEFAULT_PORTS } from '../shared/constants.js';\n\nexport const defaultRoutingEntries: RoutingEntry[] = [\n // Gmail twin\n {\n hostname: 'gmail.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.GMAIL}`,\n },\n {\n hostname: 'oauth2.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.GMAIL}`,\n },\n // YouTube twin — Data API\n {\n hostname: 'www.googleapis.com',\n target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n pathPrefix: '/youtube/',\n },\n // YouTube twin — IFrame API (browser CDN)\n {\n hostname: 'www.youtube.com',\n target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`,\n },\n];\n"],"names":["DEFAULT_PORTS","defaultRoutingEntries","hostname","target","GMAIL","YOUTUBE","pathPrefix"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;","mappings":"AACA,SAASA,aAAa,QAAQ,yBAAyB;AAEvD,OAAO,MAAMC,wBAAwC;IACnD,aAAa;IACb;QACEC,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcI,KAAK,CAAC,CAAC;IACnD;IACA;QACEF,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcI,KAAK,CAAC,CAAC;IACnD;IACA,0BAA0B;IAC1B;QACEF,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcK,OAAO,CAAC,CAAC;QACnDC,YAAY;IACd;IACA,0CAA0C;IAC1C;QACEJ,UAAU;QACVC,QAAQ,CAAC,iBAAiB,EAAEH,cAAcK,OAAO,CAAC,CAAC;IACrD;CACD,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RoutingEntry {
|
|
2
|
+
/** The target host to intercept (e.g., "gmail.googleapis.com") */
|
|
3
|
+
hostname: string;
|
|
4
|
+
/** The local target URL to rewrite requests to (e.g., "http://localhost:9014") */
|
|
5
|
+
target: string;
|
|
6
|
+
/** Optional path prefix restriction (only intercept paths starting with this) */
|
|
7
|
+
pathPrefix?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class RoutingTable {
|
|
10
|
+
private readonly entries;
|
|
11
|
+
constructor(initialEntries?: RoutingEntry[]);
|
|
12
|
+
addEntry(entry: RoutingEntry): void;
|
|
13
|
+
resolve(urlStr: string): string | null;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=routing-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing-table.d.ts","sourceRoot":"","sources":["../../src/interceptor/routing-table.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;gBAEtD,cAAc,GAAE,YAAY,EAAO;IAM/C,QAAQ,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAWnC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAyBvC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
export class RoutingTable {
|
|
3
|
+
addEntry(entry) {
|
|
4
|
+
const key = entry.hostname.toLowerCase();
|
|
5
|
+
const list = this.entries.get(key) || [];
|
|
6
|
+
list.push(_extends({}, entry, {
|
|
7
|
+
hostname: key,
|
|
8
|
+
target: entry.target.replace(/\/+$/, '')
|
|
9
|
+
}));
|
|
10
|
+
this.entries.set(key, list);
|
|
11
|
+
}
|
|
12
|
+
resolve(urlStr) {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(urlStr);
|
|
15
|
+
const host = url.hostname.toLowerCase();
|
|
16
|
+
const list = this.entries.get(host);
|
|
17
|
+
if (!list) return null;
|
|
18
|
+
for (const entry of list){
|
|
19
|
+
if (!entry.pathPrefix || url.pathname.startsWith(entry.pathPrefix)) {
|
|
20
|
+
const rewritten = new URL(entry.target);
|
|
21
|
+
// Set pathname correctly, preserving subpaths
|
|
22
|
+
let targetPath = rewritten.pathname;
|
|
23
|
+
if (targetPath === '/') {
|
|
24
|
+
targetPath = '';
|
|
25
|
+
}
|
|
26
|
+
rewritten.pathname = targetPath + url.pathname;
|
|
27
|
+
rewritten.search = url.search;
|
|
28
|
+
return rewritten.toString();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
constructor(initialEntries = []){
|
|
37
|
+
this.entries = new Map();
|
|
38
|
+
for (const entry of initialEntries){
|
|
39
|
+
this.addEntry(entry);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=routing-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/interceptor/routing-table.ts"],"sourcesContent":["export interface RoutingEntry {\n /** The target host to intercept (e.g., \"gmail.googleapis.com\") */\n hostname: string;\n /** The local target URL to rewrite requests to (e.g., \"http://localhost:9014\") */\n target: string;\n /** Optional path prefix restriction (only intercept paths starting with this) */\n pathPrefix?: string;\n}\n\nexport class RoutingTable {\n private readonly entries: Map<string, RoutingEntry[]> = new Map();\n\n constructor(initialEntries: RoutingEntry[] = []) {\n for (const entry of initialEntries) {\n this.addEntry(entry);\n }\n }\n\n addEntry(entry: RoutingEntry): void {\n const key = entry.hostname.toLowerCase();\n const list = this.entries.get(key) || [];\n list.push({\n ...entry,\n hostname: key,\n target: entry.target.replace(/\\/+$/, ''), // Strip trailing slashes safely\n });\n this.entries.set(key, list);\n }\n\n resolve(urlStr: string): string | null {\n try {\n const url = new URL(urlStr);\n const host = url.hostname.toLowerCase();\n const list = this.entries.get(host);\n if (!list) return null;\n\n for (const entry of list) {\n if (!entry.pathPrefix || url.pathname.startsWith(entry.pathPrefix)) {\n const rewritten = new URL(entry.target);\n // Set pathname correctly, preserving subpaths\n let targetPath = rewritten.pathname;\n if (targetPath === '/') {\n targetPath = '';\n }\n rewritten.pathname = targetPath + url.pathname;\n rewritten.search = url.search;\n return rewritten.toString();\n }\n }\n return null;\n } catch {\n return null;\n }\n }\n}\n"],"names":["RoutingTable","addEntry","entry","key","hostname","toLowerCase","list","entries","get","push","target","replace","set","resolve","urlStr","url","URL","host","pathPrefix","pathname","startsWith","rewritten","targetPath","search","toString","constructor","initialEntries","Map"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":";AASA,OAAO,MAAMA;IASXC,SAASC,KAAmB,EAAQ;QAClC,MAAMC,MAAMD,MAAME,QAAQ,CAACC,WAAW;QACtC,MAAMC,OAAO,IAAI,CAACC,OAAO,CAACC,GAAG,CAACL,QAAQ,EAAE;QACxCG,KAAKG,IAAI,CAAC,aACLP;YACHE,UAAUD;YACVO,QAAQR,MAAMQ,MAAM,CAACC,OAAO,CAAC,QAAQ;;QAEvC,IAAI,CAACJ,OAAO,CAACK,GAAG,CAACT,KAAKG;IACxB;IAEAO,QAAQC,MAAc,EAAiB;QACrC,IAAI;YACF,MAAMC,MAAM,IAAIC,IAAIF;YACpB,MAAMG,OAAOF,IAAIX,QAAQ,CAACC,WAAW;YACrC,MAAMC,OAAO,IAAI,CAACC,OAAO,CAACC,GAAG,CAACS;YAC9B,IAAI,CAACX,MAAM,OAAO;YAElB,KAAK,MAAMJ,SAASI,KAAM;gBACxB,IAAI,CAACJ,MAAMgB,UAAU,IAAIH,IAAII,QAAQ,CAACC,UAAU,CAAClB,MAAMgB,UAAU,GAAG;oBAClE,MAAMG,YAAY,IAAIL,IAAId,MAAMQ,MAAM;oBACtC,8CAA8C;oBAC9C,IAAIY,aAAaD,UAAUF,QAAQ;oBACnC,IAAIG,eAAe,KAAK;wBACtBA,aAAa;oBACf;oBACAD,UAAUF,QAAQ,GAAGG,aAAaP,IAAII,QAAQ;oBAC9CE,UAAUE,MAAM,GAAGR,IAAIQ,MAAM;oBAC7B,OAAOF,UAAUG,QAAQ;gBAC3B;YACF;YACA,OAAO;QACT,EAAE,UAAM;YACN,OAAO;QACT;IACF;IAzCAC,YAAYC,iBAAiC,EAAE,CAAE;aAFhCnB,UAAuC,IAAIoB;QAG1D,KAAK,MAAMzB,SAASwB,eAAgB;YAClC,IAAI,CAACzB,QAAQ,CAACC;QAChB;IACF;AAsCF"}
|
|
@@ -36,5 +36,9 @@ export declare const ENV_VARS: {
|
|
|
36
36
|
readonly GCS_TWIN_PORT: "GCS_TWIN_PORT";
|
|
37
37
|
/** GCS twin persistent data directory (optional) */
|
|
38
38
|
readonly GCS_TWIN_DATA_DIR: "GCS_TWIN_DATA_DIR";
|
|
39
|
+
/** Gmail twin port override */
|
|
40
|
+
readonly GMAIL_TWIN_PORT: "GMAIL_TWIN_PORT";
|
|
41
|
+
/** YouTube twin port override */
|
|
42
|
+
readonly YOUTUBE_TWIN_PORT: "YOUTUBE_TWIN_PORT";
|
|
39
43
|
};
|
|
40
44
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB,gCAAgC;;IAEhC,iDAAiD;;IAEjD,gCAAgC;;IAEhC,4BAA4B;;IAE5B,oCAAoC;;IAEpC,4BAA4B;;CAEpB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,QAAQ;IACnB,0DAA0D;;IAE1D,6BAA6B;;IAE7B,oDAAoD;;
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB,gCAAgC;;IAEhC,iDAAiD;;IAEjD,gCAAgC;;IAEhC,4BAA4B;;IAE5B,oCAAoC;;IAEpC,4BAA4B;;CAEpB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,QAAQ;IACnB,0DAA0D;;IAE1D,6BAA6B;;IAE7B,oDAAoD;;IAEpD,+BAA+B;;IAE/B,iCAAiC;;CAEzB,CAAC"}
|
package/dist/shared/constants.js
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
*/ export const ENV_VARS = {
|
|
23
23
|
/** Activation gate — must be 'true' for twins to start */ TEST_MODE: 'TEST_MODE',
|
|
24
24
|
/** GCS twin port override */ GCS_TWIN_PORT: 'GCS_TWIN_PORT',
|
|
25
|
-
/** GCS twin persistent data directory (optional) */ GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR'
|
|
25
|
+
/** GCS twin persistent data directory (optional) */ GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR',
|
|
26
|
+
/** Gmail twin port override */ GMAIL_TWIN_PORT: 'GMAIL_TWIN_PORT',
|
|
27
|
+
/** YouTube twin port override */ YOUTUBE_TWIN_PORT: 'YOUTUBE_TWIN_PORT'
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * Shared constants for integration‑test‑doubles library.\n */\n\n/**\n * Port range reserved for twin emulators.\n * Twins should use fixed ports within this range to avoid collisions.\n */\nexport const PORT_RANGE = {\n min: 9010,\n max: 9020,\n} as const;\n\n/**\n * Default port assignments for each twin.\n */\nexport const DEFAULT_PORTS = {\n /** Google Cloud Storage twin */\n GCS: 9013,\n /** Gmail/Google Workspace Email twin (future) */\n GMAIL: 9014,\n /** Google Auth twin (future) */\n GOOGLE_AUTH: 9015,\n /** YouTube twin (future) */\n YOUTUBE: 9016,\n /** Google Calendar twin (future) */\n GOOGLE_CALENDAR: 9017,\n /** Spotify twin (future) */\n SPOTIFY: 9018,\n} as const;\n\n/**\n * Environment variable names used by the library.\n */\nexport const ENV_VARS = {\n /** Activation gate — must be 'true' for twins to start */\n TEST_MODE: 'TEST_MODE',\n /** GCS twin port override */\n GCS_TWIN_PORT: 'GCS_TWIN_PORT',\n /** GCS twin persistent data directory (optional) */\n GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR',\n} as const;\n"],"names":["PORT_RANGE","min","max","DEFAULT_PORTS","GCS","GMAIL","GOOGLE_AUTH","YOUTUBE","GOOGLE_CALENDAR","SPOTIFY","ENV_VARS","TEST_MODE","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR"],"rangeMappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * Shared constants for integration‑test‑doubles library.\n */\n\n/**\n * Port range reserved for twin emulators.\n * Twins should use fixed ports within this range to avoid collisions.\n */\nexport const PORT_RANGE = {\n min: 9010,\n max: 9020,\n} as const;\n\n/**\n * Default port assignments for each twin.\n */\nexport const DEFAULT_PORTS = {\n /** Google Cloud Storage twin */\n GCS: 9013,\n /** Gmail/Google Workspace Email twin (future) */\n GMAIL: 9014,\n /** Google Auth twin (future) */\n GOOGLE_AUTH: 9015,\n /** YouTube twin (future) */\n YOUTUBE: 9016,\n /** Google Calendar twin (future) */\n GOOGLE_CALENDAR: 9017,\n /** Spotify twin (future) */\n SPOTIFY: 9018,\n} as const;\n\n/**\n * Environment variable names used by the library.\n */\nexport const ENV_VARS = {\n /** Activation gate — must be 'true' for twins to start */\n TEST_MODE: 'TEST_MODE',\n /** GCS twin port override */\n GCS_TWIN_PORT: 'GCS_TWIN_PORT',\n /** GCS twin persistent data directory (optional) */\n GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR',\n /** Gmail twin port override */\n GMAIL_TWIN_PORT: 'GMAIL_TWIN_PORT',\n /** YouTube twin port override */\n YOUTUBE_TWIN_PORT: 'YOUTUBE_TWIN_PORT',\n} as const;\n"],"names":["PORT_RANGE","min","max","DEFAULT_PORTS","GCS","GMAIL","GOOGLE_AUTH","YOUTUBE","GOOGLE_CALENDAR","SPOTIFY","ENV_VARS","TEST_MODE","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR","GMAIL_TWIN_PORT","YOUTUBE_TWIN_PORT"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA;;CAEC,GAED;;;CAGC,GACD,OAAO,MAAMA,aAAa;IACxBC,KAAK;IACLC,KAAK;AACP,EAAW;AAEX;;CAEC,GACD,OAAO,MAAMC,gBAAgB;IAC3B,8BAA8B,GAC9BC,KAAK;IACL,+CAA+C,GAC/CC,OAAO;IACP,8BAA8B,GAC9BC,aAAa;IACb,0BAA0B,GAC1BC,SAAS;IACT,kCAAkC,GAClCC,iBAAiB;IACjB,0BAA0B,GAC1BC,SAAS;AACX,EAAW;AAEX;;CAEC,GACD,OAAO,MAAMC,WAAW;IACtB,wDAAwD,GACxDC,WAAW;IACX,2BAA2B,GAC3BC,eAAe;IACf,kDAAkD,GAClDC,mBAAmB;IACnB,6BAA6B,GAC7BC,iBAAiB;IACjB,+BAA+B,GAC/BC,mBAAmB;AACrB,EAAW"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for the YouTube twin.
|
|
3
|
+
*/
|
|
4
|
+
export { YoutubeTwin } from './youtube-twin.js';
|
|
5
|
+
export type { YoutubeTwinConfig } from './youtube-twin.config.js';
|
|
6
|
+
export { defaultYoutubeConfig, createYoutubeConfig, } from './youtube-twin.config.js';
|
|
7
|
+
export { searchFixtures } from './search-fixtures.js';
|
|
8
|
+
export type { VideoFixture } from './search-fixtures.js';
|
|
9
|
+
export { formatSearchResponse } from './search-response.js';
|
|
10
|
+
export { generatePlayerShim } from './player-shim.js';
|
|
11
|
+
export { YoutubeErrorModeManager } from './youtube-error-mode.js';
|
|
12
|
+
export type { YoutubeErrorModeType, YoutubeApiErrorModeType, YoutubePlayerErrorCode, YoutubeErrorModeConfig, YoutubeErrorSimulationResult, } from './youtube-error-mode.js';
|
|
13
|
+
export { getYoutubeMswHandlerConfigs } from './msw-handlers.js';
|
|
14
|
+
export type { MswHandlerConfig } from './msw-handlers.js';
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/youtube/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACL,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EACV,oBAAoB,EACpB,uBAAuB,EACvB,sBAAsB,EACtB,sBAAsB,EACtB,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for the YouTube twin.
|
|
3
|
+
*/ export { YoutubeTwin } from './youtube-twin.js';
|
|
4
|
+
export { defaultYoutubeConfig, createYoutubeConfig } from './youtube-twin.config.js';
|
|
5
|
+
export { searchFixtures } from './search-fixtures.js';
|
|
6
|
+
export { formatSearchResponse } from './search-response.js';
|
|
7
|
+
export { generatePlayerShim } from './player-shim.js';
|
|
8
|
+
export { YoutubeErrorModeManager } from './youtube-error-mode.js';
|
|
9
|
+
export { getYoutubeMswHandlerConfigs } from './msw-handlers.js';
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/index.ts"],"sourcesContent":["/**\n * Public API for the YouTube twin.\n */\nexport { YoutubeTwin } from './youtube-twin.js';\nexport type { YoutubeTwinConfig } from './youtube-twin.config.js';\nexport {\n defaultYoutubeConfig,\n createYoutubeConfig,\n} from './youtube-twin.config.js';\nexport { searchFixtures } from './search-fixtures.js';\nexport type { VideoFixture } from './search-fixtures.js';\nexport { formatSearchResponse } from './search-response.js';\nexport { generatePlayerShim } from './player-shim.js';\nexport { YoutubeErrorModeManager } from './youtube-error-mode.js';\nexport type {\n YoutubeErrorModeType,\n YoutubeApiErrorModeType,\n YoutubePlayerErrorCode,\n YoutubeErrorModeConfig,\n YoutubeErrorSimulationResult,\n} from './youtube-error-mode.js';\nexport { getYoutubeMswHandlerConfigs } from './msw-handlers.js';\nexport type { MswHandlerConfig } from './msw-handlers.js';\n"],"names":["YoutubeTwin","defaultYoutubeConfig","createYoutubeConfig","searchFixtures","formatSearchResponse","generatePlayerShim","YoutubeErrorModeManager","getYoutubeMswHandlerConfigs"],"rangeMappings":";;;;;;;;","mappings":"AAAA;;CAEC,GACD,SAASA,WAAW,QAAQ,oBAAoB;AAEhD,SACEC,oBAAoB,EACpBC,mBAAmB,QACd,2BAA2B;AAClC,SAASC,cAAc,QAAQ,uBAAuB;AAEtD,SAASC,oBAAoB,QAAQ,uBAAuB;AAC5D,SAASC,kBAAkB,QAAQ,mBAAmB;AACtD,SAASC,uBAAuB,QAAQ,0BAA0B;AAQlE,SAASC,2BAA2B,QAAQ,oBAAoB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSW handler configuration for YouTube Twin browser-side interception.
|
|
3
|
+
*
|
|
4
|
+
* Consumers install these handlers in their MSW worker setup.
|
|
5
|
+
* The handler factory does NOT import MSW — it returns plain objects
|
|
6
|
+
* that consumers use with their own MSW installation.
|
|
7
|
+
*
|
|
8
|
+
* Usage in consuming app:
|
|
9
|
+
*
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { http, HttpResponse } from 'msw';
|
|
12
|
+
* import { getYoutubeMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';
|
|
13
|
+
*
|
|
14
|
+
* const configs = getYoutubeMswHandlerConfigs('http://localhost:9016');
|
|
15
|
+
*
|
|
16
|
+
* const handlers = configs.map(config =>
|
|
17
|
+
* http.get(config.url, async () => {
|
|
18
|
+
* const response = await fetch(config.proxyTo);
|
|
19
|
+
* const body = await response.text();
|
|
20
|
+
* return new HttpResponse(body, {
|
|
21
|
+
* headers: { 'Content-Type': config.contentType },
|
|
22
|
+
* });
|
|
23
|
+
* })
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* worker.start({ handlers });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export interface MswHandlerConfig {
|
|
30
|
+
/** The original URL to intercept */
|
|
31
|
+
url: string;
|
|
32
|
+
/** The local twin URL to proxy to */
|
|
33
|
+
proxyTo: string;
|
|
34
|
+
/** Content-Type header for the response */
|
|
35
|
+
contentType: string;
|
|
36
|
+
/** Description for debugging */
|
|
37
|
+
description: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function getYoutubeMswHandlerConfigs(twinBaseUrl?: string): MswHandlerConfig[];
|
|
40
|
+
//# sourceMappingURL=msw-handlers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"msw-handlers.d.ts","sourceRoot":"","sources":["../../src/youtube/msw-handlers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,2BAA2B,CACzC,WAAW,SAA0B,GACpC,gBAAgB,EAAE,CAepB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MSW handler configuration for YouTube Twin browser-side interception.
|
|
3
|
+
*
|
|
4
|
+
* Consumers install these handlers in their MSW worker setup.
|
|
5
|
+
* The handler factory does NOT import MSW — it returns plain objects
|
|
6
|
+
* that consumers use with their own MSW installation.
|
|
7
|
+
*
|
|
8
|
+
* Usage in consuming app:
|
|
9
|
+
*
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { http, HttpResponse } from 'msw';
|
|
12
|
+
* import { getYoutubeMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';
|
|
13
|
+
*
|
|
14
|
+
* const configs = getYoutubeMswHandlerConfigs('http://localhost:9016');
|
|
15
|
+
*
|
|
16
|
+
* const handlers = configs.map(config =>
|
|
17
|
+
* http.get(config.url, async () => {
|
|
18
|
+
* const response = await fetch(config.proxyTo);
|
|
19
|
+
* const body = await response.text();
|
|
20
|
+
* return new HttpResponse(body, {
|
|
21
|
+
* headers: { 'Content-Type': config.contentType },
|
|
22
|
+
* });
|
|
23
|
+
* })
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* worker.start({ handlers });
|
|
27
|
+
* ```
|
|
28
|
+
*/ export function getYoutubeMswHandlerConfigs(twinBaseUrl = 'http://localhost:9016') {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
url: 'https://www.youtube.com/iframe_api',
|
|
32
|
+
proxyTo: `${twinBaseUrl}/iframe_api`,
|
|
33
|
+
contentType: 'application/javascript',
|
|
34
|
+
description: 'YouTube IFrame Player API shim'
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
url: 'https://www.youtube.com/s/player/*/www-widgetapi.vflset/www-widgetapi.js',
|
|
38
|
+
proxyTo: `${twinBaseUrl}/shim/youtube-player.js`,
|
|
39
|
+
contentType: 'application/javascript',
|
|
40
|
+
description: 'YouTube widget API (secondary load)'
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=msw-handlers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/youtube/msw-handlers.ts"],"sourcesContent":["/**\n * MSW handler configuration for YouTube Twin browser-side interception.\n *\n * Consumers install these handlers in their MSW worker setup.\n * The handler factory does NOT import MSW — it returns plain objects\n * that consumers use with their own MSW installation.\n *\n * Usage in consuming app:\n *\n * ```typescript\n * import { http, HttpResponse } from 'msw';\n * import { getYoutubeMswHandlerConfigs } from '@open-kingdom/shared-backend-integration-test-doubles';\n *\n * const configs = getYoutubeMswHandlerConfigs('http://localhost:9016');\n *\n * const handlers = configs.map(config =>\n * http.get(config.url, async () => {\n * const response = await fetch(config.proxyTo);\n * const body = await response.text();\n * return new HttpResponse(body, {\n * headers: { 'Content-Type': config.contentType },\n * });\n * })\n * );\n *\n * worker.start({ handlers });\n * ```\n */\n\nexport interface MswHandlerConfig {\n /** The original URL to intercept */\n url: string;\n /** The local twin URL to proxy to */\n proxyTo: string;\n /** Content-Type header for the response */\n contentType: string;\n /** Description for debugging */\n description: string;\n}\n\nexport function getYoutubeMswHandlerConfigs(\n twinBaseUrl = 'http://localhost:9016'\n): MswHandlerConfig[] {\n return [\n {\n url: 'https://www.youtube.com/iframe_api',\n proxyTo: `${twinBaseUrl}/iframe_api`,\n contentType: 'application/javascript',\n description: 'YouTube IFrame Player API shim',\n },\n {\n url: 'https://www.youtube.com/s/player/*/www-widgetapi.vflset/www-widgetapi.js',\n proxyTo: `${twinBaseUrl}/shim/youtube-player.js`,\n contentType: 'application/javascript',\n description: 'YouTube widget API (secondary load)',\n },\n ];\n}\n"],"names":["getYoutubeMswHandlerConfigs","twinBaseUrl","url","proxyTo","contentType","description"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BC,GAaD,OAAO,SAASA,4BACdC,cAAc,uBAAuB;IAErC,OAAO;QACL;YACEC,KAAK;YACLC,SAAS,CAAC,EAAEF,YAAY,WAAW,CAAC;YACpCG,aAAa;YACbC,aAAa;QACf;QACA;YACEH,KAAK;YACLC,SAAS,CAAC,EAAEF,YAAY,uBAAuB,CAAC;YAChDG,aAAa;YACbC,aAAa;QACf;KACD;AACH"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const SHIM_TEMPLATE = "(function() {\n 'use strict';\n\n var TWIN_BASE = '__TWIN_BASE_URL__';\n\n var PlayerState = {\n UNSTARTED: -1,\n ENDED: 0,\n PLAYING: 1,\n PAUSED: 2,\n BUFFERING: 3,\n CUED: 5\n };\n\n function Player(elementIdOrElement, options) {\n options = options || {};\n\n // Resolve target container\n this._container = typeof elementIdOrElement === 'string'\n ? document.getElementById(elementIdOrElement)\n : elementIdOrElement;\n\n if (!this._container) {\n throw new Error('YT.Player: target element not found: ' + elementIdOrElement);\n }\n\n this._options = options;\n this._state = PlayerState.UNSTARTED;\n this._volume = 100;\n this._muted = false;\n this._hasError = false;\n this._errorCode = null;\n this._videoData = {\n video_id: options.videoId || '',\n title: '',\n author: ''\n };\n\n // Create HTML5 <video> element\n this._video = document.createElement('video');\n this._video.src = TWIN_BASE + '/test-assets/sample.mp4';\n this._video.width = options.width || 640;\n this._video.height = options.height || 360;\n this._video.controls = !!(options.playerVars && options.playerVars.controls !== 0);\n this._video.playsInline = !!(options.playerVars && options.playerVars.playsinline);\n this._video.style.width = '100%';\n this._video.style.height = '100%';\n this._video.style.backgroundColor = '#000';\n this._video.style.display = 'block';\n\n // Override video.play to check error state\n var self = this;\n var originalPlay = this._video.play;\n this._video.play = function() {\n if (self._hasError) {\n return Promise.reject(new Error('Playback blocked due to player error ' + self._errorCode));\n }\n return originalPlay.apply(this, arguments);\n };\n\n // Create metadata overlay\n this._overlay = document.createElement('div');\n this._overlay.style.cssText =\n 'position:absolute;bottom:0;left:0;right:0;padding:8px 12px;' +\n 'background:linear-gradient(transparent, rgba(0,0,0,0.8));' +\n 'color:#fff;font-size:14px;font-family:Roboto,Arial,sans-serif;' +\n 'pointer-events:none;z-index:10;';\n this._loadVideoMetadata(options.videoId);\n\n // Wrap in positioned container\n this._wrapper = document.createElement('div');\n this._wrapper.style.cssText = 'position:relative;overflow:hidden;width:' + (options.width ? options.width + 'px' : '100%') + ';height:' + (options.height ? options.height + 'px' : '100%') + ';';\n this._wrapper.appendChild(this._video);\n this._wrapper.appendChild(this._overlay);\n\n // Replace target element content\n this._container.innerHTML = '';\n this._container.appendChild(this._wrapper);\n\n // Wire HTML5 video events -> YT state changes\n this._video.addEventListener('play', function() {\n if (self._hasError) {\n self._video.pause();\n }\n });\n this._video.addEventListener('playing', function() {\n if (self._hasError) {\n self._video.pause();\n return;\n }\n self._state = PlayerState.PLAYING;\n self._fireStateChange();\n });\n this._video.addEventListener('pause', function() {\n if (self._video.ended) return; // 'ended' fires separately\n if (self._hasError) return;\n self._state = PlayerState.PAUSED;\n self._fireStateChange();\n });\n this._video.addEventListener('ended', function() {\n if (self._hasError) return;\n self._state = PlayerState.ENDED;\n self._fireStateChange();\n });\n this._video.addEventListener('waiting', function() {\n if (self._hasError) return;\n self._state = PlayerState.BUFFERING;\n self._fireStateChange();\n });\n\n // Fire onReady asynchronously (matches real YT behavior)\n setTimeout(function() {\n if (options.events && typeof options.events.onReady === 'function') {\n options.events.onReady({ target: self });\n }\n }, 100);\n\n }\n\n Player.prototype._loadVideoMetadata = function(videoId) {\n if (!videoId) {\n this._overlay.textContent = 'YouTube Twin Player';\n return;\n }\n\n var self = this;\n\n // Check error-mode control endpoint first\n fetch(TWIN_BASE + '/test/youtube/error-mode')\n .then(function(res) { return res.json(); })\n .then(function(data) {\n if (data.active && data.mode && data.mode.startsWith('player-error-')) {\n var code = parseInt(data.mode.split('-')[2], 10);\n setTimeout(function() {\n self._fireError(code);\n }, 200);\n return;\n }\n\n // Auto-play if configured and no error\n if (self._options.playerVars && self._options.playerVars.autoplay === 1) {\n self._video.play().catch(function() {});\n }\n\n // Fetch actual mock metadata\n fetch(TWIN_BASE + '/youtube/v3/search?q=' + encodeURIComponent(videoId) +\n '&type=video&maxResults=1&key=shim&part=snippet')\n .then(function(res) {\n if (!res.ok) throw new Error('Search failed');\n return res.json();\n })\n .then(function(searchData) {\n if (searchData.items && searchData.items.length > 0) {\n var snippet = searchData.items[0].snippet;\n self._videoData = {\n video_id: videoId,\n title: snippet.title,\n author: snippet.channelTitle\n };\n self._overlay.textContent = snippet.title + ' - ' + snippet.channelTitle;\n } else {\n self._overlay.textContent = 'Video: ' + videoId;\n }\n })\n .catch(function() {\n self._overlay.textContent = 'Video: ' + videoId;\n });\n })\n .catch(function() {\n // Auto-play if configured and endpoint query fails\n if (self._options.playerVars && self._options.playerVars.autoplay === 1) {\n self._video.play().catch(function() {});\n }\n self._overlay.textContent = 'Video: ' + videoId;\n });\n };\n\n Player.prototype._fireStateChange = function() {\n if (this._options.events && typeof this._options.events.onStateChange === 'function') {\n this._options.events.onStateChange({ target: this, data: this._state });\n }\n };\n\n Player.prototype._fireError = function(code) {\n this._hasError = true;\n this._errorCode = code;\n this._state = PlayerState.UNSTARTED;\n\n // Pause and clean up video\n if (this._video) {\n this._video.pause();\n this._video.src = '';\n try {\n this._video.removeAttribute('src');\n this._video.load();\n } catch (e) {}\n }\n\n var desc = 'An unknown player error occurred.';\n if (code === 2) desc = 'The request contains an invalid parameter value.';\n else if (code === 5) desc = 'The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.';\n else if (code === 100) desc = 'The video requested was not found (removed or marked as private).';\n else if (code === 101 || code === 150) desc = 'The owner of the requested video does not allow it to be played in embedded players.';\n\n if (this._overlay) {\n this._overlay.style.cssText =\n 'position:absolute;top:0;left:0;right:0;bottom:0;padding:24px;' +\n 'background:#151515;color:#f1f1f1;font-size:14px;' +\n 'font-family:Roboto,Arial,sans-serif;display:flex;' +\n 'flex-direction:column;align-items:center;justify-content:center;' +\n 'text-align:center;pointer-events:auto;z-index:10;box-sizing:border-box;';\n this._overlay.innerHTML =\n '<div style=\"font-size:28px;margin-bottom:8px;\">\u26A0\uFE0F</div>' +\n '<div style=\"font-weight:bold;margin-bottom:4px;color:#fff;\">An error occurred.</div>' +\n '<div style=\"font-size:12px;color:#aaa;line-height:1.4;max-width:280px;\">' + desc + ' (Error Code: ' + code + ')</div>';\n }\n\n if (this._options.events && typeof this._options.events.onError === 'function') {\n this._options.events.onError({ target: this, data: code });\n }\n };\n\n // Playback controls\n Player.prototype.playVideo = function() {\n if (this._hasError) return;\n this._video.play().catch(function() {});\n };\n Player.prototype.pauseVideo = function() {\n if (this._hasError) return;\n this._video.pause();\n };\n Player.prototype.stopVideo = function() {\n if (this._hasError) return;\n this._video.pause();\n this._video.currentTime = 0;\n };\n Player.prototype.seekTo = function(seconds) {\n if (this._hasError) return;\n this._video.currentTime = seconds;\n };\n\n // Volume controls\n Player.prototype.setVolume = function(vol) {\n this._volume = vol;\n this._video.volume = vol / 100;\n };\n Player.prototype.getVolume = function() { return this._volume; };\n Player.prototype.mute = function() { this._muted = true; this._video.muted = true; };\n Player.prototype.unMute = function() { this._muted = false; this._video.muted = false; };\n Player.prototype.isMuted = function() { return this._muted; };\n\n // State queries\n Player.prototype.getPlayerState = function() { return this._state; };\n Player.prototype.getCurrentTime = function() { return this._video.currentTime; };\n Player.prototype.getDuration = function() { return this._video.duration || 0; };\n Player.prototype.getVideoUrl = function() {\n return 'https://www.youtube.com/watch?v=' + (this._options.videoId || 'test');\n };\n Player.prototype.getVideoData = function() { return this._videoData; };\n\n // Sizing\n Player.prototype.setSize = function(w, h) {\n this._video.width = w;\n this._video.height = h;\n if (this._wrapper) {\n this._wrapper.style.width = w + 'px';\n this._wrapper.style.height = h + 'px';\n }\n };\n\n // Cleanup\n Player.prototype.destroy = function() {\n this._video.pause();\n this._video.src = '';\n this._video.load();\n if (this._container) {\n this._container.innerHTML = '';\n }\n };\n\n // Install globals\n window.YT = {\n Player: Player,\n PlayerState: PlayerState\n };\n\n // Fire the API-ready callback\n if (typeof window.onYouTubeIframeAPIReady === 'function') {\n setTimeout(function() { window.onYouTubeIframeAPIReady(); }, 0);\n }\n})();";
|
|
2
|
+
export declare function generatePlayerShim(twinBaseUrl: string): string;
|
|
3
|
+
//# sourceMappingURL=player-shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"player-shim.d.ts","sourceRoot":"","sources":["../../src/youtube/player-shim.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,ouUAkSpB,CAAC;AAEP,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D"}
|