@open-kingdom/shared-backend-integration-test-doubles 0.0.2-18 → 0.0.2-21
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 +39 -0
- package/dist/apple-music/apple-music-error-mode.d.ts +17 -0
- package/dist/apple-music/apple-music-error-mode.d.ts.map +1 -0
- package/dist/apple-music/apple-music-error-mode.js +61 -0
- package/dist/apple-music/apple-music-error-mode.js.map +1 -0
- package/dist/apple-music/apple-music-twin.config.d.ts +9 -0
- package/dist/apple-music/apple-music-twin.config.d.ts.map +1 -0
- package/dist/apple-music/apple-music-twin.config.js +25 -0
- package/dist/apple-music/apple-music-twin.config.js.map +1 -0
- package/dist/apple-music/apple-music-twin.d.ts +17 -0
- package/dist/apple-music/apple-music-twin.d.ts.map +1 -0
- package/dist/apple-music/apple-music-twin.js +325 -0
- package/dist/apple-music/apple-music-twin.js.map +1 -0
- package/dist/apple-music/catalog-fixtures.d.ts +19 -0
- package/dist/apple-music/catalog-fixtures.d.ts.map +1 -0
- package/dist/apple-music/catalog-fixtures.js +94 -0
- package/dist/apple-music/catalog-fixtures.js.map +1 -0
- package/dist/apple-music/catalog-response.d.ts +59 -0
- package/dist/apple-music/catalog-response.d.ts.map +1 -0
- package/dist/apple-music/catalog-response.js +63 -0
- package/dist/apple-music/catalog-response.js.map +1 -0
- package/dist/apple-music/index.d.ts +8 -0
- package/dist/apple-music/index.d.ts.map +1 -0
- package/dist/apple-music/index.js +6 -0
- package/dist/apple-music/index.js.map +1 -0
- package/dist/apple-music/msw-handlers.d.ts +12 -0
- package/dist/apple-music/msw-handlers.d.ts.map +1 -0
- package/dist/apple-music/msw-handlers.js +18 -0
- package/dist/apple-music/msw-handlers.js.map +1 -0
- package/dist/apple-music/player-shim.d.ts +3 -0
- package/dist/apple-music/player-shim.d.ts.map +1 -0
- package/dist/apple-music/player-shim.js +310 -0
- package/dist/apple-music/player-shim.js.map +1 -0
- package/dist/artwork/playlist-001.svg +13 -0
- package/dist/artwork/playlist-002.svg +13 -0
- package/dist/artwork/track-001.svg +12 -0
- package/dist/artwork/track-002.svg +12 -0
- package/dist/artwork/track-003.svg +13 -0
- package/dist/artwork/track-004.svg +13 -0
- package/dist/artwork/track-005.svg +13 -0
- package/dist/artwork/track-006.svg +13 -0
- package/dist/artwork/track-007.svg +13 -0
- package/dist/audio/mock-1.mp3 +0 -0
- package/dist/audio/mock-2.mp3 +0 -0
- package/dist/audio/mock-3.mp3 +0 -0
- package/dist/gcs/gcs-error-mode.js +1 -1
- package/dist/gcs/gcs-error-mode.js.map +1 -1
- package/dist/gcs/gcs-error-simulation.interceptor.js.map +1 -1
- package/dist/gcs/gcs-twin.config.js.map +1 -1
- package/dist/gcs/gcs-twin.js +1 -1
- package/dist/gcs/gcs-twin.js.map +1 -1
- package/dist/gcs/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptor/http-helper.js.map +1 -1
- package/dist/interceptor/index.js.map +1 -1
- package/dist/interceptor/node-interceptor.js.map +1 -1
- package/dist/interceptor/routing-table.config.d.ts.map +1 -1
- package/dist/interceptor/routing-table.config.js +10 -0
- package/dist/interceptor/routing-table.config.js.map +1 -1
- package/dist/interceptor/routing-table.js +1 -1
- package/dist/interceptor/routing-table.js.map +1 -1
- package/dist/lib/integration-test-doubles.js.map +1 -1
- package/dist/shared/activation.js.map +1 -1
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/constants.d.ts +4 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +4 -2
- package/dist/shared/constants.js.map +1 -1
- package/dist/youtube/index.js.map +1 -1
- package/dist/youtube/msw-handlers.js.map +1 -1
- package/dist/youtube/player-shim.js.map +1 -1
- package/dist/youtube/search-fixtures.js.map +1 -1
- package/dist/youtube/search-response.js.map +1 -1
- package/dist/youtube/youtube-error-mode.js.map +1 -1
- package/dist/youtube/youtube-twin.config.js.map +1 -1
- package/dist/youtube/youtube-twin.js +5 -5
- package/dist/youtube/youtube-twin.js.map +1 -1
- package/package.json +23 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/apple-music/player-shim.ts"],"sourcesContent":["export const SHIM_TEMPLATE = `(function() {\n 'use strict';\n \n var PlaybackStates = {\n none: 0,\n loading: 1,\n playing: 2,\n paused: 3,\n stopped: 4,\n ended: 5,\n seeking: 6,\n waiting: 7,\n stalled: 8,\n completed: 9\n };\n\n var PlayerShuffleMode = {\n off: 0,\n songs: 1,\n albums: 2\n };\n\n var instance = null;\n\n function MusicKitInstance() {\n this.musicUserToken = null;\n this.developerToken = null;\n this.playbackState = PlaybackStates.stopped;\n this.nowPlayingItem = null;\n this.shuffleMode = PlayerShuffleMode.off;\n this.isAuthorized = true;\n this._listeners = {};\n this._queue = [];\n this._currentIndex = -1;\n this.currentPlaybackTime = 0;\n this.currentPlaybackDuration = 0;\n\n // Add HTML5 audio player\n this._audio = document.createElement('audio');\n this._audio.preload = 'auto';\n document.body.appendChild(this._audio);\n }\n\n MusicKitInstance.prototype.addEventListener = function(event, callback) {\n if (!this._listeners[event]) {\n this._listeners[event] = [];\n }\n this._listeners[event].push(callback);\n };\n\n MusicKitInstance.prototype.removeEventListener = function(event, callback) {\n if (!this._listeners[event]) return;\n this._listeners[event] = this._listeners[event].filter(function(cb) {\n return cb !== callback;\n });\n };\n\n MusicKitInstance.prototype._trigger = function(event) {\n if (this._listeners[event]) {\n this._listeners[event].forEach(function(callback) {\n try { callback(); } catch (e) { console.error(e); }\n });\n }\n };\n\n MusicKitInstance.prototype.setQueue = function(options) {\n var self = this;\n return new Promise(function(resolve) {\n console.log('[MusicKit Shim] setQueue:', options);\n var preloaded = options._preloadedAttributes || {};\n\n if (options.song) {\n self._queue = [{ id: options.song, type: 'songs', attributes: preloaded[options.song] || null }];\n self._currentIndex = 0;\n } else if (options.songs) {\n self._queue = options.songs.map(function(id) {\n return { id: id, type: 'songs', attributes: preloaded[id] || null };\n });\n self._currentIndex = 0;\n } else if (options.playlist) {\n fetch('__TWIN_BASE_URL__/v1/catalog/us/playlists/' + options.playlist)\n .then(function(res) { return res.json(); })\n .then(function(data) {\n if (data && data.data && data.data[0] && data.data[0].relationships && data.data[0].relationships.tracks) {\n self._queue = data.data[0].relationships.tracks.data.map(function(track) {\n return {\n id: track.id,\n type: 'songs',\n attributes: track.attributes\n };\n });\n self._currentIndex = 0;\n self._updateNowPlaying();\n resolve();\n } else {\n self._queue = [{ id: 'mock-track-001', type: 'songs', attributes: null }];\n self._currentIndex = 0;\n self._updateNowPlaying();\n resolve();\n }\n })\n .catch(function() {\n self._queue = [{ id: 'mock-track-001', type: 'songs', attributes: null }];\n self._currentIndex = 0;\n self._updateNowPlaying();\n resolve();\n });\n return;\n }\n self._updateNowPlaying();\n resolve();\n });\n };\n\n MusicKitInstance.prototype._updateNowPlaying = function() {\n var item = this._queue[this._currentIndex];\n var self = this;\n if (item) {\n if (item.attributes) {\n this.nowPlayingItem = {\n id: item.id,\n attributes: item.attributes\n };\n if (item.attributes.audioUrl) {\n this._audio.src = item.attributes.audioUrl;\n this._audio.load();\n this.currentPlaybackTime = 0;\n this.currentPlaybackDuration = 0;\n this._trigger('playbackTimeDidChange');\n this._trigger('playbackDurationDidChange');\n }\n this._trigger('nowPlayingItemDidChange');\n } else {\n fetch('__TWIN_BASE_URL__/v1/catalog/us/songs/' + item.id)\n .then(function(res) { return res.json(); })\n .then(function(data) {\n if (data && data.data && data.data[0]) {\n // Cache attributes back on the queue item so skips don't re-fetch\n self._queue[self._currentIndex].attributes = data.data[0].attributes;\n self.nowPlayingItem = {\n id: item.id,\n attributes: data.data[0].attributes\n };\n if (data.data[0].attributes.audioUrl) {\n self._audio.src = data.data[0].attributes.audioUrl;\n self._audio.load();\n self.currentPlaybackTime = 0;\n self.currentPlaybackDuration = 0;\n self._trigger('playbackTimeDidChange');\n self._trigger('playbackDurationDidChange');\n }\n self._trigger('nowPlayingItemDidChange');\n }\n });\n return;\n }\n } else {\n this.nowPlayingItem = null;\n this._audio.src = '';\n this.currentPlaybackTime = 0;\n this.currentPlaybackDuration = 0;\n this._trigger('playbackTimeDidChange');\n this._trigger('playbackDurationDidChange');\n this._trigger('nowPlayingItemDidChange');\n }\n };\n\n MusicKitInstance.prototype.changeToMediaAtIndex = function(index) {\n var self = this;\n return new Promise(function(resolve, reject) {\n if (index >= 0 && index < self._queue.length) {\n self._currentIndex = index;\n self._updateNowPlaying();\n resolve();\n } else {\n reject(new Error('Index out of bounds'));\n }\n });\n };\n\n MusicKitInstance.prototype.changeToMediaItem = function(id) {\n var self = this;\n return new Promise(function(resolve, reject) {\n var index = self._queue.findIndex(function(item) { return item.id === id; });\n if (index !== -1) {\n self._currentIndex = index;\n self._updateNowPlaying();\n resolve();\n } else {\n reject(new Error('Item not in queue'));\n }\n });\n };\n\n MusicKitInstance.prototype.play = function() {\n var self = this;\n return new Promise(function(resolve) {\n self.playbackState = PlaybackStates.loading;\n self._trigger('playbackStateDidChange');\n self._audio.play()\n .then(function() { resolve(); })\n .catch(function(e) {\n console.error('[MusicKit Shim] play() failed:', e);\n resolve();\n });\n });\n };\n\n MusicKitInstance.prototype.pause = function() {\n var self = this;\n return new Promise(function(resolve) {\n self._audio.pause();\n resolve();\n });\n };\n\n MusicKitInstance.prototype.stop = function() {\n var self = this;\n return new Promise(function(resolve) {\n self._audio.pause();\n self._audio.currentTime = 0;\n self.playbackState = PlaybackStates.stopped;\n self._trigger('playbackStateDidChange');\n resolve();\n });\n };\n\n // Bug 5 fix: auto-play after skipping\n MusicKitInstance.prototype.skipToNextItem = function() {\n var self = this;\n return new Promise(function(resolve) {\n if (self._queue.length > 0) {\n if (self.shuffleMode === PlayerShuffleMode.songs) {\n self._currentIndex = Math.floor(Math.random() * self._queue.length);\n } else {\n self._currentIndex = (self._currentIndex + 1) % self._queue.length;\n }\n self._updateNowPlaying();\n // Auto-play after skip\n setTimeout(function() { self._audio.play().catch(function(){}); }, 150);\n }\n resolve();\n });\n };\n\n MusicKitInstance.prototype.skipToPreviousItem = function() {\n var self = this;\n return new Promise(function(resolve) {\n if (self._queue.length > 0) {\n self._currentIndex = (self._currentIndex - 1 + self._queue.length) % self._queue.length;\n self._updateNowPlaying();\n // Auto-play after skip\n setTimeout(function() { self._audio.play().catch(function(){}); }, 150);\n }\n resolve();\n });\n };\n\n window.MusicKit = {\n PlaybackStates: PlaybackStates,\n PlayerShuffleMode: PlayerShuffleMode,\n configure: function(config) {\n return new Promise(function(resolve) {\n instance = new MusicKitInstance();\n instance.developerToken = config.developerToken;\n\n // Wire audio events\n instance._audio.addEventListener('playing', function() {\n instance.playbackState = PlaybackStates.playing;\n instance._trigger('playbackStateDidChange');\n });\n instance._audio.addEventListener('pause', function() {\n if (instance.playbackState !== PlaybackStates.stopped) {\n instance.playbackState = PlaybackStates.paused;\n instance._trigger('playbackStateDidChange');\n }\n });\n instance._audio.addEventListener('ended', function() {\n instance.playbackState = PlaybackStates.ended;\n instance._trigger('playbackStateDidChange');\n });\n instance._audio.addEventListener('waiting', function() {\n instance.playbackState = PlaybackStates.waiting;\n instance._trigger('playbackStateDidChange');\n });\n instance._audio.addEventListener('timeupdate', function() {\n instance.currentPlaybackTime = instance._audio.currentTime;\n instance._trigger('playbackTimeDidChange');\n });\n instance._audio.addEventListener('durationchange', function() {\n instance.currentPlaybackDuration = instance._audio.duration;\n instance._trigger('playbackDurationDidChange');\n });\n\n resolve(instance);\n });\n },\n getInstance: function() {\n return instance;\n }\n };\n\n document.dispatchEvent(new Event('musickitloaded'));\n})();\n`;\n\nexport function generatePlayerShim(baseUrl: string): string {\n return SHIM_TEMPLATE.replace(/__TWIN_BASE_URL__/g, baseUrl);\n}\n"],"names":["SHIM_TEMPLATE","generatePlayerShim","baseUrl","replace"],"mappings":"AAAA,OAAO,MAAMA,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgT9B,CAAC,CAAC;AAEF,OAAO,SAASC,mBAAmBC,OAAe;IAChD,OAAOF,cAAcG,OAAO,CAAC,sBAAsBD;AACrD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#8b5cf6"/>
|
|
5
|
+
<stop offset="100%" stop-color="#831843"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<rect x="96" y="100" width="72" height="44" rx="6" fill="none" stroke="#fff" stroke-width="8"/>
|
|
10
|
+
<path d="M168 122h28l20 22v56h-48z" fill="none" stroke="#fff" stroke-width="8" stroke-linejoin="round"/>
|
|
11
|
+
<path d="M168 122v78M196 122v78" fill="none" stroke="#fff" stroke-width="8"/>
|
|
12
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Playlist 1</text>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#38bdf8"/>
|
|
5
|
+
<stop offset="100%" stop-color="#4338ca"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<path d="M104 108h92M104 136h92M104 164h60" fill="none" stroke="#fff" stroke-width="10" stroke-linecap="round"/>
|
|
10
|
+
<circle cx="176" cy="164" r="22" fill="none" stroke="#fff" stroke-width="8"/>
|
|
11
|
+
<path d="M176 150v28l20 12" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round"/>
|
|
12
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Playlist 2</text>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#f97316"/>
|
|
5
|
+
<stop offset="100%" stop-color="#7c3aed"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<circle cx="150" cy="140" r="52" fill="none" stroke="#fff" stroke-width="10"/>
|
|
10
|
+
<path d="M150 108v64M150 120l36-14v52" fill="none" stroke="#fff" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
|
11
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Track 1</text>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#0ea5e9"/>
|
|
5
|
+
<stop offset="100%" stop-color="#312e81"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<path d="M150 80c-30 22-30 62 0 84 30-22 30-62 0-84z" fill="none" stroke="#fff" stroke-width="10" stroke-linejoin="round"/>
|
|
10
|
+
<path d="M96 132c-8 12-8 26 0 38M204 132c8 12 8 26 0 38" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round"/>
|
|
11
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Track 2</text>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#22c55e"/>
|
|
5
|
+
<stop offset="100%" stop-color="#065f46"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<rect x="102" y="96" width="96" height="72" rx="8" fill="none" stroke="#fff" stroke-width="10"/>
|
|
10
|
+
<circle cx="128" cy="168" r="18" fill="none" stroke="#fff" stroke-width="8"/>
|
|
11
|
+
<circle cx="178" cy="168" r="18" fill="none" stroke="#fff" stroke-width="8"/>
|
|
12
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Track 3</text>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#ec4899"/>
|
|
5
|
+
<stop offset="100%" stop-color="#4c1d95"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<rect x="88" y="88" width="124" height="124" rx="16" fill="none" stroke="#fff" stroke-width="10"/>
|
|
10
|
+
<circle cx="138" cy="150" r="26" fill="none" stroke="#fff" stroke-width="8"/>
|
|
11
|
+
<circle cx="182" cy="150" r="26" fill="none" stroke="#fff" stroke-width="8"/>
|
|
12
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Track 4</text>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#f59e0b"/>
|
|
5
|
+
<stop offset="100%" stop-color="#991b1b"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<path d="M120 190V104l70-24v86" fill="none" stroke="#fff" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
|
|
10
|
+
<circle cx="104" cy="190" r="18" fill="none" stroke="#fff" stroke-width="8"/>
|
|
11
|
+
<circle cx="174" cy="166" r="18" fill="none" stroke="#fff" stroke-width="8"/>
|
|
12
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Track 5</text>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#06b6d4"/>
|
|
5
|
+
<stop offset="100%" stop-color="#1e3a8a"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<rect x="90" y="90" width="120" height="120" rx="10" fill="none" stroke="#fff" stroke-width="10"/>
|
|
10
|
+
<rect x="90" y="90" width="48" height="120" rx="10" fill="#fff" opacity="0.9"/>
|
|
11
|
+
<rect x="90" y="90" width="24" height="120" rx="10" fill="#0ea5e9"/>
|
|
12
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Track 6</text>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#10b981"/>
|
|
5
|
+
<stop offset="100%" stop-color="#14532d"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="300" height="300" fill="url(#g)"/>
|
|
9
|
+
<path d="M96 118c40-34 68 22 108-12v104c-40 34-68-22-108 12z" fill="none" stroke="#fff" stroke-width="10" stroke-linejoin="round"/>
|
|
10
|
+
<circle cx="104" cy="182" r="10" fill="#fff"/>
|
|
11
|
+
<circle cx="186" cy="150" r="10" fill="#fff"/>
|
|
12
|
+
<text x="150" y="250" font-family="sans-serif" font-size="24" font-weight="bold" fill="#fff" text-anchor="middle">Track 7</text>
|
|
13
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -235,12 +235,12 @@
|
|
|
235
235
|
return null;
|
|
236
236
|
}
|
|
237
237
|
matchIntermittentFailure(method, path, config) {
|
|
238
|
+
var _config_every;
|
|
238
239
|
// Only apply to GCS data operations
|
|
239
240
|
if (!isGcsDataOperation(method, path)) {
|
|
240
241
|
return null;
|
|
241
242
|
}
|
|
242
243
|
this.requestCounter++;
|
|
243
|
-
var _config_every;
|
|
244
244
|
const every = (_config_every = config.every) != null ? _config_every : 2;
|
|
245
245
|
if (this.requestCounter % every === 0) {
|
|
246
246
|
return gcsError(500, 'Internal Server Error', 'internalError');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gcs/gcs-error-mode.ts"],"sourcesContent":["/**\n * GCS error mode simulation — pure state management and request matching.\n *\n * The GcsErrorModeManager knows nothing about HTTP, NestJS, or networking.\n * It holds the currently active error mode, matches incoming requests against\n * the mode's criteria, and returns the appropriate GCS-shaped error response.\n *\n * Design decisions:\n * - Single-mode constraint: only one error mode can be active at a time.\n * setMode() replaces any existing mode.\n * - Intermittent-failure uses a deterministic counter (every Nth request fails),\n * not random percentage, for reproducible test results.\n * - reset() and clearMode() clear the mode and counter, ensuring no leakage\n * between test runs.\n */\n\n// ---------------------------------------------------------------------------\n// Type definitions\n// ---------------------------------------------------------------------------\n\n/**\n * The four supported GCS error simulation modes.\n */\nexport type GcsErrorModeType =\n | 'bucket-not-found'\n | 'quota-exceeded'\n | 'permission-denied'\n | 'intermittent-failure';\n\n/**\n * Configuration for bucket-not-found mode.\n * Matches requests targeting a specific bucket name.\n */\nexport interface BucketNotFoundConfig {\n type: 'bucket-not-found';\n /** The bucket name that will trigger the 404 response. */\n bucketName: string;\n}\n\n/**\n * Configuration for quota-exceeded mode.\n * Matches upload/create operations (POST to upload paths).\n */\nexport interface QuotaExceededConfig {\n type: 'quota-exceeded';\n}\n\n/**\n * Configuration for permission-denied mode.\n * Matches all GCS data operations.\n */\nexport interface PermissionDeniedConfig {\n type: 'permission-denied';\n}\n\n/**\n * Configuration for intermittent-failure mode.\n * Every Nth request fails with 500, deterministic counter.\n */\nexport interface IntermittentFailureConfig {\n type: 'intermittent-failure';\n /** Fail every Nth request. Defaults to 2 (every other request). */\n every?: number;\n}\n\n/**\n * Discriminated union of all error mode configurations.\n * The `type` field discriminates which kind of mode is active.\n */\nexport type GcsErrorModeConfig =\n | BucketNotFoundConfig\n | QuotaExceededConfig\n | PermissionDeniedConfig\n | IntermittentFailureConfig;\n\n/**\n * A GCS-shaped error response that the NestJS interceptor will use\n * to short-circuit matching requests.\n */\nexport interface GcsErrorSimulationResult {\n /** HTTP status code to respond with. */\n status: number;\n /** GCS-shaped JSON error body. */\n body: {\n error: {\n code: number;\n message: string;\n errors: Array<{\n domain: string;\n reason: string;\n message?: string;\n }>;\n };\n };\n}\n\n// ---------------------------------------------------------------------------\n// GCS error response factory\n// ---------------------------------------------------------------------------\n\n/** Creates a standard GCS error response shape. */\nfunction gcsError(\n status: number,\n message: string,\n reason: string,\n detailMessage?: string\n): GcsErrorSimulationResult {\n return {\n status,\n body: {\n error: {\n code: status,\n message,\n errors: [\n {\n domain: 'global',\n reason,\n message: detailMessage ?? message,\n },\n ],\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Request-matching helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the bucket name from a GCS API path.\n *\n * Supports both path-style and virtual-hosted-style paths:\n * - `/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/storage/v1/b/my-bucket` → \"my-bucket\"\n * - `/upload/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/download/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/b/my-bucket/o` → \"my-bucket\"\n *\n * Returns undefined if the path doesn't match any known GCS pattern.\n */\nexport function extractBucketName(path: string): string | undefined {\n // Strip leading slash for consistent processing\n const normalized = path.startsWith('/') ? path.slice(1) : path;\n\n // Match /api/gcs/files?bucket=<name> (query-string style)\n const apiGcsBucketMatch = normalized.match(\n /api\\/gcs\\/files[?&]bucket=([^&]+)/\n );\n if (apiGcsBucketMatch) {\n return decodeURIComponent(apiGcsBucketMatch[1]);\n }\n\n // Match /api/gcs/files/<bucket>/<name>/... or /api/gcs/files/<bucket>/<name>\n const apiGcsPathMatch = normalized.match(/api\\/gcs\\/files\\/([^/]+)/);\n if (apiGcsPathMatch) {\n return decodeURIComponent(apiGcsPathMatch[1]);\n }\n\n // Match /api/gcs/upload (uses query param or default bucket — derive from body is not possible here)\n // Uploads via /api/gcs/upload use the bucket in the request body, not URL. Return undefined for now.\n\n // Match patterns with optional prefixes (upload/, download/) followed by\n // the GCS API path structure.\n const patterns = [\n // Full v1 API paths: storage/v1/b/{bucket}/... or upload/storage/v1/b/{bucket}/...\n /(?:upload\\/|download\\/)?storage\\/v1\\/b\\/([^/]+)/,\n // Short form: b/{bucket}/...\n /b\\/([^/]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = normalized.match(pattern);\n if (match) {\n return match[1];\n }\n }\n\n return undefined;\n}\n\n/**\n * Determine if a request is an upload/create operation.\n *\n * Upload operations are POST requests to paths containing \"/upload/\" or\n * POST requests to bucket or object creation endpoints.\n */\nexport function isUploadOperation(method: string, path: string): boolean {\n if (method.toUpperCase() !== 'POST') return false;\n\n const normalized = path.toLowerCase();\n\n // /api/gcs/upload — the NestJS GCS upload endpoint\n if (normalized.includes('/api/gcs/upload')) return true;\n\n // /api/gcs/files/<bucket>/<name>/delete — treated as a write operation for permission-denied\n // (not an upload, but this function is specifically for quota-exceeded which targets uploads)\n\n // Direct upload path prefix (GCS native)\n if (normalized.includes('/upload/storage/')) return true;\n\n // Object insert via bucket's objects collection\n // POST /storage/v1/b/{bucket}/o\n if (normalized.includes('/b/') && normalized.includes('/o')) return true;\n\n // Bucket creation\n // POST /storage/v1/b\n if (normalized.match(/\\/storage\\/v1\\/b\\/?$/)) return true;\n\n return false;\n}\n\n/**\n * Determine if a request is a GCS data operation.\n *\n * GCS data operations include CRUD on buckets and objects.\n * They exclude twin control endpoints (/api/twin/...) and health checks.\n */\nexport function isGcsDataOperation(method: string, path: string): boolean {\n const normalized = path.toLowerCase();\n\n // Exclude twin control endpoints\n if (normalized.startsWith('/api/twin')) return false;\n\n // Exclude error-mode control endpoints (control plane, not data plane)\n if (normalized.startsWith('/api/gcs/error-mode')) return false;\n\n // Exclude health checks and non-GCS paths\n if (\n normalized === '/' ||\n normalized === '/health' ||\n normalized === '/healthz'\n ) {\n return false;\n }\n\n // NestJS GCS controller routes: /api/gcs/*\n if (normalized.startsWith('/api/gcs')) return true;\n\n // Include known GCS API paths\n if (normalized.includes('/storage/v1/')) return true;\n if (normalized.includes('/upload/storage/')) return true;\n if (normalized.includes('/download/storage/')) return true;\n\n // Include paths that match GCS bucket/object patterns\n if (normalized.match(/\\/b\\/[^/]+/)) return true;\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// GcsErrorModeManager\n// ---------------------------------------------------------------------------\n\n/**\n * Pure state manager for GCS error simulation modes.\n *\n * Tracks the active error mode and matches incoming requests against it.\n * Returns null for pass-through when no mode is active.\n *\n * Thread-safety: this class is NOT thread-safe. In NestJS, requests are\n * handled concurrently, so the counter for intermittent-failure mode must\n * be managed with atomic operations or a mutex in the interceptor layer.\n * For now, we use a simple counter since NestJS interceptors run on the\n * event loop and the matching logic is synchronous.\n */\nexport class GcsErrorModeManager {\n private activeMode: GcsErrorModeConfig | null = null;\n private requestCounter = 0;\n\n /**\n * Activate an error mode. Replaces any existing mode (single-mode constraint).\n * Resets the request counter for intermittent-failure mode.\n */\n setMode(config: GcsErrorModeConfig): void {\n this.activeMode = config;\n this.requestCounter = 0;\n }\n\n /**\n * Deactivate the current error mode. Returns to pass-through.\n * Resets the request counter.\n */\n clearMode(): void {\n this.activeMode = null;\n this.requestCounter = 0;\n }\n\n /**\n * Get the currently active error mode configuration.\n * Returns null when no mode is active (pass-through).\n */\n getMode(): GcsErrorModeConfig | null {\n return this.activeMode;\n }\n\n /**\n * Match an incoming request against the active error mode.\n *\n * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)\n * @param path - Request path (e.g., /storage/v1/b/my-bucket/o)\n * @returns GcsErrorSimulationResult if the request matches, null for pass-through\n */\n matchRequest(method: string, path: string): GcsErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'bucket-not-found':\n return this.matchBucketNotFound(method, path, this.activeMode);\n case 'quota-exceeded':\n return this.matchQuotaExceeded(method, path, this.activeMode);\n case 'permission-denied':\n return this.matchPermissionDenied(method, path, this.activeMode);\n case 'intermittent-failure':\n return this.matchIntermittentFailure(method, path, this.activeMode);\n }\n }\n\n /**\n * Simulate a sample error for the currently active mode.\n * This produces the same GcsErrorSimulationResult that the interceptor would\n * return for a matching request, without mutating the request counter.\n *\n * Used by the GET /api/twin/gcs/error-mode endpoint so the frontend can\n * display the actual error response the backend will produce.\n */\n simulateSampleError(): GcsErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'bucket-not-found':\n // Representative: a GET to the configured bucket\n return gcsError(404, 'Not Found', 'notFound');\n case 'quota-exceeded':\n return gcsError(403, 'Quota exceeded', 'quotaExceeded');\n case 'permission-denied':\n return gcsError(403, 'Forbidden', 'forbidden');\n case 'intermittent-failure':\n return gcsError(500, 'Internal Server Error', 'internalError');\n }\n }\n\n /**\n * Reset all state: clear the active mode and reset the request counter.\n * Called by GcsTwin.reset() and GcsTwin.stop() to prevent leakage between tests.\n */\n reset(): void {\n this.activeMode = null;\n this.requestCounter = 0;\n }\n\n // ---------------------------------------------------------------------------\n // Private matching methods\n // ---------------------------------------------------------------------------\n\n private matchBucketNotFound(\n _method: string,\n path: string,\n config: BucketNotFoundConfig\n ): GcsErrorSimulationResult | null {\n const bucket = extractBucketName(path);\n if (bucket === config.bucketName) {\n return gcsError(404, 'Not Found', 'notFound');\n }\n return null;\n }\n\n private matchQuotaExceeded(\n method: string,\n path: string,\n _config: QuotaExceededConfig\n ): GcsErrorSimulationResult | null {\n if (isUploadOperation(method, path)) {\n return gcsError(403, 'Quota exceeded', 'quotaExceeded');\n }\n return null;\n }\n\n private matchPermissionDenied(\n method: string,\n path: string,\n _config: PermissionDeniedConfig\n ): GcsErrorSimulationResult | null {\n if (isGcsDataOperation(method, path)) {\n return gcsError(403, 'Forbidden', 'forbidden');\n }\n return null;\n }\n\n private matchIntermittentFailure(\n method: string,\n path: string,\n config: IntermittentFailureConfig\n ): GcsErrorSimulationResult | null {\n // Only apply to GCS data operations\n if (!isGcsDataOperation(method, path)) {\n return null;\n }\n\n this.requestCounter++;\n const every = config.every ?? 2;\n\n if (this.requestCounter % every === 0) {\n return gcsError(500, 'Internal Server Error', 'internalError');\n }\n return null;\n }\n}\n"],"names":["gcsError","status","message","reason","detailMessage","body","error","code","errors","domain","extractBucketName","path","normalized","startsWith","slice","apiGcsBucketMatch","match","decodeURIComponent","apiGcsPathMatch","patterns","pattern","undefined","isUploadOperation","method","toUpperCase","toLowerCase","includes","isGcsDataOperation","GcsErrorModeManager","setMode","config","activeMode","requestCounter","clearMode","getMode","matchRequest","type","matchBucketNotFound","matchQuotaExceeded","matchPermissionDenied","matchIntermittentFailure","simulateSampleError","reset","_method","bucket","bucketName","_config","every"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/gcs/gcs-error-mode.ts"],"sourcesContent":["/**\n * GCS error mode simulation — pure state management and request matching.\n *\n * The GcsErrorModeManager knows nothing about HTTP, NestJS, or networking.\n * It holds the currently active error mode, matches incoming requests against\n * the mode's criteria, and returns the appropriate GCS-shaped error response.\n *\n * Design decisions:\n * - Single-mode constraint: only one error mode can be active at a time.\n * setMode() replaces any existing mode.\n * - Intermittent-failure uses a deterministic counter (every Nth request fails),\n * not random percentage, for reproducible test results.\n * - reset() and clearMode() clear the mode and counter, ensuring no leakage\n * between test runs.\n */\n\n// ---------------------------------------------------------------------------\n// Type definitions\n// ---------------------------------------------------------------------------\n\n/**\n * The four supported GCS error simulation modes.\n */\nexport type GcsErrorModeType =\n | 'bucket-not-found'\n | 'quota-exceeded'\n | 'permission-denied'\n | 'intermittent-failure';\n\n/**\n * Configuration for bucket-not-found mode.\n * Matches requests targeting a specific bucket name.\n */\nexport interface BucketNotFoundConfig {\n type: 'bucket-not-found';\n /** The bucket name that will trigger the 404 response. */\n bucketName: string;\n}\n\n/**\n * Configuration for quota-exceeded mode.\n * Matches upload/create operations (POST to upload paths).\n */\nexport interface QuotaExceededConfig {\n type: 'quota-exceeded';\n}\n\n/**\n * Configuration for permission-denied mode.\n * Matches all GCS data operations.\n */\nexport interface PermissionDeniedConfig {\n type: 'permission-denied';\n}\n\n/**\n * Configuration for intermittent-failure mode.\n * Every Nth request fails with 500, deterministic counter.\n */\nexport interface IntermittentFailureConfig {\n type: 'intermittent-failure';\n /** Fail every Nth request. Defaults to 2 (every other request). */\n every?: number;\n}\n\n/**\n * Discriminated union of all error mode configurations.\n * The `type` field discriminates which kind of mode is active.\n */\nexport type GcsErrorModeConfig =\n | BucketNotFoundConfig\n | QuotaExceededConfig\n | PermissionDeniedConfig\n | IntermittentFailureConfig;\n\n/**\n * A GCS-shaped error response that the NestJS interceptor will use\n * to short-circuit matching requests.\n */\nexport interface GcsErrorSimulationResult {\n /** HTTP status code to respond with. */\n status: number;\n /** GCS-shaped JSON error body. */\n body: {\n error: {\n code: number;\n message: string;\n errors: Array<{\n domain: string;\n reason: string;\n message?: string;\n }>;\n };\n };\n}\n\n// ---------------------------------------------------------------------------\n// GCS error response factory\n// ---------------------------------------------------------------------------\n\n/** Creates a standard GCS error response shape. */\nfunction gcsError(\n status: number,\n message: string,\n reason: string,\n detailMessage?: string\n): GcsErrorSimulationResult {\n return {\n status,\n body: {\n error: {\n code: status,\n message,\n errors: [\n {\n domain: 'global',\n reason,\n message: detailMessage ?? message,\n },\n ],\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Request-matching helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract the bucket name from a GCS API path.\n *\n * Supports both path-style and virtual-hosted-style paths:\n * - `/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/storage/v1/b/my-bucket` → \"my-bucket\"\n * - `/upload/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/download/storage/v1/b/my-bucket/o` → \"my-bucket\"\n * - `/b/my-bucket/o` → \"my-bucket\"\n *\n * Returns undefined if the path doesn't match any known GCS pattern.\n */\nexport function extractBucketName(path: string): string | undefined {\n // Strip leading slash for consistent processing\n const normalized = path.startsWith('/') ? path.slice(1) : path;\n\n // Match /api/gcs/files?bucket=<name> (query-string style)\n const apiGcsBucketMatch = normalized.match(\n /api\\/gcs\\/files[?&]bucket=([^&]+)/\n );\n if (apiGcsBucketMatch) {\n return decodeURIComponent(apiGcsBucketMatch[1]);\n }\n\n // Match /api/gcs/files/<bucket>/<name>/... or /api/gcs/files/<bucket>/<name>\n const apiGcsPathMatch = normalized.match(/api\\/gcs\\/files\\/([^/]+)/);\n if (apiGcsPathMatch) {\n return decodeURIComponent(apiGcsPathMatch[1]);\n }\n\n // Match /api/gcs/upload (uses query param or default bucket — derive from body is not possible here)\n // Uploads via /api/gcs/upload use the bucket in the request body, not URL. Return undefined for now.\n\n // Match patterns with optional prefixes (upload/, download/) followed by\n // the GCS API path structure.\n const patterns = [\n // Full v1 API paths: storage/v1/b/{bucket}/... or upload/storage/v1/b/{bucket}/...\n /(?:upload\\/|download\\/)?storage\\/v1\\/b\\/([^/]+)/,\n // Short form: b/{bucket}/...\n /b\\/([^/]+)/,\n ];\n\n for (const pattern of patterns) {\n const match = normalized.match(pattern);\n if (match) {\n return match[1];\n }\n }\n\n return undefined;\n}\n\n/**\n * Determine if a request is an upload/create operation.\n *\n * Upload operations are POST requests to paths containing \"/upload/\" or\n * POST requests to bucket or object creation endpoints.\n */\nexport function isUploadOperation(method: string, path: string): boolean {\n if (method.toUpperCase() !== 'POST') return false;\n\n const normalized = path.toLowerCase();\n\n // /api/gcs/upload — the NestJS GCS upload endpoint\n if (normalized.includes('/api/gcs/upload')) return true;\n\n // /api/gcs/files/<bucket>/<name>/delete — treated as a write operation for permission-denied\n // (not an upload, but this function is specifically for quota-exceeded which targets uploads)\n\n // Direct upload path prefix (GCS native)\n if (normalized.includes('/upload/storage/')) return true;\n\n // Object insert via bucket's objects collection\n // POST /storage/v1/b/{bucket}/o\n if (normalized.includes('/b/') && normalized.includes('/o')) return true;\n\n // Bucket creation\n // POST /storage/v1/b\n if (normalized.match(/\\/storage\\/v1\\/b\\/?$/)) return true;\n\n return false;\n}\n\n/**\n * Determine if a request is a GCS data operation.\n *\n * GCS data operations include CRUD on buckets and objects.\n * They exclude twin control endpoints (/api/twin/...) and health checks.\n */\nexport function isGcsDataOperation(method: string, path: string): boolean {\n const normalized = path.toLowerCase();\n\n // Exclude twin control endpoints\n if (normalized.startsWith('/api/twin')) return false;\n\n // Exclude error-mode control endpoints (control plane, not data plane)\n if (normalized.startsWith('/api/gcs/error-mode')) return false;\n\n // Exclude health checks and non-GCS paths\n if (\n normalized === '/' ||\n normalized === '/health' ||\n normalized === '/healthz'\n ) {\n return false;\n }\n\n // NestJS GCS controller routes: /api/gcs/*\n if (normalized.startsWith('/api/gcs')) return true;\n\n // Include known GCS API paths\n if (normalized.includes('/storage/v1/')) return true;\n if (normalized.includes('/upload/storage/')) return true;\n if (normalized.includes('/download/storage/')) return true;\n\n // Include paths that match GCS bucket/object patterns\n if (normalized.match(/\\/b\\/[^/]+/)) return true;\n\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// GcsErrorModeManager\n// ---------------------------------------------------------------------------\n\n/**\n * Pure state manager for GCS error simulation modes.\n *\n * Tracks the active error mode and matches incoming requests against it.\n * Returns null for pass-through when no mode is active.\n *\n * Thread-safety: this class is NOT thread-safe. In NestJS, requests are\n * handled concurrently, so the counter for intermittent-failure mode must\n * be managed with atomic operations or a mutex in the interceptor layer.\n * For now, we use a simple counter since NestJS interceptors run on the\n * event loop and the matching logic is synchronous.\n */\nexport class GcsErrorModeManager {\n private activeMode: GcsErrorModeConfig | null = null;\n private requestCounter = 0;\n\n /**\n * Activate an error mode. Replaces any existing mode (single-mode constraint).\n * Resets the request counter for intermittent-failure mode.\n */\n setMode(config: GcsErrorModeConfig): void {\n this.activeMode = config;\n this.requestCounter = 0;\n }\n\n /**\n * Deactivate the current error mode. Returns to pass-through.\n * Resets the request counter.\n */\n clearMode(): void {\n this.activeMode = null;\n this.requestCounter = 0;\n }\n\n /**\n * Get the currently active error mode configuration.\n * Returns null when no mode is active (pass-through).\n */\n getMode(): GcsErrorModeConfig | null {\n return this.activeMode;\n }\n\n /**\n * Match an incoming request against the active error mode.\n *\n * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)\n * @param path - Request path (e.g., /storage/v1/b/my-bucket/o)\n * @returns GcsErrorSimulationResult if the request matches, null for pass-through\n */\n matchRequest(method: string, path: string): GcsErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'bucket-not-found':\n return this.matchBucketNotFound(method, path, this.activeMode);\n case 'quota-exceeded':\n return this.matchQuotaExceeded(method, path, this.activeMode);\n case 'permission-denied':\n return this.matchPermissionDenied(method, path, this.activeMode);\n case 'intermittent-failure':\n return this.matchIntermittentFailure(method, path, this.activeMode);\n }\n }\n\n /**\n * Simulate a sample error for the currently active mode.\n * This produces the same GcsErrorSimulationResult that the interceptor would\n * return for a matching request, without mutating the request counter.\n *\n * Used by the GET /api/twin/gcs/error-mode endpoint so the frontend can\n * display the actual error response the backend will produce.\n */\n simulateSampleError(): GcsErrorSimulationResult | null {\n if (!this.activeMode) return null;\n\n switch (this.activeMode.type) {\n case 'bucket-not-found':\n // Representative: a GET to the configured bucket\n return gcsError(404, 'Not Found', 'notFound');\n case 'quota-exceeded':\n return gcsError(403, 'Quota exceeded', 'quotaExceeded');\n case 'permission-denied':\n return gcsError(403, 'Forbidden', 'forbidden');\n case 'intermittent-failure':\n return gcsError(500, 'Internal Server Error', 'internalError');\n }\n }\n\n /**\n * Reset all state: clear the active mode and reset the request counter.\n * Called by GcsTwin.reset() and GcsTwin.stop() to prevent leakage between tests.\n */\n reset(): void {\n this.activeMode = null;\n this.requestCounter = 0;\n }\n\n // ---------------------------------------------------------------------------\n // Private matching methods\n // ---------------------------------------------------------------------------\n\n private matchBucketNotFound(\n _method: string,\n path: string,\n config: BucketNotFoundConfig\n ): GcsErrorSimulationResult | null {\n const bucket = extractBucketName(path);\n if (bucket === config.bucketName) {\n return gcsError(404, 'Not Found', 'notFound');\n }\n return null;\n }\n\n private matchQuotaExceeded(\n method: string,\n path: string,\n _config: QuotaExceededConfig\n ): GcsErrorSimulationResult | null {\n if (isUploadOperation(method, path)) {\n return gcsError(403, 'Quota exceeded', 'quotaExceeded');\n }\n return null;\n }\n\n private matchPermissionDenied(\n method: string,\n path: string,\n _config: PermissionDeniedConfig\n ): GcsErrorSimulationResult | null {\n if (isGcsDataOperation(method, path)) {\n return gcsError(403, 'Forbidden', 'forbidden');\n }\n return null;\n }\n\n private matchIntermittentFailure(\n method: string,\n path: string,\n config: IntermittentFailureConfig\n ): GcsErrorSimulationResult | null {\n // Only apply to GCS data operations\n if (!isGcsDataOperation(method, path)) {\n return null;\n }\n\n this.requestCounter++;\n const every = config.every ?? 2;\n\n if (this.requestCounter % every === 0) {\n return gcsError(500, 'Internal Server Error', 'internalError');\n }\n return null;\n }\n}\n"],"names":["gcsError","status","message","reason","detailMessage","body","error","code","errors","domain","extractBucketName","path","normalized","startsWith","slice","apiGcsBucketMatch","match","decodeURIComponent","apiGcsPathMatch","patterns","pattern","undefined","isUploadOperation","method","toUpperCase","toLowerCase","includes","isGcsDataOperation","GcsErrorModeManager","setMode","config","activeMode","requestCounter","clearMode","getMode","matchRequest","type","matchBucketNotFound","matchQuotaExceeded","matchPermissionDenied","matchIntermittentFailure","simulateSampleError","reset","_method","bucket","bucketName","_config","every"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gcs/gcs-error-simulation.interceptor.ts"],"sourcesContent":["import {\n Injectable,\n NestInterceptor,\n ExecutionContext,\n CallHandler,\n Logger,\n HttpException,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { GcsErrorModeManager, isGcsDataOperation } from './gcs-error-mode.js';\n\n/**\n * NestJS interceptor that checks the active GCS error mode and short-circuits\n * matching requests with GCS-shaped error responses.\n *\n * Registered as APP_INTERCEPTOR (global) but exits immediately for non-GCS routes.\n * The route-prefix check is a cheap string comparison before any mode lookup.\n *\n * When an error mode matches, the interceptor throws an HttpException with the\n * GCS error status code (403, 404, 500) so that the frontend receives a proper\n * HTTP error response — not a 200 with an error body.\n */\n@Injectable()\nexport class GcsErrorSimulationInterceptor implements NestInterceptor {\n private readonly logger = new Logger(GcsErrorSimulationInterceptor.name);\n\n constructor(private readonly errorModeManager: GcsErrorModeManager) {}\n\n intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {\n const request = context\n .switchToHttp()\n .getRequest<{ method: string; url: string }>();\n const method: string = request.method;\n const path: string = request.url;\n\n // Fast exit: only intercept GCS data operation routes\n if (!isGcsDataOperation(method, path)) {\n return next.handle();\n }\n\n const result = this.errorModeManager.matchRequest(method, path);\n\n if (result) {\n this.logger.warn(\n `Error mode intercepted ${method} ${path} → ${result.status} (${result.body.error.errors[0].reason})`\n );\n // Throw with the real HTTP status code so the client gets a proper error response\n throw new HttpException(result.body, result.status);\n }\n\n // Debug-level log when a mode is active but this request passed through\n const activeMode = this.errorModeManager.getMode();\n if (activeMode) {\n this.logger.debug(\n `Error mode '${activeMode.type}' active but ${method} ${path} passed through`\n );\n }\n\n return next.handle();\n }\n}\n"],"names":["Injectable","Logger","HttpException","GcsErrorModeManager","isGcsDataOperation","GcsErrorSimulationInterceptor","intercept","context","next","request","switchToHttp","getRequest","method","path","url","handle","result","errorModeManager","matchRequest","logger","warn","status","body","error","errors","reason","activeMode","getMode","debug","type","
|
|
1
|
+
{"version":3,"sources":["../../src/gcs/gcs-error-simulation.interceptor.ts"],"sourcesContent":["import {\n Injectable,\n NestInterceptor,\n ExecutionContext,\n CallHandler,\n Logger,\n HttpException,\n} from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { GcsErrorModeManager, isGcsDataOperation } from './gcs-error-mode.js';\n\n/**\n * NestJS interceptor that checks the active GCS error mode and short-circuits\n * matching requests with GCS-shaped error responses.\n *\n * Registered as APP_INTERCEPTOR (global) but exits immediately for non-GCS routes.\n * The route-prefix check is a cheap string comparison before any mode lookup.\n *\n * When an error mode matches, the interceptor throws an HttpException with the\n * GCS error status code (403, 404, 500) so that the frontend receives a proper\n * HTTP error response — not a 200 with an error body.\n */\n@Injectable()\nexport class GcsErrorSimulationInterceptor implements NestInterceptor {\n private readonly logger = new Logger(GcsErrorSimulationInterceptor.name);\n\n constructor(private readonly errorModeManager: GcsErrorModeManager) {}\n\n intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {\n const request = context\n .switchToHttp()\n .getRequest<{ method: string; url: string }>();\n const method: string = request.method;\n const path: string = request.url;\n\n // Fast exit: only intercept GCS data operation routes\n if (!isGcsDataOperation(method, path)) {\n return next.handle();\n }\n\n const result = this.errorModeManager.matchRequest(method, path);\n\n if (result) {\n this.logger.warn(\n `Error mode intercepted ${method} ${path} → ${result.status} (${result.body.error.errors[0].reason})`\n );\n // Throw with the real HTTP status code so the client gets a proper error response\n throw new HttpException(result.body, result.status);\n }\n\n // Debug-level log when a mode is active but this request passed through\n const activeMode = this.errorModeManager.getMode();\n if (activeMode) {\n this.logger.debug(\n `Error mode '${activeMode.type}' active but ${method} ${path} passed through`\n );\n }\n\n return next.handle();\n }\n}\n"],"names":["Injectable","Logger","HttpException","GcsErrorModeManager","isGcsDataOperation","GcsErrorSimulationInterceptor","intercept","context","next","request","switchToHttp","getRequest","method","path","url","handle","result","errorModeManager","matchRequest","logger","warn","status","body","error","errors","reason","activeMode","getMode","debug","type","name"],"mappings":";;AAAA,SACEA,UAAU,EAIVC,MAAM,EACNC,aAAa,QACR,iBAAiB;AAExB,SAASC,mBAAmB,EAAEC,kBAAkB,QAAQ,sBAAsB;AAc9E,OAAO,MAAMC;IAKXC,UAAUC,OAAyB,EAAEC,IAAiB,EAAuB;QAC3E,MAAMC,UAAUF,QACbG,YAAY,GACZC,UAAU;QACb,MAAMC,SAAiBH,QAAQG,MAAM;QACrC,MAAMC,OAAeJ,QAAQK,GAAG;QAEhC,sDAAsD;QACtD,IAAI,CAACV,mBAAmBQ,QAAQC,OAAO;YACrC,OAAOL,KAAKO,MAAM;QACpB;QAEA,MAAMC,SAAS,IAAI,CAACC,gBAAgB,CAACC,YAAY,CAACN,QAAQC;QAE1D,IAAIG,QAAQ;YACV,IAAI,CAACG,MAAM,CAACC,IAAI,CACd,CAAC,uBAAuB,EAAER,OAAO,CAAC,EAAEC,KAAK,GAAG,EAAEG,OAAOK,MAAM,CAAC,EAAE,EAAEL,OAAOM,IAAI,CAACC,KAAK,CAACC,MAAM,CAAC,EAAE,CAACC,MAAM,CAAC,CAAC,CAAC;YAEvG,kFAAkF;YAClF,MAAM,IAAIvB,cAAcc,OAAOM,IAAI,EAAEN,OAAOK,MAAM;QACpD;QAEA,wEAAwE;QACxE,MAAMK,aAAa,IAAI,CAACT,gBAAgB,CAACU,OAAO;QAChD,IAAID,YAAY;YACd,IAAI,CAACP,MAAM,CAACS,KAAK,CACf,CAAC,YAAY,EAAEF,WAAWG,IAAI,CAAC,aAAa,EAAEjB,OAAO,CAAC,EAAEC,KAAK,eAAe,CAAC;QAEjF;QAEA,OAAOL,KAAKO,MAAM;IACpB;IAjCA,YAAY,AAAiBE,gBAAqC,CAAE;aAAvCA,mBAAAA;aAFZE,SAAS,IAAIlB,OAAOI,8BAA8ByB,IAAI;IAEF;AAkCvE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gcs/gcs-twin.config.ts"],"sourcesContent":["/**\n * GCS twin configuration.\n *\n * Re‑exports the shared GcsTwinConfig and BucketConfig interfaces\n * along with the default configuration.\n */\n\nexport type { GcsTwinConfig, BucketConfig } from '../shared/config.js';\nexport { defaultGcsConfig } from '../shared/config.js';\n"],"names":["defaultGcsConfig"],"
|
|
1
|
+
{"version":3,"sources":["../../src/gcs/gcs-twin.config.ts"],"sourcesContent":["/**\n * GCS twin configuration.\n *\n * Re‑exports the shared GcsTwinConfig and BucketConfig interfaces\n * along with the default configuration.\n */\n\nexport type { GcsTwinConfig, BucketConfig } from '../shared/config.js';\nexport { defaultGcsConfig } from '../shared/config.js';\n"],"names":["defaultGcsConfig"],"mappings":"AAAA;;;;;CAKC,GAGD,SAASA,gBAAgB,QAAQ,sBAAsB"}
|
package/dist/gcs/gcs-twin.js
CHANGED
package/dist/gcs/gcs-twin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gcs/gcs-twin.ts"],"sourcesContent":["import Docker from 'dockerode';\nimport { createGcsConfig, GcsTwinConfig } from '../shared/config.js';\n\n/**\n * GCS twin lifecycle manager.\n *\n * Starts/stops a fake‑gcs‑server Docker container, creates buckets,\n * and sets the GCS_EMULATOR_URL environment variable.\n */\nexport class GcsTwin {\n private readonly config: GcsTwinConfig;\n private readonly docker: Docker;\n private container: Docker.Container | null = null;\n private readonly containerName: string;\n private dockerUnavailable = false;\n\n /**\n * Create a new GCS twin.\n *\n * @param overrides Optional configuration overrides (merged with defaults and env vars).\n * @param docker Optional Docker instance (for testing).\n */\n constructor(overrides?: Partial<GcsTwinConfig>, docker?: Docker) {\n this.config = createGcsConfig(overrides);\n this.docker = docker ?? new Docker();\n this.containerName = `itd-gcs-twin-${this.config.port}`;\n }\n\n /**\n * Start the Docker container and create buckets.\n *\n * @throws {Error} If the container fails to start.\n */\n async start(): Promise<void> {\n console.log(`Starting GCS twin on port ${this.config.port}...`);\n try {\n await this.docker.ping();\n } catch (_err) {\n console.warn(\n `[GcsTwin] Docker is not available. Skipping GCS emulator container startup. GCS emulator will be offline.`\n );\n this.dockerUnavailable = true;\n return;\n }\n\n await this.ensureImageExists();\n await this.ensureContainerRemoved();\n await this.createContainer();\n await this.startContainer();\n await this.waitForHealthy();\n await this.createBuckets();\n process.env.GCS_EMULATOR_URL = this.config.externalUrl;\n console.log(`GCS twin ready at ${this.config.externalUrl}`);\n }\n\n /**\n * Stop and remove the Docker container.\n */\n async stop(): Promise<void> {\n if (this.dockerUnavailable) {\n this.dockerUnavailable = false;\n return;\n }\n if (!this.container) {\n return;\n }\n console.log(`Stopping GCS twin (${this.containerName})...`);\n try {\n await this.container.stop();\n await this.container.remove();\n } catch (err) {\n console.warn('Error stopping container:', err);\n }\n this.container = null;\n delete process.env.GCS_EMULATOR_URL;\n console.log('GCS twin stopped');\n }\n\n /**\n * Reset all data: delete buckets then re‑create them.\n */\n async reset(): Promise<void> {\n if (this.dockerUnavailable) {\n return;\n }\n console.log('Resetting GCS twin...');\n for (const bucket of this.config.buckets) {\n try {\n await fetch(`${this.config.externalUrl}/storage/v1/b/${bucket.name}`, {\n method: 'DELETE',\n });\n } catch (_err) {\n // Bucket may not exist; ignore\n }\n }\n await this.createBuckets();\n console.log('GCS twin reset complete');\n }\n\n /**\n * Get the emulator host URL for SDK configuration.\n */\n getEmulatorHost(): string {\n return this.config.externalUrl;\n }\n\n /**\n * Check if the twin is running and healthy.\n */\n async isHealthy(): Promise<boolean> {\n if (this.dockerUnavailable) {\n return false;\n }\n try {\n const response = await fetch(`${this.config.externalUrl}/storage/v1/b`);\n return response.ok;\n } catch {\n return false;\n }\n }\n\n // --- internal helpers ---\n\n private async ensureContainerRemoved(): Promise<void> {\n try {\n const existing = this.docker.getContainer(this.containerName);\n const info = await existing.inspect().catch(() => null);\n if (info) {\n console.log(`Removing existing container ${this.containerName}...`);\n await existing.stop().catch(() => {\n /* ignore */\n });\n await existing.remove().catch(() => {\n /* ignore */\n });\n }\n } catch (_err) {\n // No existing container\n }\n }\n\n private async ensureImageExists(): Promise<void> {\n const imageName = 'fsouza/fake-gcs-server';\n const images = await this.docker.listImages({\n filters: JSON.stringify({ reference: [imageName] }),\n });\n\n if (images.length === 0) {\n console.log(`Image ${imageName} not found. Pulling...`);\n await this.pullImage(imageName);\n }\n }\n\n private async pullImage(imageName: string): Promise<void> {\n return new Promise((resolve, reject) => {\n this.docker.pull(imageName, {}, (err, stream) => {\n if (err || !stream)\n return reject(\n err ?? new Error('No stream returned from docker.pull')\n );\n this.docker.modem.followProgress(\n stream,\n (err: Error | null, _output: unknown) =>\n err ? reject(err) : resolve(),\n (event: { status?: string }) => {\n if (\n event.status === 'Downloading' ||\n event.status === 'Extracting'\n ) {\n // Optionally log progress here\n }\n }\n );\n });\n });\n }\n\n private async createContainer(): Promise<void> {\n const container = await this.docker.createContainer({\n Image: 'fsouza/fake-gcs-server',\n name: this.containerName,\n HostConfig: {\n PortBindings: {\n '4443/tcp': [{ HostPort: `${this.config.port}` }],\n },\n },\n ExposedPorts: {\n '4443/tcp': {},\n },\n Cmd: [\n '-scheme',\n 'http',\n '-port',\n '4443',\n '-external-url',\n this.config.externalUrl,\n ],\n });\n this.container = container;\n }\n\n private async startContainer(): Promise<void> {\n if (!this.container) {\n throw new Error('Container not created');\n }\n await this.container.start();\n }\n\n private async waitForHealthy(): Promise<void> {\n const maxAttempts = 30;\n const intervalMs = 500;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n if (await this.isHealthy()) {\n // Small delay after health check passes — the container may report\n // healthy before all HTTP methods are ready (common with fake-gcs-server).\n await new Promise((resolve) => setTimeout(resolve, 1000));\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n\n throw new Error(\n `GCS twin did not become healthy within ${\n (maxAttempts * intervalMs) / 1000\n }s`\n );\n }\n\n /**\n * Internal: create buckets. Errors are logged but not thrown.\n */\n private async createBuckets(): Promise<void> {\n console.log('Creating buckets...');\n for (const bucket of this.config.buckets) {\n try {\n const createRes = await this.fetchWithTimeout(\n `${this.config.externalUrl}/storage/v1/b`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name: bucket.name }),\n },\n 10_000\n );\n if (!createRes.ok && createRes.status !== 409) {\n console.warn(\n `Warning: failed to create bucket ${bucket.name}: ${createRes.status} ${createRes.statusText}`\n );\n } else {\n console.log(` Created bucket ${bucket.name}`);\n }\n } catch (err) {\n console.warn(\n `Warning: failed to create bucket ${bucket.name}: ${\n err instanceof Error ? err.message : String(err)\n }`\n );\n }\n }\n console.log('Bucket creation complete');\n }\n\n /**\n * Fetch with a timeout. Aborts the request if it exceeds the given duration.\n */\n private async fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number\n ): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } catch (err) {\n if (controller.signal.aborted) {\n throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"],"names":["Docker","createGcsConfig","GcsTwin","start","console","log","config","port","docker","ping","_err","warn","dockerUnavailable","ensureImageExists","ensureContainerRemoved","createContainer","startContainer","waitForHealthy","createBuckets","process","env","GCS_EMULATOR_URL","externalUrl","stop","container","containerName","remove","err","reset","bucket","buckets","fetch","name","method","getEmulatorHost","isHealthy","response","ok","existing","getContainer","info","inspect","catch","imageName","images","listImages","filters","JSON","stringify","reference","length","pullImage","Promise","resolve","reject","pull","stream","Error","modem","followProgress","_output","event","status","Image","HostConfig","PortBindings","HostPort","ExposedPorts","Cmd","maxAttempts","intervalMs","attempt","setTimeout","createRes","fetchWithTimeout","headers","body","statusText","message","String","url","init","timeoutMs","controller","AbortController","timer","abort","signal","aborted","clearTimeout","constructor","overrides"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":";AAAA,OAAOA,YAAY,YAAY;AAC/B,SAASC,eAAe,QAAuB,sBAAsB;AAErE;;;;;CAKC,GACD,OAAO,MAAMC;IAmBX;;;;GAIC,GACD,MAAMC,QAAuB;QAC3BC,QAAQC,GAAG,CAAC,CAAC,0BAA0B,EAAE,IAAI,CAACC,MAAM,CAACC,IAAI,CAAC,GAAG,CAAC;QAC9D,IAAI;YACF,MAAM,IAAI,CAACC,MAAM,CAACC,IAAI;QACxB,EAAE,OAAOC,MAAM;YACbN,QAAQO,IAAI,CACV,CAAC,yGAAyG,CAAC;YAE7G,IAAI,CAACC,iBAAiB,GAAG;YACzB;QACF;QAEA,MAAM,IAAI,CAACC,iBAAiB;QAC5B,MAAM,IAAI,CAACC,sBAAsB;QACjC,MAAM,IAAI,CAACC,eAAe;QAC1B,MAAM,IAAI,CAACC,cAAc;QACzB,MAAM,IAAI,CAACC,cAAc;QACzB,MAAM,IAAI,CAACC,aAAa;QACxBC,QAAQC,GAAG,CAACC,gBAAgB,GAAG,IAAI,CAACf,MAAM,CAACgB,WAAW;QACtDlB,QAAQC,GAAG,CAAC,CAAC,kBAAkB,EAAE,IAAI,CAACC,MAAM,CAACgB,WAAW,CAAC,CAAC;IAC5D;IAEA;;GAEC,GACD,MAAMC,OAAsB;QAC1B,IAAI,IAAI,CAACX,iBAAiB,EAAE;YAC1B,IAAI,CAACA,iBAAiB,GAAG;YACzB;QACF;QACA,IAAI,CAAC,IAAI,CAACY,SAAS,EAAE;YACnB;QACF;QACApB,QAAQC,GAAG,CAAC,CAAC,mBAAmB,EAAE,IAAI,CAACoB,aAAa,CAAC,IAAI,CAAC;QAC1D,IAAI;YACF,MAAM,IAAI,CAACD,SAAS,CAACD,IAAI;YACzB,MAAM,IAAI,CAACC,SAAS,CAACE,MAAM;QAC7B,EAAE,OAAOC,KAAK;YACZvB,QAAQO,IAAI,CAAC,6BAA6BgB;QAC5C;QACA,IAAI,CAACH,SAAS,GAAG;QACjB,OAAOL,QAAQC,GAAG,CAACC,gBAAgB;QACnCjB,QAAQC,GAAG,CAAC;IACd;IAEA;;GAEC,GACD,MAAMuB,QAAuB;QAC3B,IAAI,IAAI,CAAChB,iBAAiB,EAAE;YAC1B;QACF;QACAR,QAAQC,GAAG,CAAC;QACZ,KAAK,MAAMwB,UAAU,IAAI,CAACvB,MAAM,CAACwB,OAAO,CAAE;YACxC,IAAI;gBACF,MAAMC,MAAM,CAAC,EAAE,IAAI,CAACzB,MAAM,CAACgB,WAAW,CAAC,cAAc,EAAEO,OAAOG,IAAI,CAAC,CAAC,EAAE;oBACpEC,QAAQ;gBACV;YACF,EAAE,OAAOvB,MAAM;YACb,+BAA+B;YACjC;QACF;QACA,MAAM,IAAI,CAACQ,aAAa;QACxBd,QAAQC,GAAG,CAAC;IACd;IAEA;;GAEC,GACD6B,kBAA0B;QACxB,OAAO,IAAI,CAAC5B,MAAM,CAACgB,WAAW;IAChC;IAEA;;GAEC,GACD,MAAMa,YAA8B;QAClC,IAAI,IAAI,CAACvB,iBAAiB,EAAE;YAC1B,OAAO;QACT;QACA,IAAI;YACF,MAAMwB,WAAW,MAAML,MAAM,CAAC,EAAE,IAAI,CAACzB,MAAM,CAACgB,WAAW,CAAC,aAAa,CAAC;YACtE,OAAOc,SAASC,EAAE;QACpB,EAAE,UAAM;YACN,OAAO;QACT;IACF;IAEA,2BAA2B;IAE3B,MAAcvB,yBAAwC;QACpD,IAAI;YACF,MAAMwB,WAAW,IAAI,CAAC9B,MAAM,CAAC+B,YAAY,CAAC,IAAI,CAACd,aAAa;YAC5D,MAAMe,OAAO,MAAMF,SAASG,OAAO,GAAGC,KAAK,CAAC,IAAM;YAClD,IAAIF,MAAM;gBACRpC,QAAQC,GAAG,CAAC,CAAC,4BAA4B,EAAE,IAAI,CAACoB,aAAa,CAAC,GAAG,CAAC;gBAClE,MAAMa,SAASf,IAAI,GAAGmB,KAAK,CAAC;gBAC1B,UAAU,GACZ;gBACA,MAAMJ,SAASZ,MAAM,GAAGgB,KAAK,CAAC;gBAC5B,UAAU,GACZ;YACF;QACF,EAAE,OAAOhC,MAAM;QACb,wBAAwB;QAC1B;IACF;IAEA,MAAcG,oBAAmC;QAC/C,MAAM8B,YAAY;QAClB,MAAMC,SAAS,MAAM,IAAI,CAACpC,MAAM,CAACqC,UAAU,CAAC;YAC1CC,SAASC,KAAKC,SAAS,CAAC;gBAAEC,WAAW;oBAACN;iBAAU;YAAC;QACnD;QAEA,IAAIC,OAAOM,MAAM,KAAK,GAAG;YACvB9C,QAAQC,GAAG,CAAC,CAAC,MAAM,EAAEsC,UAAU,sBAAsB,CAAC;YACtD,MAAM,IAAI,CAACQ,SAAS,CAACR;QACvB;IACF;IAEA,MAAcQ,UAAUR,SAAiB,EAAiB;QACxD,OAAO,IAAIS,QAAQ,CAACC,SAASC;YAC3B,IAAI,CAAC9C,MAAM,CAAC+C,IAAI,CAACZ,WAAW,CAAC,GAAG,CAAChB,KAAK6B;gBACpC,IAAI7B,OAAO,CAAC6B,QACV,OAAOF,OACL3B,cAAAA,MAAO,IAAI8B,MAAM;gBAErB,IAAI,CAACjD,MAAM,CAACkD,KAAK,CAACC,cAAc,CAC9BH,QACA,CAAC7B,KAAmBiC,UAClBjC,MAAM2B,OAAO3B,OAAO0B,WACtB,CAACQ;oBACC,IACEA,MAAMC,MAAM,KAAK,iBACjBD,MAAMC,MAAM,KAAK,cACjB;oBACA,+BAA+B;oBACjC;gBACF;YAEJ;QACF;IACF;IAEA,MAAc/C,kBAAiC;QAC7C,MAAMS,YAAY,MAAM,IAAI,CAAChB,MAAM,CAACO,eAAe,CAAC;YAClDgD,OAAO;YACP/B,MAAM,IAAI,CAACP,aAAa;YACxBuC,YAAY;gBACVC,cAAc;oBACZ,YAAY;wBAAC;4BAAEC,UAAU,CAAC,EAAE,IAAI,CAAC5D,MAAM,CAACC,IAAI,CAAC,CAAC;wBAAC;qBAAE;gBACnD;YACF;YACA4D,cAAc;gBACZ,YAAY,CAAC;YACf;YACAC,KAAK;gBACH;gBACA;gBACA;gBACA;gBACA;gBACA,IAAI,CAAC9D,MAAM,CAACgB,WAAW;aACxB;QACH;QACA,IAAI,CAACE,SAAS,GAAGA;IACnB;IAEA,MAAcR,iBAAgC;QAC5C,IAAI,CAAC,IAAI,CAACQ,SAAS,EAAE;YACnB,MAAM,IAAIiC,MAAM;QAClB;QACA,MAAM,IAAI,CAACjC,SAAS,CAACrB,KAAK;IAC5B;IAEA,MAAcc,iBAAgC;QAC5C,MAAMoD,cAAc;QACpB,MAAMC,aAAa;QAEnB,IAAK,IAAIC,UAAU,GAAGA,UAAUF,aAAaE,UAAW;YACtD,IAAI,MAAM,IAAI,CAACpC,SAAS,IAAI;gBAC1B,mEAAmE;gBACnE,2EAA2E;gBAC3E,MAAM,IAAIiB,QAAQ,CAACC,UAAYmB,WAAWnB,SAAS;gBACnD;YACF;YACA,MAAM,IAAID,QAAQ,CAACC,UAAYmB,WAAWnB,SAASiB;QACrD;QAEA,MAAM,IAAIb,MACR,CAAC,uCAAuC,EACtC,AAACY,cAAcC,aAAc,KAC9B,CAAC,CAAC;IAEP;IAEA;;GAEC,GACD,MAAcpD,gBAA+B;QAC3Cd,QAAQC,GAAG,CAAC;QACZ,KAAK,MAAMwB,UAAU,IAAI,CAACvB,MAAM,CAACwB,OAAO,CAAE;YACxC,IAAI;gBACF,MAAM2C,YAAY,MAAM,IAAI,CAACC,gBAAgB,CAC3C,CAAC,EAAE,IAAI,CAACpE,MAAM,CAACgB,WAAW,CAAC,aAAa,CAAC,EACzC;oBACEW,QAAQ;oBACR0C,SAAS;wBAAE,gBAAgB;oBAAmB;oBAC9CC,MAAM7B,KAAKC,SAAS,CAAC;wBAAEhB,MAAMH,OAAOG,IAAI;oBAAC;gBAC3C,GACA;gBAEF,IAAI,CAACyC,UAAUpC,EAAE,IAAIoC,UAAUX,MAAM,KAAK,KAAK;oBAC7C1D,QAAQO,IAAI,CACV,CAAC,iCAAiC,EAAEkB,OAAOG,IAAI,CAAC,EAAE,EAAEyC,UAAUX,MAAM,CAAC,CAAC,EAAEW,UAAUI,UAAU,CAAC,CAAC;gBAElG,OAAO;oBACLzE,QAAQC,GAAG,CAAC,CAAC,iBAAiB,EAAEwB,OAAOG,IAAI,CAAC,CAAC;gBAC/C;YACF,EAAE,OAAOL,KAAK;gBACZvB,QAAQO,IAAI,CACV,CAAC,iCAAiC,EAAEkB,OAAOG,IAAI,CAAC,EAAE,EAChDL,eAAe8B,QAAQ9B,IAAImD,OAAO,GAAGC,OAAOpD,KAC7C,CAAC;YAEN;QACF;QACAvB,QAAQC,GAAG,CAAC;IACd;IAEA;;GAEC,GACD,MAAcqE,iBACZM,GAAW,EACXC,IAAiB,EACjBC,SAAiB,EACE;QACnB,MAAMC,aAAa,IAAIC;QACvB,MAAMC,QAAQb,WAAW,IAAMW,WAAWG,KAAK,IAAIJ;QACnD,IAAI;YACF,OAAO,MAAMnD,MAAMiD,KAAK,aAAKC;gBAAMM,QAAQJ,WAAWI,MAAM;;QAC9D,EAAE,OAAO5D,KAAK;YACZ,IAAIwD,WAAWI,MAAM,CAACC,OAAO,EAAE;gBAC7B,MAAM,IAAI/B,MAAM,CAAC,WAAW,EAAEuB,IAAI,iBAAiB,EAAEE,UAAU,EAAE,CAAC;YACpE;YACA,MAAMvD;QACR,SAAU;YACR8D,aAAaJ;QACf;IACF;IA3QA;;;;;GAKC,GACDK,YAAYC,SAAkC,EAAEnF,MAAe,CAAE;aAVzDgB,YAAqC;aAErCZ,oBAAoB;QAS1B,IAAI,CAACN,MAAM,GAAGL,gBAAgB0F;QAC9B,IAAI,CAACnF,MAAM,GAAGA,iBAAAA,SAAU,IAAIR;QAC5B,IAAI,CAACyB,aAAa,GAAG,CAAC,aAAa,EAAE,IAAI,CAACnB,MAAM,CAACC,IAAI,CAAC,CAAC;IACzD;AAkQF"}
|
|
1
|
+
{"version":3,"sources":["../../src/gcs/gcs-twin.ts"],"sourcesContent":["import Docker from 'dockerode';\nimport { createGcsConfig, GcsTwinConfig } from '../shared/config.js';\n\n/**\n * GCS twin lifecycle manager.\n *\n * Starts/stops a fake‑gcs‑server Docker container, creates buckets,\n * and sets the GCS_EMULATOR_URL environment variable.\n */\nexport class GcsTwin {\n private readonly config: GcsTwinConfig;\n private readonly docker: Docker;\n private container: Docker.Container | null = null;\n private readonly containerName: string;\n private dockerUnavailable = false;\n\n /**\n * Create a new GCS twin.\n *\n * @param overrides Optional configuration overrides (merged with defaults and env vars).\n * @param docker Optional Docker instance (for testing).\n */\n constructor(overrides?: Partial<GcsTwinConfig>, docker?: Docker) {\n this.config = createGcsConfig(overrides);\n this.docker = docker ?? new Docker();\n this.containerName = `itd-gcs-twin-${this.config.port}`;\n }\n\n /**\n * Start the Docker container and create buckets.\n *\n * @throws {Error} If the container fails to start.\n */\n async start(): Promise<void> {\n console.log(`Starting GCS twin on port ${this.config.port}...`);\n try {\n await this.docker.ping();\n } catch (_err) {\n console.warn(\n `[GcsTwin] Docker is not available. Skipping GCS emulator container startup. GCS emulator will be offline.`\n );\n this.dockerUnavailable = true;\n return;\n }\n\n await this.ensureImageExists();\n await this.ensureContainerRemoved();\n await this.createContainer();\n await this.startContainer();\n await this.waitForHealthy();\n await this.createBuckets();\n process.env.GCS_EMULATOR_URL = this.config.externalUrl;\n console.log(`GCS twin ready at ${this.config.externalUrl}`);\n }\n\n /**\n * Stop and remove the Docker container.\n */\n async stop(): Promise<void> {\n if (this.dockerUnavailable) {\n this.dockerUnavailable = false;\n return;\n }\n if (!this.container) {\n return;\n }\n console.log(`Stopping GCS twin (${this.containerName})...`);\n try {\n await this.container.stop();\n await this.container.remove();\n } catch (err) {\n console.warn('Error stopping container:', err);\n }\n this.container = null;\n delete process.env.GCS_EMULATOR_URL;\n console.log('GCS twin stopped');\n }\n\n /**\n * Reset all data: delete buckets then re‑create them.\n */\n async reset(): Promise<void> {\n if (this.dockerUnavailable) {\n return;\n }\n console.log('Resetting GCS twin...');\n for (const bucket of this.config.buckets) {\n try {\n await fetch(`${this.config.externalUrl}/storage/v1/b/${bucket.name}`, {\n method: 'DELETE',\n });\n } catch (_err) {\n // Bucket may not exist; ignore\n }\n }\n await this.createBuckets();\n console.log('GCS twin reset complete');\n }\n\n /**\n * Get the emulator host URL for SDK configuration.\n */\n getEmulatorHost(): string {\n return this.config.externalUrl;\n }\n\n /**\n * Check if the twin is running and healthy.\n */\n async isHealthy(): Promise<boolean> {\n if (this.dockerUnavailable) {\n return false;\n }\n try {\n const response = await fetch(`${this.config.externalUrl}/storage/v1/b`);\n return response.ok;\n } catch {\n return false;\n }\n }\n\n // --- internal helpers ---\n\n private async ensureContainerRemoved(): Promise<void> {\n try {\n const existing = this.docker.getContainer(this.containerName);\n const info = await existing.inspect().catch(() => null);\n if (info) {\n console.log(`Removing existing container ${this.containerName}...`);\n await existing.stop().catch(() => {\n /* ignore */\n });\n await existing.remove().catch(() => {\n /* ignore */\n });\n }\n } catch (_err) {\n // No existing container\n }\n }\n\n private async ensureImageExists(): Promise<void> {\n const imageName = 'fsouza/fake-gcs-server';\n const images = await this.docker.listImages({\n filters: JSON.stringify({ reference: [imageName] }),\n });\n\n if (images.length === 0) {\n console.log(`Image ${imageName} not found. Pulling...`);\n await this.pullImage(imageName);\n }\n }\n\n private async pullImage(imageName: string): Promise<void> {\n return new Promise((resolve, reject) => {\n this.docker.pull(imageName, {}, (err, stream) => {\n if (err || !stream)\n return reject(\n err ?? new Error('No stream returned from docker.pull')\n );\n this.docker.modem.followProgress(\n stream,\n (err: Error | null, _output: unknown) =>\n err ? reject(err) : resolve(),\n (event: { status?: string }) => {\n if (\n event.status === 'Downloading' ||\n event.status === 'Extracting'\n ) {\n // Optionally log progress here\n }\n }\n );\n });\n });\n }\n\n private async createContainer(): Promise<void> {\n const container = await this.docker.createContainer({\n Image: 'fsouza/fake-gcs-server',\n name: this.containerName,\n HostConfig: {\n PortBindings: {\n '4443/tcp': [{ HostPort: `${this.config.port}` }],\n },\n },\n ExposedPorts: {\n '4443/tcp': {},\n },\n Cmd: [\n '-scheme',\n 'http',\n '-port',\n '4443',\n '-external-url',\n this.config.externalUrl,\n ],\n });\n this.container = container;\n }\n\n private async startContainer(): Promise<void> {\n if (!this.container) {\n throw new Error('Container not created');\n }\n await this.container.start();\n }\n\n private async waitForHealthy(): Promise<void> {\n const maxAttempts = 30;\n const intervalMs = 500;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n if (await this.isHealthy()) {\n // Small delay after health check passes — the container may report\n // healthy before all HTTP methods are ready (common with fake-gcs-server).\n await new Promise((resolve) => setTimeout(resolve, 1000));\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n\n throw new Error(\n `GCS twin did not become healthy within ${\n (maxAttempts * intervalMs) / 1000\n }s`\n );\n }\n\n /**\n * Internal: create buckets. Errors are logged but not thrown.\n */\n private async createBuckets(): Promise<void> {\n console.log('Creating buckets...');\n for (const bucket of this.config.buckets) {\n try {\n const createRes = await this.fetchWithTimeout(\n `${this.config.externalUrl}/storage/v1/b`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name: bucket.name }),\n },\n 10_000\n );\n if (!createRes.ok && createRes.status !== 409) {\n console.warn(\n `Warning: failed to create bucket ${bucket.name}: ${createRes.status} ${createRes.statusText}`\n );\n } else {\n console.log(` Created bucket ${bucket.name}`);\n }\n } catch (err) {\n console.warn(\n `Warning: failed to create bucket ${bucket.name}: ${\n err instanceof Error ? err.message : String(err)\n }`\n );\n }\n }\n console.log('Bucket creation complete');\n }\n\n /**\n * Fetch with a timeout. Aborts the request if it exceeds the given duration.\n */\n private async fetchWithTimeout(\n url: string,\n init: RequestInit,\n timeoutMs: number\n ): Promise<Response> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } catch (err) {\n if (controller.signal.aborted) {\n throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n }\n}\n"],"names":["Docker","createGcsConfig","GcsTwin","start","console","log","config","port","docker","ping","_err","warn","dockerUnavailable","ensureImageExists","ensureContainerRemoved","createContainer","startContainer","waitForHealthy","createBuckets","process","env","GCS_EMULATOR_URL","externalUrl","stop","container","containerName","remove","err","reset","bucket","buckets","fetch","name","method","getEmulatorHost","isHealthy","response","ok","existing","getContainer","info","inspect","catch","imageName","images","listImages","filters","JSON","stringify","reference","length","pullImage","Promise","resolve","reject","pull","stream","Error","modem","followProgress","_output","event","status","Image","HostConfig","PortBindings","HostPort","ExposedPorts","Cmd","maxAttempts","intervalMs","attempt","setTimeout","createRes","fetchWithTimeout","headers","body","statusText","message","String","url","init","timeoutMs","controller","AbortController","timer","abort","signal","aborted","clearTimeout","overrides"],"mappings":"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"}
|
package/dist/gcs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gcs/index.ts"],"sourcesContent":["/**\n * Public API for the GCS twin.\n */\n\nexport { GcsTwin } from './gcs-twin.js';\nexport type { GcsTwinConfig, BucketConfig } from './gcs-twin.config.js';\nexport { defaultGcsConfig } from './gcs-twin.config.js';\n\nexport {\n GcsErrorModeManager,\n extractBucketName,\n isUploadOperation,\n isGcsDataOperation,\n} from './gcs-error-mode.js';\nexport type {\n GcsErrorModeType,\n GcsErrorModeConfig,\n GcsErrorSimulationResult,\n BucketNotFoundConfig,\n QuotaExceededConfig,\n PermissionDeniedConfig,\n IntermittentFailureConfig,\n} from './gcs-error-mode.js';\n\nexport { GcsErrorSimulationInterceptor } from './gcs-error-simulation.interceptor.js';\n"],"names":["GcsTwin","defaultGcsConfig","GcsErrorModeManager","extractBucketName","isUploadOperation","isGcsDataOperation","GcsErrorSimulationInterceptor"],"
|
|
1
|
+
{"version":3,"sources":["../../src/gcs/index.ts"],"sourcesContent":["/**\n * Public API for the GCS twin.\n */\n\nexport { GcsTwin } from './gcs-twin.js';\nexport type { GcsTwinConfig, BucketConfig } from './gcs-twin.config.js';\nexport { defaultGcsConfig } from './gcs-twin.config.js';\n\nexport {\n GcsErrorModeManager,\n extractBucketName,\n isUploadOperation,\n isGcsDataOperation,\n} from './gcs-error-mode.js';\nexport type {\n GcsErrorModeType,\n GcsErrorModeConfig,\n GcsErrorSimulationResult,\n BucketNotFoundConfig,\n QuotaExceededConfig,\n PermissionDeniedConfig,\n IntermittentFailureConfig,\n} from './gcs-error-mode.js';\n\nexport { GcsErrorSimulationInterceptor } from './gcs-error-simulation.interceptor.js';\n"],"names":["GcsTwin","defaultGcsConfig","GcsErrorModeManager","extractBucketName","isUploadOperation","isGcsDataOperation","GcsErrorSimulationInterceptor"],"mappings":"AAAA;;CAEC,GAED,SAASA,OAAO,QAAQ,gBAAgB;AAExC,SAASC,gBAAgB,QAAQ,uBAAuB;AAExD,SACEC,mBAAmB,EACnBC,iBAAiB,EACjBC,iBAAiB,EACjBC,kBAAkB,QACb,sBAAsB;AAW7B,SAASC,6BAA6B,QAAQ,wCAAwC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mCAAmC,CAAC;AAGlD,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAG/B,cAAc,oBAAoB,CAAC;AAGnC,cAAc,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mCAAmC,CAAC;AAGlD,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAG/B,cAAc,oBAAoB,CAAC;AAGnC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,wBAAwB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './lib/integration-test-doubles.js';\n\n// Shared constants and utilities\nexport * from './shared/constants.js';\nexport * from './shared/activation.js';\nexport * from './shared/config.js';\n\n// GCS twin\nexport * from './gcs/index.js';\n\n// YouTube twin\nexport * from './youtube/index.js';\n\n// Interceptor layer\nexport * from './interceptor/index.js';\n"],"names":[],"
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './lib/integration-test-doubles.js';\n\n// Shared constants and utilities\nexport * from './shared/constants.js';\nexport * from './shared/activation.js';\nexport * from './shared/config.js';\n\n// GCS twin\nexport * from './gcs/index.js';\n\n// YouTube twin\nexport * from './youtube/index.js';\n\n// Apple Music twin\nexport * from './apple-music/index.js';\n\n// Interceptor layer\nexport * from './interceptor/index.js';\n"],"names":[],"mappings":"AAAA,cAAc,oCAAoC;AAElD,iCAAiC;AACjC,cAAc,wBAAwB;AACtC,cAAc,yBAAyB;AACvC,cAAc,qBAAqB;AAEnC,WAAW;AACX,cAAc,iBAAiB;AAE/B,eAAe;AACf,cAAc,qBAAqB;AAEnC,mBAAmB;AACnB,cAAc,yBAAyB;AAEvC,oBAAoB;AACpB,cAAc,yBAAyB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/interceptor/http-helper.ts"],"sourcesContent":["import http from 'http';\n\nexport interface ParsedRequestArgs {\n urlObj: URL;\n options: http.RequestOptions;\n callback?: (res: http.IncomingMessage) => void;\n}\n\n/**\n * Standardizes the polymorphic arguments of http.request / https.request\n * into a single unified object with urlObj, options, and callback.\n */\nexport function parseHttpArguments(\n moduleProtocol: string,\n arg1: string | URL | http.RequestOptions,\n arg2?: http.RequestOptions | ((res: http.IncomingMessage) => void),\n arg3?: (res: http.IncomingMessage) => void\n): ParsedRequestArgs {\n let urlObj: URL;\n let options: http.RequestOptions = {};\n let callback: ((res: http.IncomingMessage) => void) | undefined = undefined;\n\n if (typeof arg1 === 'string') {\n urlObj = new URL(arg1);\n if (typeof arg2 === 'object' && arg2 !== null) {\n options = arg2;\n callback = arg3;\n } else if (typeof arg2 === 'function') {\n callback = arg2;\n }\n } else if (arg1 instanceof URL) {\n urlObj = arg1;\n if (typeof arg2 === 'object' && arg2 !== null) {\n options = arg2;\n callback = arg3;\n } else if (typeof arg2 === 'function') {\n callback = arg2;\n }\n } else {\n options = arg1 || {};\n if (typeof arg2 === 'function') {\n callback = arg2;\n }\n const protocol = options.protocol || moduleProtocol;\n const host =\n options.host ||\n (options.hostname\n ? `${options.hostname}${\n options.port !== undefined && options.port !== null\n ? `:${options.port}`\n : ''\n }`\n : '');\n const path = options.path || '/';\n urlObj = new URL(`${protocol}//${host}${path}`);\n }\n\n return { urlObj, options, callback };\n}\n\n/**\n * Safely copies headers and injects the 'X-Original-Host' tracking header,\n * handling both object-based (OutgoingHttpHeaders) and array-based (string[]) formats.\n */\nexport function injectOriginalHostHeader(\n options: http.RequestOptions,\n originalHost: string\n): http.RequestOptions {\n const newOptions = { ...options };\n\n if (options.headers) {\n if (Array.isArray(options.headers)) {\n const headersArr = [...options.headers];\n headersArr.push('X-Original-Host', originalHost);\n newOptions.headers = headersArr;\n } else {\n const headersObj: http.OutgoingHttpHeaders = {\n ...(options.headers as http.OutgoingHttpHeaders),\n };\n headersObj['X-Original-Host'] = originalHost;\n newOptions.headers = headersObj;\n }\n } else {\n newOptions.headers = {\n 'X-Original-Host': originalHost,\n };\n }\n\n return newOptions;\n}\n\n/**\n * Creates new RequestOptions targeting a rewritten URL destination.\n */\nexport function rewriteRequestOptions(\n options: http.RequestOptions,\n rewrittenUrl: URL\n): http.RequestOptions {\n return {\n ...options,\n protocol: rewrittenUrl.protocol,\n hostname: rewrittenUrl.hostname,\n port: rewrittenUrl.port,\n path: rewrittenUrl.pathname + rewrittenUrl.search,\n };\n}\n\n/**\n * Safely invokes a request function with options and an optional callback.\n */\nexport function callRequestWithOptions(\n requestFn: typeof http.request,\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n const fn = requestFn as (\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n ) => http.ClientRequest;\n return fn(options, callback);\n}\n\n/**\n * Safely invokes a request function with a URL/string, options, and an optional callback.\n */\nexport function callRequestWithUrlAndOptions(\n requestFn: typeof http.request,\n url: string | URL,\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n const fn = requestFn as (\n url: string | URL,\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n ) => http.ClientRequest;\n return fn(url, options, callback);\n}\n\n/**\n * Safely invokes a request function with a URL/string and an optional callback.\n */\nexport function callRequestWithUrl(\n requestFn: typeof http.request,\n url: string | URL,\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n const fn = requestFn as (\n url: string | URL,\n callback?: (res: http.IncomingMessage) => void\n ) => http.ClientRequest;\n return fn(url, callback);\n}\n\n/**\n * Forwards arguments untouched to the original request function.\n */\nexport function forwardToOriginalRequest(\n originalRequest: typeof http.request,\n arg1: string | URL | http.RequestOptions,\n arg2?: http.RequestOptions | ((res: http.IncomingMessage) => void),\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n if (typeof arg1 === 'string' || arg1 instanceof URL) {\n if (typeof arg2 === 'object' && arg2 !== null) {\n return callRequestWithUrlAndOptions(\n originalRequest,\n arg1,\n arg2,\n callback\n );\n } else {\n const cb = typeof arg2 === 'function' ? arg2 : callback;\n return callRequestWithUrl(originalRequest, arg1, cb);\n }\n } else {\n const cb = typeof arg2 === 'function' ? arg2 : callback;\n return callRequestWithOptions(originalRequest, arg1, cb);\n }\n}\n"],"names":["parseHttpArguments","moduleProtocol","arg1","arg2","arg3","urlObj","options","callback","undefined","URL","protocol","host","hostname","port","path","injectOriginalHostHeader","originalHost","newOptions","headers","Array","isArray","headersArr","push","headersObj","rewriteRequestOptions","rewrittenUrl","pathname","search","callRequestWithOptions","requestFn","fn","callRequestWithUrlAndOptions","url","callRequestWithUrl","forwardToOriginalRequest","originalRequest","cb"],"
|
|
1
|
+
{"version":3,"sources":["../../src/interceptor/http-helper.ts"],"sourcesContent":["import http from 'http';\n\nexport interface ParsedRequestArgs {\n urlObj: URL;\n options: http.RequestOptions;\n callback?: (res: http.IncomingMessage) => void;\n}\n\n/**\n * Standardizes the polymorphic arguments of http.request / https.request\n * into a single unified object with urlObj, options, and callback.\n */\nexport function parseHttpArguments(\n moduleProtocol: string,\n arg1: string | URL | http.RequestOptions,\n arg2?: http.RequestOptions | ((res: http.IncomingMessage) => void),\n arg3?: (res: http.IncomingMessage) => void\n): ParsedRequestArgs {\n let urlObj: URL;\n let options: http.RequestOptions = {};\n let callback: ((res: http.IncomingMessage) => void) | undefined = undefined;\n\n if (typeof arg1 === 'string') {\n urlObj = new URL(arg1);\n if (typeof arg2 === 'object' && arg2 !== null) {\n options = arg2;\n callback = arg3;\n } else if (typeof arg2 === 'function') {\n callback = arg2;\n }\n } else if (arg1 instanceof URL) {\n urlObj = arg1;\n if (typeof arg2 === 'object' && arg2 !== null) {\n options = arg2;\n callback = arg3;\n } else if (typeof arg2 === 'function') {\n callback = arg2;\n }\n } else {\n options = arg1 || {};\n if (typeof arg2 === 'function') {\n callback = arg2;\n }\n const protocol = options.protocol || moduleProtocol;\n const host =\n options.host ||\n (options.hostname\n ? `${options.hostname}${\n options.port !== undefined && options.port !== null\n ? `:${options.port}`\n : ''\n }`\n : '');\n const path = options.path || '/';\n urlObj = new URL(`${protocol}//${host}${path}`);\n }\n\n return { urlObj, options, callback };\n}\n\n/**\n * Safely copies headers and injects the 'X-Original-Host' tracking header,\n * handling both object-based (OutgoingHttpHeaders) and array-based (string[]) formats.\n */\nexport function injectOriginalHostHeader(\n options: http.RequestOptions,\n originalHost: string\n): http.RequestOptions {\n const newOptions = { ...options };\n\n if (options.headers) {\n if (Array.isArray(options.headers)) {\n const headersArr = [...options.headers];\n headersArr.push('X-Original-Host', originalHost);\n newOptions.headers = headersArr;\n } else {\n const headersObj: http.OutgoingHttpHeaders = {\n ...(options.headers as http.OutgoingHttpHeaders),\n };\n headersObj['X-Original-Host'] = originalHost;\n newOptions.headers = headersObj;\n }\n } else {\n newOptions.headers = {\n 'X-Original-Host': originalHost,\n };\n }\n\n return newOptions;\n}\n\n/**\n * Creates new RequestOptions targeting a rewritten URL destination.\n */\nexport function rewriteRequestOptions(\n options: http.RequestOptions,\n rewrittenUrl: URL\n): http.RequestOptions {\n return {\n ...options,\n protocol: rewrittenUrl.protocol,\n hostname: rewrittenUrl.hostname,\n port: rewrittenUrl.port,\n path: rewrittenUrl.pathname + rewrittenUrl.search,\n };\n}\n\n/**\n * Safely invokes a request function with options and an optional callback.\n */\nexport function callRequestWithOptions(\n requestFn: typeof http.request,\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n const fn = requestFn as (\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n ) => http.ClientRequest;\n return fn(options, callback);\n}\n\n/**\n * Safely invokes a request function with a URL/string, options, and an optional callback.\n */\nexport function callRequestWithUrlAndOptions(\n requestFn: typeof http.request,\n url: string | URL,\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n const fn = requestFn as (\n url: string | URL,\n options: http.RequestOptions,\n callback?: (res: http.IncomingMessage) => void\n ) => http.ClientRequest;\n return fn(url, options, callback);\n}\n\n/**\n * Safely invokes a request function with a URL/string and an optional callback.\n */\nexport function callRequestWithUrl(\n requestFn: typeof http.request,\n url: string | URL,\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n const fn = requestFn as (\n url: string | URL,\n callback?: (res: http.IncomingMessage) => void\n ) => http.ClientRequest;\n return fn(url, callback);\n}\n\n/**\n * Forwards arguments untouched to the original request function.\n */\nexport function forwardToOriginalRequest(\n originalRequest: typeof http.request,\n arg1: string | URL | http.RequestOptions,\n arg2?: http.RequestOptions | ((res: http.IncomingMessage) => void),\n callback?: (res: http.IncomingMessage) => void\n): http.ClientRequest {\n if (typeof arg1 === 'string' || arg1 instanceof URL) {\n if (typeof arg2 === 'object' && arg2 !== null) {\n return callRequestWithUrlAndOptions(\n originalRequest,\n arg1,\n arg2,\n callback\n );\n } else {\n const cb = typeof arg2 === 'function' ? arg2 : callback;\n return callRequestWithUrl(originalRequest, arg1, cb);\n }\n } else {\n const cb = typeof arg2 === 'function' ? arg2 : callback;\n return callRequestWithOptions(originalRequest, arg1, cb);\n }\n}\n"],"names":["parseHttpArguments","moduleProtocol","arg1","arg2","arg3","urlObj","options","callback","undefined","URL","protocol","host","hostname","port","path","injectOriginalHostHeader","originalHost","newOptions","headers","Array","isArray","headersArr","push","headersObj","rewriteRequestOptions","rewrittenUrl","pathname","search","callRequestWithOptions","requestFn","fn","callRequestWithUrlAndOptions","url","callRequestWithUrl","forwardToOriginalRequest","originalRequest","cb"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/interceptor/index.ts"],"sourcesContent":["export * from './routing-table.js';\nexport * from './routing-table.config.js';\nexport * from './node-interceptor.js';\n"],"names":[],"
|
|
1
|
+
{"version":3,"sources":["../../src/interceptor/index.ts"],"sourcesContent":["export * from './routing-table.js';\nexport * from './routing-table.config.js';\nexport * from './node-interceptor.js';\n"],"names":[],"mappings":"AAAA,cAAc,qBAAqB;AACnC,cAAc,4BAA4B;AAC1C,cAAc,wBAAwB"}
|
|
@@ -1 +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":"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"}
|
|
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"],"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;IA9PA,YAAYpC,YAA0B,CAAE;aARhCH,gBAAgD;aAChDkC,sBAAkD;aAClDE,kBAA0C;aAC1CE,uBAAoD;aACpDC,mBAA4C;aAE5C9C,SAAS;QAGf,IAAI,CAACU,YAAY,GAAGA;IACtB;AA6PF"}
|
|
@@ -1 +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,
|
|
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,EA+B/C,CAAC"}
|
|
@@ -19,6 +19,16 @@ export const defaultRoutingEntries = [
|
|
|
19
19
|
{
|
|
20
20
|
hostname: 'www.youtube.com',
|
|
21
21
|
target: `http://localhost:${DEFAULT_PORTS.YOUTUBE}`
|
|
22
|
+
},
|
|
23
|
+
// Apple Music twin — Catalog API
|
|
24
|
+
{
|
|
25
|
+
hostname: 'api.music.apple.com',
|
|
26
|
+
target: `http://localhost:${DEFAULT_PORTS.APPLE_MUSIC}`
|
|
27
|
+
},
|
|
28
|
+
// Apple Music twin — Web SDK (browser CDN)
|
|
29
|
+
{
|
|
30
|
+
hostname: 'js-cdn.music.apple.com',
|
|
31
|
+
target: `http://localhost:${DEFAULT_PORTS.APPLE_MUSIC}`
|
|
22
32
|
}
|
|
23
33
|
];
|
|
24
34
|
|