@open-kingdom/shared-backend-integration-test-doubles 0.0.2-17

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 ADDED
@@ -0,0 +1,242 @@
1
+ # Integration Test Doubles
2
+
3
+ A library for creating integration test doubles (fakes) for external services. Provides local twins that can be used in place of real services during testing and development.
4
+
5
+ ## Features
6
+
7
+ - **GCS Twin**: Local fake for Google Cloud Storage using `fsouza/fake-gcs-server`
8
+ - **Configuration**: Environment-based configuration with sensible defaults and validation
9
+ - **Lifecycle Management**: Start, stop, and reset twins programmatically
10
+ - **NestJS Integration**: REST API controller for managing twins at runtime
11
+
12
+ ## Quick Start
13
+
14
+ ### Using GCS Twin
15
+
16
+ ```typescript
17
+ import { GcsTwin } from '@open-kingdom/shared-backend-integration-test-doubles';
18
+
19
+ // Start the twin (default port 9013)
20
+ const twin = new GcsTwin();
21
+ await twin.start();
22
+
23
+ // GCS_EMULATOR_URL is automatically set to http://localhost:9013
24
+ // Use the twin URL in your SDK configuration
25
+ const client = new Storage({
26
+ apiEndpoint: twin.getEmulatorHost(),
27
+ projectId: 'emulator-project',
28
+ credentials: {
29
+ client_email: 'emulator@emulator.iam',
30
+ private_key: 'unused',
31
+ },
32
+ });
33
+
34
+ // Run your tests...
35
+
36
+ // Clean up (also unsets GCS_EMULATOR_URL)
37
+ await twin.stop();
38
+ ```
39
+
40
+ ### NestJS Integration
41
+
42
+ Use the `TwinService` to manage the twin lifecycle within a NestJS application:
43
+
44
+ ```typescript
45
+ import { TwinService } from './twin/twin.service';
46
+
47
+ // The service wraps GcsTwin and exposes start/stop/status methods.
48
+ // See the demo-scaffold-backend app for a full controller example.
49
+ const twinService = new TwinService();
50
+ await twinService.start();
51
+ const status = await twinService.status();
52
+ await twinService.stop();
53
+ ```
54
+
55
+ ## API Reference
56
+
57
+ ### GcsTwin
58
+
59
+ #### Constructor
60
+
61
+ ```typescript
62
+ constructor(overrides?: Partial<GcsTwinConfig>, docker?: Docker)
63
+ ```
64
+
65
+ - `overrides.port`: Port to run the twin on (default: 9013, or from `GCS_TWIN_PORT` env var; must be within 9010-9020)
66
+ - `overrides.externalUrl`: External URL for the twin (default: `http://localhost:{port}`)
67
+ - `overrides.dataDir`: Optional persistent data directory
68
+ - `overrides.buckets`: Buckets to create and seed (default: `[{ name: 'app-assets' }, { name: 'user-uploads' }]`)
69
+ - `docker`: Optional Docker instance (for testing)
70
+
71
+ Config merges defaults, environment variables, and explicit overrides (explicit overrides take precedence).
72
+
73
+ #### Methods
74
+
75
+ - `start()`: Start the twin container, create buckets, and set `GCS_EMULATOR_URL` env var
76
+ - `stop()`: Stop and remove the container, unset `GCS_EMULATOR_URL`
77
+ - `reset()`: Reset the twin to its initial seeded state
78
+ - `getEmulatorHost()`: Get the twin URL (e.g., `http://localhost:9013`)
79
+ - `isHealthy()`: Check if the twin is running and responding
80
+
81
+ #### GcsTwinConfig
82
+
83
+ ```typescript
84
+ interface GcsTwinConfig {
85
+ port: number; // Port the fake-gcs-server listens on
86
+ externalUrl: string; // External URL for SDK configuration (e.g., http://localhost:9013)
87
+ dataDir?: string; // Optional persistent data directory
88
+ buckets: BucketConfig[]; // Buckets to create on startup
89
+ }
90
+
91
+ interface BucketConfig {
92
+ name: string; // Bucket name
93
+ }
94
+ ```
95
+
96
+ ### Exports
97
+
98
+ ```typescript
99
+ // GCS twin
100
+ export { GcsTwin } from './gcs/gcs-twin.js';
101
+ export type { GcsTwinConfig, BucketConfig } from './gcs/gcs-twin.config.js';
102
+ export { defaultGcsConfig } from './gcs/gcs-twin.config.js';
103
+
104
+ // Shared constants and utilities
105
+ export { PORT_RANGE, DEFAULT_PORTS, ENV_VARS } from './shared/constants.js';
106
+ export { isTestMode } from './shared/activation.js';
107
+ export { createGcsConfig, defaultGcsConfig } from './shared/config.js';
108
+ ```
109
+
110
+ ## Configuration
111
+
112
+ ### Environment Variables
113
+
114
+ | Variable | Description | Default |
115
+ | ------------------- | ------------------------------------- | ------------------------- |
116
+ | `GCS_TWIN_PORT` | Port for GCS twin (must be 9010-9020) | 9013 |
117
+ | `GCS_TWIN_DATA_DIR` | Persistent data directory (optional) | (ephemeral) |
118
+ | `GCS_EMULATOR_URL` | Auto-set by `GcsTwin.start()` | `http://localhost:{port}` |
119
+ | `TEST_MODE` | Activation gate; must be `'true'` | (unset) |
120
+
121
+ ### Port Assignments
122
+
123
+ The library reserves ports 9010-9020 for test doubles:
124
+
125
+ | Twin | Default Port |
126
+ | --------------- | ------------- |
127
+ | GCS | 9013 |
128
+ | Gmail | 9014 (future) |
129
+ | Google Auth | 9015 (future) |
130
+ | YouTube | 9016 (future) |
131
+ | Google Calendar | 9017 (future) |
132
+ | Spotify | 9018 (future) |
133
+
134
+ Configure in `constants.ts` if you need to change this range.
135
+
136
+ ### How Configuration Merging Works
137
+
138
+ Configuration is resolved in this order (later takes precedence):
139
+
140
+ 1. **Defaults** (`defaultGcsConfig`)
141
+ 2. **Environment variables** (`GCS_TWIN_PORT`, `GCS_TWIN_DATA_DIR`)
142
+ 3. **Explicit overrides** (passed to constructor or `createGcsConfig()`)
143
+
144
+ Port validation ensures the value is within the reserved range (9010-9020).
145
+
146
+ ## Scripts
147
+
148
+ ### Start Twin Manually
149
+
150
+ ```bash
151
+ # Start on default port (9013)
152
+ npx tsx scripts/start-twin.ts
153
+
154
+ # Start on specific port
155
+ npx tsx scripts/start-twin.ts 9013
156
+ ```
157
+
158
+ The script outputs the twin URL and sets `GCS_EMULATOR_URL`. Keeps running until Ctrl+C.
159
+
160
+ ## Architecture
161
+
162
+ ### Components
163
+
164
+ ```
165
+ ┌─────────────────┐ ┌────────────────────┐
166
+ │ NestJS App │ │ GcsTwin │
167
+ │ (GcsStorage │────▶│ (fake-gcs-server │
168
+ │ Service) │ │ Docker container) │
169
+ └─────────────────┘ └────────────────────┘
170
+ ```
171
+
172
+ ### How It Works
173
+
174
+ 1. **GcsTwin** starts a Docker container running `fsouza/fake-gcs-server`
175
+ 2. `GCS_EMULATOR_URL` is automatically set in `process.env` on start and unset on stop
176
+ 3. **GcsStorageService** (in `feature-gcp-resources`) reads `GCS_EMULATOR_URL` or `STORAGE_EMULATOR_HOST` and configures the Google Cloud Storage SDK with `apiEndpoint` pointing to the twin
177
+ 4. When no emulator is configured, `GcsStorageService` falls back to real GCS with signed URLs
178
+
179
+ ### Default Buckets
180
+
181
+ Two buckets are created by default:
182
+
183
+ - **app-assets** - Application static assets
184
+ - **user-uploads** - User-uploaded files
185
+
186
+ ## Troubleshooting
187
+
188
+ ### Docker Not Running
189
+
190
+ ```
191
+ Error: connect ENOENT /var/run/docker.sock
192
+ ```
193
+
194
+ **Solution**: Ensure Docker Desktop is running.
195
+
196
+ ### Port Already in Use
197
+
198
+ ```
199
+ Error: Port 9013 is already in use
200
+ ```
201
+
202
+ **Solution**:
203
+
204
+ - Use a different port: `new GcsTwin({ port: 9014 })`
205
+ - Or set `GCS_TWIN_PORT=9014`
206
+
207
+ ### Container Won't Start
208
+
209
+ ```
210
+ Error: (HTTP code 404) not found - No such image: fsouza/fake-gcs-server:latest
211
+ ```
212
+
213
+ **Solution**: Pull the image manually:
214
+
215
+ ```bash
216
+ docker pull fsouza/fake-gcs-server
217
+ ```
218
+
219
+ ### Port Out of Range
220
+
221
+ ```
222
+ Error: GCS twin port 9099 is outside the reserved range 9010-9020
223
+ ```
224
+
225
+ **Solution**: Use a port within 9010-9020, or adjust `PORT_RANGE` in `constants.ts`.
226
+
227
+ ### Tests Fail When Run Together
228
+
229
+ **Symptoms**: Tests pass in isolation but fail in suite.
230
+
231
+ **Cause**: Port conflicts between tests.
232
+
233
+ **Solution**: Use unique ports for each test suite:
234
+
235
+ ```typescript
236
+ const twin1 = new GcsTwin({ port: 9013 });
237
+ const twin2 = new GcsTwin({ port: 9014 });
238
+ ```
239
+
240
+ ## License
241
+
242
+ MIT
@@ -0,0 +1,9 @@
1
+ /**
2
+ * GCS twin configuration.
3
+ *
4
+ * Re‑exports the shared GcsTwinConfig and BucketConfig interfaces
5
+ * along with the default configuration.
6
+ */
7
+ export type { GcsTwinConfig, BucketConfig } from '../shared/config.js';
8
+ export { defaultGcsConfig } from '../shared/config.js';
9
+ //# sourceMappingURL=gcs-twin.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs-twin.config.d.ts","sourceRoot":"","sources":["../../src/gcs/gcs-twin.config.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * GCS twin configuration.
3
+ *
4
+ * Re‑exports the shared GcsTwinConfig and BucketConfig interfaces
5
+ * along with the default configuration.
6
+ */ export { defaultGcsConfig } from '../shared/config.js';
7
+
8
+ //# sourceMappingURL=gcs-twin.config.js.map
@@ -0,0 +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"],"rangeMappings":";;;;;","mappings":"AAAA;;;;;CAKC,GAGD,SAASA,gBAAgB,QAAQ,sBAAsB"}
@@ -0,0 +1,58 @@
1
+ import Docker from 'dockerode';
2
+ import { GcsTwinConfig } from '../shared/config.js';
3
+ /**
4
+ * GCS twin lifecycle manager.
5
+ *
6
+ * Starts/stops a fake‑gcs‑server Docker container, creates buckets,
7
+ * and sets the GCS_EMULATOR_URL environment variable.
8
+ */
9
+ export declare class GcsTwin {
10
+ private readonly config;
11
+ private readonly docker;
12
+ private container;
13
+ private readonly containerName;
14
+ /**
15
+ * Create a new GCS twin.
16
+ *
17
+ * @param overrides Optional configuration overrides (merged with defaults and env vars).
18
+ * @param docker Optional Docker instance (for testing).
19
+ */
20
+ constructor(overrides?: Partial<GcsTwinConfig>, docker?: Docker);
21
+ /**
22
+ * Start the Docker container and create buckets.
23
+ *
24
+ * @throws {Error} If the container fails to start.
25
+ */
26
+ start(): Promise<void>;
27
+ /**
28
+ * Stop and remove the Docker container.
29
+ */
30
+ stop(): Promise<void>;
31
+ /**
32
+ * Reset all data: delete buckets then re‑create them.
33
+ */
34
+ reset(): Promise<void>;
35
+ /**
36
+ * Get the emulator host URL for SDK configuration.
37
+ */
38
+ getEmulatorHost(): string;
39
+ /**
40
+ * Check if the twin is running and healthy.
41
+ */
42
+ isHealthy(): Promise<boolean>;
43
+ private ensureContainerRemoved;
44
+ private ensureImageExists;
45
+ private pullImage;
46
+ private createContainer;
47
+ private startContainer;
48
+ private waitForHealthy;
49
+ /**
50
+ * Internal: create buckets. Errors are logged but not thrown.
51
+ */
52
+ private createBuckets;
53
+ /**
54
+ * Fetch with a timeout. Aborts the request if it exceeds the given duration.
55
+ */
56
+ private fetchWithTimeout;
57
+ }
58
+ //# sourceMappingURL=gcs-twin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcs-twin.d.ts","sourceRoot":"","sources":["../../src/gcs/gcs-twin.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,WAAW,CAAC;AAC/B,OAAO,EAAmB,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAErE;;;;;GAKG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC;;;;;OAKG;gBACS,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM;IAM/D;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAY5B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB3B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B;;OAEG;IACH,eAAe,IAAI,MAAM;IAIzB;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;YAWrB,sBAAsB;YAkBtB,iBAAiB;YAYjB,SAAS;YAwBT,eAAe;YAwBf,cAAc;YAOd,cAAc;IAqB5B;;OAEG;YACW,aAAa;IA+B3B;;OAEG;YACW,gBAAgB;CAkB/B"}
@@ -0,0 +1,219 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import Docker from 'dockerode';
3
+ import { createGcsConfig } from '../shared/config.js';
4
+ /**
5
+ * GCS twin lifecycle manager.
6
+ *
7
+ * Starts/stops a fake‑gcs‑server Docker container, creates buckets,
8
+ * and sets the GCS_EMULATOR_URL environment variable.
9
+ */ export class GcsTwin {
10
+ /**
11
+ * Start the Docker container and create buckets.
12
+ *
13
+ * @throws {Error} If the container fails to start.
14
+ */ async start() {
15
+ console.log(`Starting GCS twin on port ${this.config.port}...`);
16
+ await this.ensureImageExists();
17
+ await this.ensureContainerRemoved();
18
+ await this.createContainer();
19
+ await this.startContainer();
20
+ await this.waitForHealthy();
21
+ await this.createBuckets();
22
+ process.env.GCS_EMULATOR_URL = this.config.externalUrl;
23
+ console.log(`GCS twin ready at ${this.config.externalUrl}`);
24
+ }
25
+ /**
26
+ * Stop and remove the Docker container.
27
+ */ async stop() {
28
+ if (!this.container) {
29
+ return;
30
+ }
31
+ console.log(`Stopping GCS twin (${this.containerName})...`);
32
+ try {
33
+ await this.container.stop();
34
+ await this.container.remove();
35
+ } catch (err) {
36
+ console.warn('Error stopping container:', err);
37
+ }
38
+ this.container = null;
39
+ delete process.env.GCS_EMULATOR_URL;
40
+ console.log('GCS twin stopped');
41
+ }
42
+ /**
43
+ * Reset all data: delete buckets then re‑create them.
44
+ */ async reset() {
45
+ console.log('Resetting GCS twin...');
46
+ for (const bucket of this.config.buckets){
47
+ try {
48
+ await fetch(`${this.config.externalUrl}/storage/v1/b/${bucket.name}`, {
49
+ method: 'DELETE'
50
+ });
51
+ } catch (_err) {
52
+ // Bucket may not exist; ignore
53
+ }
54
+ }
55
+ await this.createBuckets();
56
+ console.log('GCS twin reset complete');
57
+ }
58
+ /**
59
+ * Get the emulator host URL for SDK configuration.
60
+ */ getEmulatorHost() {
61
+ return this.config.externalUrl;
62
+ }
63
+ /**
64
+ * Check if the twin is running and healthy.
65
+ */ async isHealthy() {
66
+ try {
67
+ const response = await fetch(`${this.config.externalUrl}/storage/v1/b`);
68
+ return response.ok;
69
+ } catch (e) {
70
+ return false;
71
+ }
72
+ }
73
+ // --- internal helpers ---
74
+ async ensureContainerRemoved() {
75
+ try {
76
+ const existing = this.docker.getContainer(this.containerName);
77
+ const info = await existing.inspect().catch(()=>null);
78
+ if (info) {
79
+ console.log(`Removing existing container ${this.containerName}...`);
80
+ await existing.stop().catch(()=>{
81
+ /* ignore */ });
82
+ await existing.remove().catch(()=>{
83
+ /* ignore */ });
84
+ }
85
+ } catch (_err) {
86
+ // No existing container
87
+ }
88
+ }
89
+ async ensureImageExists() {
90
+ const imageName = 'fsouza/fake-gcs-server';
91
+ const images = await this.docker.listImages({
92
+ filters: JSON.stringify({
93
+ reference: [
94
+ imageName
95
+ ]
96
+ })
97
+ });
98
+ if (images.length === 0) {
99
+ console.log(`Image ${imageName} not found. Pulling...`);
100
+ await this.pullImage(imageName);
101
+ }
102
+ }
103
+ async pullImage(imageName) {
104
+ return new Promise((resolve, reject)=>{
105
+ this.docker.pull(imageName, {}, (err, stream)=>{
106
+ if (err || !stream) return reject(err != null ? err : new Error('No stream returned from docker.pull'));
107
+ this.docker.modem.followProgress(stream, (err, output)=>err ? reject(err) : resolve(output), (event)=>{
108
+ if (event.status === 'Downloading' || event.status === 'Extracting') {
109
+ // Optionally log progress here
110
+ }
111
+ });
112
+ });
113
+ });
114
+ }
115
+ async createContainer() {
116
+ const container = await this.docker.createContainer({
117
+ Image: 'fsouza/fake-gcs-server',
118
+ name: this.containerName,
119
+ HostConfig: {
120
+ PortBindings: {
121
+ '4443/tcp': [
122
+ {
123
+ HostPort: `${this.config.port}`
124
+ }
125
+ ]
126
+ }
127
+ },
128
+ ExposedPorts: {
129
+ '4443/tcp': {}
130
+ },
131
+ Cmd: [
132
+ '-scheme',
133
+ 'http',
134
+ '-port',
135
+ '4443',
136
+ '-external-url',
137
+ this.config.externalUrl
138
+ ]
139
+ });
140
+ this.container = container;
141
+ }
142
+ async startContainer() {
143
+ if (!this.container) {
144
+ throw new Error('Container not created');
145
+ }
146
+ await this.container.start();
147
+ }
148
+ async waitForHealthy() {
149
+ const maxAttempts = 30;
150
+ const intervalMs = 500;
151
+ for(let attempt = 0; attempt < maxAttempts; attempt++){
152
+ if (await this.isHealthy()) {
153
+ // Small delay after health check passes — the container may report
154
+ // healthy before all HTTP methods are ready (common with fake-gcs-server).
155
+ await new Promise((resolve)=>setTimeout(resolve, 1000));
156
+ return;
157
+ }
158
+ await new Promise((resolve)=>setTimeout(resolve, intervalMs));
159
+ }
160
+ throw new Error(`GCS twin did not become healthy within ${maxAttempts * intervalMs / 1000}s`);
161
+ }
162
+ /**
163
+ * Internal: create buckets. Errors are logged but not thrown.
164
+ */ async createBuckets() {
165
+ console.log('Creating buckets...');
166
+ for (const bucket of this.config.buckets){
167
+ try {
168
+ const createRes = await this.fetchWithTimeout(`${this.config.externalUrl}/storage/v1/b`, {
169
+ method: 'POST',
170
+ headers: {
171
+ 'Content-Type': 'application/json'
172
+ },
173
+ body: JSON.stringify({
174
+ name: bucket.name
175
+ })
176
+ }, 10000);
177
+ if (!createRes.ok && createRes.status !== 409) {
178
+ console.warn(`Warning: failed to create bucket ${bucket.name}: ${createRes.status} ${createRes.statusText}`);
179
+ } else {
180
+ console.log(` Created bucket ${bucket.name}`);
181
+ }
182
+ } catch (err) {
183
+ console.warn(`Warning: failed to create bucket ${bucket.name}: ${err instanceof Error ? err.message : String(err)}`);
184
+ }
185
+ }
186
+ console.log('Bucket creation complete');
187
+ }
188
+ /**
189
+ * Fetch with a timeout. Aborts the request if it exceeds the given duration.
190
+ */ async fetchWithTimeout(url, init, timeoutMs) {
191
+ const controller = new AbortController();
192
+ const timer = setTimeout(()=>controller.abort(), timeoutMs);
193
+ try {
194
+ return await fetch(url, _extends({}, init, {
195
+ signal: controller.signal
196
+ }));
197
+ } catch (err) {
198
+ if (controller.signal.aborted) {
199
+ throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
200
+ }
201
+ throw err;
202
+ } finally{
203
+ clearTimeout(timer);
204
+ }
205
+ }
206
+ /**
207
+ * Create a new GCS twin.
208
+ *
209
+ * @param overrides Optional configuration overrides (merged with defaults and env vars).
210
+ * @param docker Optional Docker instance (for testing).
211
+ */ constructor(overrides, docker){
212
+ this.container = null;
213
+ this.config = createGcsConfig(overrides);
214
+ this.docker = docker != null ? docker : new Docker();
215
+ this.containerName = `itd-gcs-twin-${this.config.port}`;
216
+ }
217
+ }
218
+
219
+ //# sourceMappingURL=gcs-twin.js.map
@@ -0,0 +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\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 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.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 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 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: any) =>\n err ? reject(err) : resolve(output),\n (event: any) => {\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","ensureImageExists","ensureContainerRemoved","createContainer","startContainer","waitForHealthy","createBuckets","process","env","GCS_EMULATOR_URL","externalUrl","stop","container","containerName","remove","err","warn","reset","bucket","buckets","fetch","name","method","_err","getEmulatorHost","isHealthy","response","ok","existing","docker","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":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public API for the GCS twin.
3
+ */
4
+ export { GcsTwin } from './gcs-twin.js';
5
+ export type { GcsTwinConfig, BucketConfig } from './gcs-twin.config.js';
6
+ export { defaultGcsConfig } from './gcs-twin.config.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/gcs/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Public API for the GCS twin.
3
+ */ export { GcsTwin } from './gcs-twin.js';
4
+ export { defaultGcsConfig } from './gcs-twin.config.js';
5
+
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"],"names":["GcsTwin","defaultGcsConfig"],"rangeMappings":";;;","mappings":"AAAA;;CAEC,GAED,SAASA,OAAO,QAAQ,gBAAgB;AAExC,SAASC,gBAAgB,QAAQ,uBAAuB"}
@@ -0,0 +1,6 @@
1
+ export * from './lib/integration-test-doubles.js';
2
+ export * from './shared/constants.js';
3
+ export * from './shared/activation.js';
4
+ export * from './shared/config.js';
5
+ export * from './gcs/index.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from './lib/integration-test-doubles.js';
2
+ // Shared constants and utilities
3
+ export * from './shared/constants.js';
4
+ export * from './shared/activation.js';
5
+ export * from './shared/config.js';
6
+ // GCS twin
7
+ export * from './gcs/index.js';
8
+
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"],"names":[],"rangeMappings":";;;;;;","mappings":"AAAA,cAAc,oCAAoC;AAElD,iCAAiC;AACjC,cAAc,wBAAwB;AACtC,cAAc,yBAAyB;AACvC,cAAc,qBAAqB;AAEnC,WAAW;AACX,cAAc,iBAAiB"}
@@ -0,0 +1,2 @@
1
+ export declare function integrationTestDoubles(): string;
2
+ //# sourceMappingURL=integration-test-doubles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integration-test-doubles.d.ts","sourceRoot":"","sources":["../../src/lib/integration-test-doubles.ts"],"names":[],"mappings":"AAAA,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C"}
@@ -0,0 +1,5 @@
1
+ export function integrationTestDoubles() {
2
+ return 'integration-test-doubles';
3
+ }
4
+
5
+ //# sourceMappingURL=integration-test-doubles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/integration-test-doubles.ts"],"sourcesContent":["export function integrationTestDoubles(): string {\n return 'integration-test-doubles';\n}\n"],"names":["integrationTestDoubles"],"rangeMappings":";;","mappings":"AAAA,OAAO,SAASA;IACd,OAAO;AACT"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Activation gate for integration‑test‑doubles.
3
+ * Twins should only start when the test‑mode environment variable is set.
4
+ */
5
+ /**
6
+ * Returns `true` if the library is running in test mode.
7
+ *
8
+ * Test mode is enabled when the environment variable `TEST_MODE` is exactly the string `'true'`.
9
+ * This check is case‑sensitive.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * if (isTestMode()) {
14
+ * await twin.start();
15
+ * }
16
+ * ```
17
+ */
18
+ export declare function isTestMode(): boolean;
19
+ //# sourceMappingURL=activation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activation.d.ts","sourceRoot":"","sources":["../../src/shared/activation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAEpC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Activation gate for integration‑test‑doubles.
3
+ * Twins should only start when the test‑mode environment variable is set.
4
+ */ /**
5
+ * Returns `true` if the library is running in test mode.
6
+ *
7
+ * Test mode is enabled when the environment variable `TEST_MODE` is exactly the string `'true'`.
8
+ * This check is case‑sensitive.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * if (isTestMode()) {
13
+ * await twin.start();
14
+ * }
15
+ * ```
16
+ */ export function isTestMode() {
17
+ return process.env.TEST_MODE === 'true';
18
+ }
19
+
20
+ //# sourceMappingURL=activation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shared/activation.ts"],"sourcesContent":["/**\n * Activation gate for integration‑test‑doubles.\n * Twins should only start when the test‑mode environment variable is set.\n */\n\n/**\n * Returns `true` if the library is running in test mode.\n *\n * Test mode is enabled when the environment variable `TEST_MODE` is exactly the string `'true'`.\n * This check is case‑sensitive.\n *\n * @example\n * ```ts\n * if (isTestMode()) {\n * await twin.start();\n * }\n * ```\n */\nexport function isTestMode(): boolean {\n return process.env.TEST_MODE === 'true';\n}\n"],"names":["isTestMode","process","env","TEST_MODE"],"rangeMappings":";;;;;;;;;;;;;;;;;","mappings":"AAAA;;;CAGC,GAED;;;;;;;;;;;;CAYC,GACD,OAAO,SAASA;IACd,OAAOC,QAAQC,GAAG,CAACC,SAAS,KAAK;AACnC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Configuration for a single bucket to create on startup.
3
+ */
4
+ export interface BucketConfig {
5
+ /** Bucket name */
6
+ name: string;
7
+ }
8
+ /**
9
+ * Configuration for the GCS twin.
10
+ */
11
+ export interface GcsTwinConfig {
12
+ /** Port the fake‑gcs‑server listens on */
13
+ port: number;
14
+ /** External URL the SDK will connect to (e.g., http://localhost:9013) */
15
+ externalUrl: string;
16
+ /** Optional persistent data directory (if not set, data is ephemeral) */
17
+ dataDir?: string;
18
+ /** Buckets to create on startup */
19
+ buckets: BucketConfig[];
20
+ }
21
+ /**
22
+ * Default configuration for the GCS twin.
23
+ *
24
+ * Values are derived from the implementation guide (docs/replication‑engine/twins/01‑gcs‑twin.md).
25
+ */
26
+ export declare const defaultGcsConfig: GcsTwinConfig;
27
+ /**
28
+ * Creates a GcsTwinConfig by merging defaults, environment variables, and explicit overrides.
29
+ *
30
+ * Environment variables take precedence over defaults.
31
+ * Explicit overrides (passed as argument) take precedence over environment variables.
32
+ *
33
+ * @param overrides - Partial configuration that overrides both defaults and environment variables.
34
+ * @returns A complete GcsTwinConfig.
35
+ */
36
+ export declare function createGcsConfig(overrides?: Partial<GcsTwinConfig>): GcsTwinConfig;
37
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,aAI9B,CAAC;AA6BF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,SAAS,GAAE,OAAO,CAAC,aAAa,CAAM,GACrC,aAAa,CAoCf"}
@@ -0,0 +1,70 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ import { ConfigService, nodeEnvAdapter } from '@open-kingdom/shared-poly-util-env-config';
3
+ import { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from './constants.js';
4
+ /**
5
+ * Default configuration for the GCS twin.
6
+ *
7
+ * Values are derived from the implementation guide (docs/replication‑engine/twins/01‑gcs‑twin.md).
8
+ */ export const defaultGcsConfig = {
9
+ port: DEFAULT_PORTS.GCS,
10
+ externalUrl: `http://localhost:${DEFAULT_PORTS.GCS}`,
11
+ buckets: [
12
+ {
13
+ name: 'app-assets'
14
+ },
15
+ {
16
+ name: 'user-uploads'
17
+ }
18
+ ]
19
+ };
20
+ /**
21
+ * Environment‑variable keys that affect GCS twin configuration.
22
+ */ const _CONFIG_ENV_KEYS = [
23
+ ENV_VARS.GCS_TWIN_PORT,
24
+ ENV_VARS.GCS_TWIN_DATA_DIR
25
+ ];
26
+ /**
27
+ * Creates a typed ConfigService for GCS twin environment variables.
28
+ */ function createEnvConfigService() {
29
+ return new ConfigService(nodeEnvAdapter);
30
+ }
31
+ /**
32
+ * Parses a string environment variable as a number.
33
+ * Returns undefined if the variable is not set or cannot be parsed.
34
+ */ function parseOptionalInt(value) {
35
+ if (value === undefined) return undefined;
36
+ const parsed = Number.parseInt(value, 10);
37
+ return Number.isNaN(parsed) ? undefined : parsed;
38
+ }
39
+ /**
40
+ * Creates a GcsTwinConfig by merging defaults, environment variables, and explicit overrides.
41
+ *
42
+ * Environment variables take precedence over defaults.
43
+ * Explicit overrides (passed as argument) take precedence over environment variables.
44
+ *
45
+ * @param overrides - Partial configuration that overrides both defaults and environment variables.
46
+ * @returns A complete GcsTwinConfig.
47
+ */ export function createGcsConfig(overrides = {}) {
48
+ const env = createEnvConfigService();
49
+ // Environment‑variable overrides
50
+ const envPort = parseOptionalInt(env.get(ENV_VARS.GCS_TWIN_PORT));
51
+ const envDataDir = env.get(ENV_VARS.GCS_TWIN_DATA_DIR);
52
+ const baseConfig = _extends({}, defaultGcsConfig, envPort !== undefined && {
53
+ port: envPort
54
+ }, envDataDir !== undefined && {
55
+ dataDir: envDataDir
56
+ });
57
+ // Explicit overrides take final precedence
58
+ const finalConfig = _extends({}, baseConfig, overrides);
59
+ // Validate port is within reserved range
60
+ if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {
61
+ throw new Error(`GCS twin port ${finalConfig.port} is outside the reserved range ${PORT_RANGE.min}‑${PORT_RANGE.max}. ` + `Adjust PORT_RANGE in constants.ts or set ${ENV_VARS.GCS_TWIN_PORT} appropriately.`);
62
+ }
63
+ // Ensure externalUrl matches the port (unless explicitly overridden)
64
+ if (!overrides.externalUrl && finalConfig.externalUrl === defaultGcsConfig.externalUrl) {
65
+ finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;
66
+ }
67
+ return finalConfig;
68
+ }
69
+
70
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shared/config.ts"],"sourcesContent":["import {\n ConfigService,\n nodeEnvAdapter,\n} from '@open-kingdom/shared-poly-util-env-config';\nimport { DEFAULT_PORTS, ENV_VARS, PORT_RANGE } from './constants.js';\n\n/**\n * Configuration for a single bucket to create on startup.\n */\nexport interface BucketConfig {\n /** Bucket name */\n name: string;\n}\n\n/**\n * Configuration for the GCS twin.\n */\nexport interface GcsTwinConfig {\n /** Port the fake‑gcs‑server listens on */\n port: number;\n /** External URL the SDK will connect to (e.g., http://localhost:9013) */\n externalUrl: string;\n /** Optional persistent data directory (if not set, data is ephemeral) */\n dataDir?: string;\n /** Buckets to create on startup */\n buckets: BucketConfig[];\n}\n\n/**\n * Default configuration for the GCS twin.\n *\n * Values are derived from the implementation guide (docs/replication‑engine/twins/01‑gcs‑twin.md).\n */\nexport const defaultGcsConfig: GcsTwinConfig = {\n port: DEFAULT_PORTS.GCS,\n externalUrl: `http://localhost:${DEFAULT_PORTS.GCS}`,\n buckets: [{ name: 'app-assets' }, { name: 'user-uploads' }],\n};\n\n/**\n * Environment‑variable keys that affect GCS twin configuration.\n */\nconst _CONFIG_ENV_KEYS = [\n ENV_VARS.GCS_TWIN_PORT,\n ENV_VARS.GCS_TWIN_DATA_DIR,\n] as const;\n\n/**\n * Creates a typed ConfigService for GCS twin environment variables.\n */\nfunction createEnvConfigService(): ConfigService<\n (typeof _CONFIG_ENV_KEYS)[number]\n> {\n return new ConfigService(nodeEnvAdapter);\n}\n\n/**\n * Parses a string environment variable as a number.\n * Returns undefined if the variable is not set or cannot be parsed.\n */\nfunction parseOptionalInt(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number.parseInt(value, 10);\n return Number.isNaN(parsed) ? undefined : parsed;\n}\n\n/**\n * Creates a GcsTwinConfig by merging defaults, environment variables, and explicit overrides.\n *\n * Environment variables take precedence over defaults.\n * Explicit overrides (passed as argument) take precedence over environment variables.\n *\n * @param overrides - Partial configuration that overrides both defaults and environment variables.\n * @returns A complete GcsTwinConfig.\n */\nexport function createGcsConfig(\n overrides: Partial<GcsTwinConfig> = {}\n): GcsTwinConfig {\n const env = createEnvConfigService();\n\n // Environment‑variable overrides\n const envPort = parseOptionalInt(env.get(ENV_VARS.GCS_TWIN_PORT));\n const envDataDir = env.get(ENV_VARS.GCS_TWIN_DATA_DIR);\n\n const baseConfig: GcsTwinConfig = {\n ...defaultGcsConfig,\n ...(envPort !== undefined && { port: envPort }),\n ...(envDataDir !== undefined && { dataDir: envDataDir }),\n };\n\n // Explicit overrides take final precedence\n const finalConfig: GcsTwinConfig = {\n ...baseConfig,\n ...overrides,\n };\n\n // Validate port is within reserved range\n if (finalConfig.port < PORT_RANGE.min || finalConfig.port > PORT_RANGE.max) {\n throw new Error(\n `GCS twin port ${finalConfig.port} is outside the reserved range ${PORT_RANGE.min}‑${PORT_RANGE.max}. ` +\n `Adjust PORT_RANGE in constants.ts or set ${ENV_VARS.GCS_TWIN_PORT} appropriately.`\n );\n }\n\n // Ensure externalUrl matches the port (unless explicitly overridden)\n if (\n !overrides.externalUrl &&\n finalConfig.externalUrl === defaultGcsConfig.externalUrl\n ) {\n finalConfig.externalUrl = `http://localhost:${finalConfig.port}`;\n }\n\n return finalConfig;\n}\n"],"names":["ConfigService","nodeEnvAdapter","DEFAULT_PORTS","ENV_VARS","PORT_RANGE","defaultGcsConfig","port","GCS","externalUrl","buckets","name","_CONFIG_ENV_KEYS","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR","createEnvConfigService","parseOptionalInt","value","undefined","parsed","Number","parseInt","isNaN","createGcsConfig","overrides","env","envPort","get","envDataDir","baseConfig","dataDir","finalConfig","min","max","Error"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;","mappings":";AAAA,SACEA,aAAa,EACbC,cAAc,QACT,4CAA4C;AACnD,SAASC,aAAa,EAAEC,QAAQ,EAAEC,UAAU,QAAQ,iBAAiB;AAwBrE;;;;CAIC,GACD,OAAO,MAAMC,mBAAkC;IAC7CC,MAAMJ,cAAcK,GAAG;IACvBC,aAAa,CAAC,iBAAiB,EAAEN,cAAcK,GAAG,CAAC,CAAC;IACpDE,SAAS;QAAC;YAAEC,MAAM;QAAa;QAAG;YAAEA,MAAM;QAAe;KAAE;AAC7D,EAAE;AAEF;;CAEC,GACD,MAAMC,mBAAmB;IACvBR,SAASS,aAAa;IACtBT,SAASU,iBAAiB;CAC3B;AAED;;CAEC,GACD,SAASC;IAGP,OAAO,IAAId,cAAcC;AAC3B;AAEA;;;CAGC,GACD,SAASc,iBAAiBC,KAAyB;IACjD,IAAIA,UAAUC,WAAW,OAAOA;IAChC,MAAMC,SAASC,OAAOC,QAAQ,CAACJ,OAAO;IACtC,OAAOG,OAAOE,KAAK,CAACH,UAAUD,YAAYC;AAC5C;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASI,gBACdC,YAAoC,CAAC,CAAC;IAEtC,MAAMC,MAAMV;IAEZ,iCAAiC;IACjC,MAAMW,UAAUV,iBAAiBS,IAAIE,GAAG,CAACvB,SAASS,aAAa;IAC/D,MAAMe,aAAaH,IAAIE,GAAG,CAACvB,SAASU,iBAAiB;IAErD,MAAMe,aAA4B,aAC7BvB,kBACCoB,YAAYR,aAAa;QAAEX,MAAMmB;IAAQ,GACzCE,eAAeV,aAAa;QAAEY,SAASF;IAAW;IAGxD,2CAA2C;IAC3C,MAAMG,cAA6B,aAC9BF,YACAL;IAGL,yCAAyC;IACzC,IAAIO,YAAYxB,IAAI,GAAGF,WAAW2B,GAAG,IAAID,YAAYxB,IAAI,GAAGF,WAAW4B,GAAG,EAAE;QAC1E,MAAM,IAAIC,MACR,CAAC,cAAc,EAAEH,YAAYxB,IAAI,CAAC,+BAA+B,EAAEF,WAAW2B,GAAG,CAAC,CAAC,EAAE3B,WAAW4B,GAAG,CAAC,EAAE,CAAC,GACrG,CAAC,yCAAyC,EAAE7B,SAASS,aAAa,CAAC,eAAe,CAAC;IAEzF;IAEA,qEAAqE;IACrE,IACE,CAACW,UAAUf,WAAW,IACtBsB,YAAYtB,WAAW,KAAKH,iBAAiBG,WAAW,EACxD;QACAsB,YAAYtB,WAAW,GAAG,CAAC,iBAAiB,EAAEsB,YAAYxB,IAAI,CAAC,CAAC;IAClE;IAEA,OAAOwB;AACT"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared constants for integration‑test‑doubles library.
3
+ */
4
+ /**
5
+ * Port range reserved for twin emulators.
6
+ * Twins should use fixed ports within this range to avoid collisions.
7
+ */
8
+ export declare const PORT_RANGE: {
9
+ readonly min: 9010;
10
+ readonly max: 9020;
11
+ };
12
+ /**
13
+ * Default port assignments for each twin.
14
+ */
15
+ export declare const DEFAULT_PORTS: {
16
+ /** Google Cloud Storage twin */
17
+ readonly GCS: 9013;
18
+ /** Gmail/Google Workspace Email twin (future) */
19
+ readonly GMAIL: 9014;
20
+ /** Google Auth twin (future) */
21
+ readonly GOOGLE_AUTH: 9015;
22
+ /** YouTube twin (future) */
23
+ readonly YOUTUBE: 9016;
24
+ /** Google Calendar twin (future) */
25
+ readonly GOOGLE_CALENDAR: 9017;
26
+ /** Spotify twin (future) */
27
+ readonly SPOTIFY: 9018;
28
+ };
29
+ /**
30
+ * Environment variable names used by the library.
31
+ */
32
+ export declare const ENV_VARS: {
33
+ /** Activation gate — must be 'true' for twins to start */
34
+ readonly TEST_MODE: "TEST_MODE";
35
+ /** GCS twin port override */
36
+ readonly GCS_TWIN_PORT: "GCS_TWIN_PORT";
37
+ /** GCS twin persistent data directory (optional) */
38
+ readonly GCS_TWIN_DATA_DIR: "GCS_TWIN_DATA_DIR";
39
+ };
40
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/shared/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB,gCAAgC;;IAEhC,iDAAiD;;IAEjD,gCAAgC;;IAEhC,4BAA4B;;IAE5B,oCAAoC;;IAEpC,4BAA4B;;CAEpB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,QAAQ;IACnB,0DAA0D;;IAE1D,6BAA6B;;IAE7B,oDAAoD;;CAE5C,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared constants for integration‑test‑doubles library.
3
+ */ /**
4
+ * Port range reserved for twin emulators.
5
+ * Twins should use fixed ports within this range to avoid collisions.
6
+ */ export const PORT_RANGE = {
7
+ min: 9010,
8
+ max: 9020
9
+ };
10
+ /**
11
+ * Default port assignments for each twin.
12
+ */ export const DEFAULT_PORTS = {
13
+ /** Google Cloud Storage twin */ GCS: 9013,
14
+ /** Gmail/Google Workspace Email twin (future) */ GMAIL: 9014,
15
+ /** Google Auth twin (future) */ GOOGLE_AUTH: 9015,
16
+ /** YouTube twin (future) */ YOUTUBE: 9016,
17
+ /** Google Calendar twin (future) */ GOOGLE_CALENDAR: 9017,
18
+ /** Spotify twin (future) */ SPOTIFY: 9018
19
+ };
20
+ /**
21
+ * Environment variable names used by the library.
22
+ */ export const ENV_VARS = {
23
+ /** Activation gate — must be 'true' for twins to start */ TEST_MODE: 'TEST_MODE',
24
+ /** GCS twin port override */ GCS_TWIN_PORT: 'GCS_TWIN_PORT',
25
+ /** GCS twin persistent data directory (optional) */ GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR'
26
+ };
27
+
28
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/shared/constants.ts"],"sourcesContent":["/**\n * Shared constants for integration‑test‑doubles library.\n */\n\n/**\n * Port range reserved for twin emulators.\n * Twins should use fixed ports within this range to avoid collisions.\n */\nexport const PORT_RANGE = {\n min: 9010,\n max: 9020,\n} as const;\n\n/**\n * Default port assignments for each twin.\n */\nexport const DEFAULT_PORTS = {\n /** Google Cloud Storage twin */\n GCS: 9013,\n /** Gmail/Google Workspace Email twin (future) */\n GMAIL: 9014,\n /** Google Auth twin (future) */\n GOOGLE_AUTH: 9015,\n /** YouTube twin (future) */\n YOUTUBE: 9016,\n /** Google Calendar twin (future) */\n GOOGLE_CALENDAR: 9017,\n /** Spotify twin (future) */\n SPOTIFY: 9018,\n} as const;\n\n/**\n * Environment variable names used by the library.\n */\nexport const ENV_VARS = {\n /** Activation gate — must be 'true' for twins to start */\n TEST_MODE: 'TEST_MODE',\n /** GCS twin port override */\n GCS_TWIN_PORT: 'GCS_TWIN_PORT',\n /** GCS twin persistent data directory (optional) */\n GCS_TWIN_DATA_DIR: 'GCS_TWIN_DATA_DIR',\n} as const;\n"],"names":["PORT_RANGE","min","max","DEFAULT_PORTS","GCS","GMAIL","GOOGLE_AUTH","YOUTUBE","GOOGLE_CALENDAR","SPOTIFY","ENV_VARS","TEST_MODE","GCS_TWIN_PORT","GCS_TWIN_DATA_DIR"],"rangeMappings":";;;;;;;;;;;;;;;;;;;;;;;;;","mappings":"AAAA;;CAEC,GAED;;;CAGC,GACD,OAAO,MAAMA,aAAa;IACxBC,KAAK;IACLC,KAAK;AACP,EAAW;AAEX;;CAEC,GACD,OAAO,MAAMC,gBAAgB;IAC3B,8BAA8B,GAC9BC,KAAK;IACL,+CAA+C,GAC/CC,OAAO;IACP,8BAA8B,GAC9BC,aAAa;IACb,0BAA0B,GAC1BC,SAAS;IACT,kCAAkC,GAClCC,iBAAiB;IACjB,0BAA0B,GAC1BC,SAAS;AACX,EAAW;AAEX;;CAEC,GACD,OAAO,MAAMC,WAAW;IACtB,wDAAwD,GACxDC,WAAW;IACX,2BAA2B,GAC3BC,eAAe;IACf,kDAAkD,GAClDC,mBAAmB;AACrB,EAAW"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@open-kingdom/shared-backend-integration-test-doubles",
3
+ "version": "0.0.2-17",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ "./package.json": "./package.json",
13
+ ".": {
14
+ "@open-kingdom/source": "./src/index.ts",
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "README.md",
22
+ "dist",
23
+ "!**/*.tsbuildinfo"
24
+ ],
25
+ "nx": {
26
+ "name": "integration-test-doubles",
27
+ "sourceRoot": "libs/shared/backend/integration-test-doubles/src",
28
+ "tags": [
29
+ "scope:shared",
30
+ "type:feature",
31
+ "environment:backend"
32
+ ],
33
+ "targets": {
34
+ "build": {
35
+ "executor": "@nx/js:swc",
36
+ "outputs": [
37
+ "{options.outputPath}"
38
+ ],
39
+ "options": {
40
+ "outputPath": "libs/shared/backend/integration-test-doubles/dist",
41
+ "main": "libs/shared/backend/integration-test-doubles/src/index.ts",
42
+ "tsConfig": "libs/shared/backend/integration-test-doubles/tsconfig.lib.json",
43
+ "skipTypeCheck": true,
44
+ "stripLeadingPaths": true
45
+ }
46
+ },
47
+ "start:twin": {
48
+ "command": "npx tsx libs/shared/backend/integration-test-doubles/scripts/start-twin.ts",
49
+ "dependsOn": [
50
+ "^build"
51
+ ]
52
+ }
53
+ }
54
+ },
55
+ "dependencies": {
56
+ "@open-kingdom/shared-poly-util-env-config": "0.0.2-17",
57
+ "dockerode": "^4.0.6",
58
+ "tslib": "^2.3.0"
59
+ }
60
+ }