galileo-generated 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.release-please-config.json +7 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/dist/commonjs/hooks/cert-management.d.ts +73 -0
  4. package/dist/commonjs/hooks/cert-management.d.ts.map +1 -0
  5. package/dist/commonjs/hooks/cert-management.js +258 -0
  6. package/dist/commonjs/hooks/cert-management.js.map +1 -0
  7. package/dist/commonjs/hooks/registration.d.ts.map +1 -1
  8. package/dist/commonjs/hooks/registration.js +8 -0
  9. package/dist/commonjs/hooks/registration.js.map +1 -1
  10. package/dist/commonjs/hooks/sdk-identifier.d.ts +5 -0
  11. package/dist/commonjs/hooks/sdk-identifier.d.ts.map +1 -0
  12. package/dist/commonjs/hooks/sdk-identifier.js +37 -0
  13. package/dist/commonjs/hooks/sdk-identifier.js.map +1 -0
  14. package/dist/commonjs/lib/galileo-config.d.ts +101 -12
  15. package/dist/commonjs/lib/galileo-config.d.ts.map +1 -1
  16. package/dist/commonjs/lib/galileo-config.js +153 -12
  17. package/dist/commonjs/lib/galileo-config.js.map +1 -1
  18. package/dist/commonjs/tests/hooks/cert-management.test.d.ts +2 -0
  19. package/dist/commonjs/tests/hooks/cert-management.test.d.ts.map +1 -0
  20. package/dist/commonjs/tests/hooks/cert-management.test.js +794 -0
  21. package/dist/commonjs/tests/hooks/cert-management.test.js.map +1 -0
  22. package/dist/commonjs/tests/hooks/sdk-identifier.test.d.ts +2 -0
  23. package/dist/commonjs/tests/hooks/sdk-identifier.test.d.ts.map +1 -0
  24. package/dist/commonjs/tests/hooks/sdk-identifier.test.js +136 -0
  25. package/dist/commonjs/tests/hooks/sdk-identifier.test.js.map +1 -0
  26. package/dist/commonjs/tests/lib/galileo-config.test.js +101 -0
  27. package/dist/commonjs/tests/lib/galileo-config.test.js.map +1 -1
  28. package/dist/esm/hooks/cert-management.d.ts +73 -0
  29. package/dist/esm/hooks/cert-management.d.ts.map +1 -0
  30. package/dist/esm/hooks/cert-management.js +254 -0
  31. package/dist/esm/hooks/cert-management.js.map +1 -0
  32. package/dist/esm/hooks/registration.d.ts.map +1 -1
  33. package/dist/esm/hooks/registration.js +8 -0
  34. package/dist/esm/hooks/registration.js.map +1 -1
  35. package/dist/esm/hooks/sdk-identifier.d.ts +5 -0
  36. package/dist/esm/hooks/sdk-identifier.d.ts.map +1 -0
  37. package/dist/esm/hooks/sdk-identifier.js +33 -0
  38. package/dist/esm/hooks/sdk-identifier.js.map +1 -0
  39. package/dist/esm/lib/galileo-config.d.ts +101 -12
  40. package/dist/esm/lib/galileo-config.d.ts.map +1 -1
  41. package/dist/esm/lib/galileo-config.js +153 -12
  42. package/dist/esm/lib/galileo-config.js.map +1 -1
  43. package/dist/esm/tests/hooks/cert-management.test.d.ts +2 -0
  44. package/dist/esm/tests/hooks/cert-management.test.d.ts.map +1 -0
  45. package/dist/esm/tests/hooks/cert-management.test.js +792 -0
  46. package/dist/esm/tests/hooks/cert-management.test.js.map +1 -0
  47. package/dist/esm/tests/hooks/sdk-identifier.test.d.ts +2 -0
  48. package/dist/esm/tests/hooks/sdk-identifier.test.d.ts.map +1 -0
  49. package/dist/esm/tests/hooks/sdk-identifier.test.js +134 -0
  50. package/dist/esm/tests/hooks/sdk-identifier.test.js.map +1 -0
  51. package/dist/esm/tests/lib/galileo-config.test.js +101 -0
  52. package/dist/esm/tests/lib/galileo-config.test.js.map +1 -1
  53. package/package.json +5 -3
  54. package/src/hooks/cert-management.ts +288 -0
  55. package/src/hooks/registration.ts +10 -0
  56. package/src/hooks/sdk-identifier.ts +41 -0
  57. package/src/lib/galileo-config.ts +214 -15
  58. package/src/tests/hooks/cert-management.test.ts +958 -0
  59. package/src/tests/hooks/sdk-identifier.test.ts +176 -0
  60. package/src/tests/lib/galileo-config.test.ts +110 -0
@@ -0,0 +1,288 @@
1
+ /*
2
+ * Certificate management SDK init hook: configures TLS/SSL for fetch API using undici Agent.
3
+ *
4
+ * Reads certificate configuration from GalileoConfig singleton and applies it to all SDK HTTP requests.
5
+ * Supports custom CA certificates, client certificates (mutual TLS), and certificate validation controls.
6
+ *
7
+ * Configuration sources (via GalileoConfig):
8
+ * - GALILEO_CA_CERT_PATH / GALILEO_CA_CERT_CONTENT: Custom CA certificate(s)
9
+ * - GALILEO_CLIENT_CERT_PATH + GALILEO_CLIENT_KEY_PATH: Client certificate and key for mTLS
10
+ * - GALILEO_REJECT_UNAUTHORIZED / NODE_TLS_REJECT_UNAUTHORIZED: Control certificate validation
11
+ * - SSL_CERT_FILE: Python httpx compatibility (treated as CA certificate)
12
+ * - NODE_EXTRA_CA_CERTS: Node.js native support for appending to default CA list (works without this hook)
13
+ *
14
+ * ⚠️ REQUIRES Node.js >= 20.18.1 for undici dispatcher support in fetch API.
15
+ * ⚠️ Gracefully skips on older Node.js versions or non-Node.js runtimes (browser, Deno).
16
+ * ⚠️ Only creates undici Agent if meaningful TLS customization is detected (prevents unnecessary overhead).
17
+ */
18
+
19
+ import { readFileSync, existsSync } from 'fs';
20
+ import { HTTPClient } from '../lib/http.js';
21
+ import { GalileoConfig } from '../lib/galileo-config.js';
22
+ import { isNodeLike } from '../lib/runtime.js';
23
+ import type { SDKInitHook } from './types.js';
24
+ import type { SDKOptions } from '../lib/config.js';
25
+ import { getSdkLogger } from '../lib/sdk-logger.js';
26
+ const sdkLogger = getSdkLogger();
27
+
28
+ type AgentConstructor = new (options: {
29
+ connect: {
30
+ ca?: string;
31
+ cert?: string;
32
+ key?: string;
33
+ rejectUnauthorized?: boolean;
34
+ };
35
+ }) => object;
36
+
37
+ let CertAgent: AgentConstructor | undefined;
38
+
39
+ try {
40
+ // Using synchronous require to support both ESM and CommonJS contexts
41
+ CertAgent = require('undici').Agent;
42
+ } catch (error) {
43
+ sdkLogger.warn(`[TLS] Failed to import undici: ${error}`);
44
+ }
45
+
46
+
47
+ /**
48
+ * SDK initialization hook that configures TLS/SSL certificates for all SDK HTTP requests.
49
+ *
50
+ * This hook reads certificate configuration from GalileoConfig and applies it by creating
51
+ * a custom undici Agent as the fetch dispatcher. Only runs on Node.js >= 20.18.1 with undici available.
52
+ *
53
+ * Configuration sources (environment variables resolved via GalileoConfig):
54
+ * - GALILEO_CA_CERT_PATH or GALILEO_CA_CERT_CONTENT: Custom CA certificate(s) for server verification
55
+ * - GALILEO_CLIENT_CERT_PATH + GALILEO_CLIENT_KEY_PATH: Client certificate and key for mutual TLS (both required)
56
+ * - GALILEO_REJECT_UNAUTHORIZED: Control whether to accept self-signed/unauthorized certificates
57
+ * (also falls back to NODE_TLS_REJECT_UNAUTHORIZED if GALILEO_REJECT_UNAUTHORIZED not set)
58
+ * - SSL_CERT_FILE: Python httpx-style CA cert file (supported for compatibility)
59
+ *
60
+ * Implementation details:
61
+ * - Skips gracefully on browsers, Deno, or Node.js without undici support
62
+ * - Returns original opts if no certificate configuration is present
63
+ * - Validates mutual TLS: requires both clientCertPath and clientKeyPath; fails if only one is set
64
+ * - Only creates undici Agent if there's meaningful TLS customization (avoids unnecessary overhead)
65
+ * - Wraps fetch with a custom dispatcher to apply the TLS configuration to all SDK requests
66
+ *
67
+ * @implements {SDKInitHook}
68
+ */
69
+ export class CertManagementHook implements SDKInitHook {
70
+ /**
71
+ * Initializes SDK options with TLS certificate configuration.
72
+ *
73
+ * Reads certificate config from GalileoConfig, creates an undici Agent if needed,
74
+ * and augments the HTTPClient (if present) with a beforeRequest hook that injects
75
+ * the TLS dispatcher. This approach preserves any custom HTTPClient and its hooks
76
+ * while layering TLS configuration on top.
77
+ *
78
+ * @param opts - The original SDK options
79
+ * @returns Enhanced SDKOptions with TLS configuration applied, or original opts otherwise
80
+ */
81
+ sdkInit(opts: SDKOptions): SDKOptions {
82
+ if (!isNodeLike() || !CertAgent) {
83
+ return opts;
84
+ }
85
+
86
+ // Get certificate configuration from GalileoConfig singleton
87
+ const cert = GalileoConfig.get().getCertConfig();
88
+ if (!cert) {
89
+ return opts;
90
+ }
91
+
92
+ try {
93
+ // Determine CA certificate source (prefer direct content over file path)
94
+ let ca: string | undefined | null;
95
+
96
+ if (cert.caCertContent) {
97
+ // CA provided directly as string (GALILEO_CA_CERT_CONTENT)
98
+ ca = cert.caCertContent;
99
+ } else if (cert.caCertPath) {
100
+ // CA certificate path provided (GALILEO_CA_CERT_PATH); read file from disk
101
+ ca = this.readFileWarning(cert.caCertPath, 'CA certificate');
102
+ if (!ca) return opts;
103
+ }
104
+
105
+ // Build undici Agent connect options (TLS settings passed to undici's socket connector)
106
+ const connectOptions: {
107
+ ca?: string;
108
+ cert?: string;
109
+ key?: string;
110
+ rejectUnauthorized?: boolean;
111
+ } = {};
112
+
113
+ if (ca) {
114
+ connectOptions.ca = ca;
115
+ }
116
+
117
+ // Validate mutual TLS: both cert and key must be configured together (all-or-nothing)
118
+ if ((cert.clientCertPath || cert.clientKeyPath) && !(cert.clientCertPath && cert.clientKeyPath)) {
119
+ sdkLogger.error('[TLS] Mutual TLS requires both GALILEO_CLIENT_CERT_PATH and GALILEO_CLIENT_KEY_PATH to be set');
120
+ return opts;
121
+ }
122
+
123
+ // Load client certificate (mutual TLS) if provided
124
+ const clientCert = cert.clientCertPath ? this.readFileWarning(cert.clientCertPath, 'Client cert') : null;
125
+ if (cert.clientCertPath && !clientCert) return opts;
126
+ if (clientCert) connectOptions.cert = clientCert;
127
+
128
+ // Load client key (mutual TLS) if provided
129
+ const clientKey = cert.clientKeyPath ? this.readFileWarning(cert.clientKeyPath, 'Client key') : null;
130
+ if (cert.clientKeyPath && !clientKey) return opts;
131
+ if (clientKey) connectOptions.key = clientKey;
132
+
133
+ // Apply certificate validation setting (whether to accept self-signed/unauthorized certs)
134
+ if (cert.rejectUnauthorized !== undefined)
135
+ connectOptions.rejectUnauthorized = cert.rejectUnauthorized;
136
+
137
+ // Guard: Only create undici Agent if there's meaningful TLS customization
138
+ // This avoids unnecessary overhead when only rejectUnauthorized=true (default behavior)
139
+ const hasCertCustomization = Boolean(connectOptions.ca || connectOptions.cert || connectOptions.key || connectOptions.rejectUnauthorized === false);
140
+ if (!hasCertCustomization) {
141
+ return opts;
142
+ }
143
+
144
+ // Create undici Agent with the configured TLS settings (singleton, reused for all requests)
145
+ const agent = new CertAgent({
146
+ connect: connectOptions
147
+ });
148
+
149
+ // Get or create HTTPClient to augment
150
+ const httpClient = opts.httpClient || new HTTPClient();
151
+
152
+ // Warn if this runtime may not support Request.dispatcher (Node.js < 20.18.1)
153
+ this.warnIfDispatcherUnsupported();
154
+
155
+ // Add a beforeRequest hook that injects the TLS dispatcher into requests.
156
+ // This hook attaches the pre-created agent to the request, applying TLS configuration
157
+ // while preserving any user-registered hooks (which execute before this one).
158
+ //
159
+ // The dispatcher option is a Node.js-specific extension for undici integration.
160
+ // Supported on Node.js >= 20.18.1 with undici available.
161
+ // On older runtimes or non-Node.js environments, the dispatcher will be silently ignored
162
+ // (Request constructor doesn't throw on unknown properties, just ignores them).
163
+ // See: https://nodejs.org/docs/latest/api/fetch.html#fetchinit-options
164
+ httpClient.addHook('beforeRequest', (req: Request): Request => {
165
+ // Create a new Request with the TLS dispatcher injected.
166
+ // The hook receives a cloned request, so the body is readable and safe to transfer.
167
+ return new Request(req.url, {
168
+ method: req.method,
169
+ headers: req.headers,
170
+ body: req.body,
171
+ // @ts-expect-error - dispatcher is Node.js-specific undici extension, not in standard fetch spec
172
+ dispatcher: agent
173
+ });
174
+ });
175
+
176
+ return {
177
+ ...opts,
178
+ httpClient: httpClient
179
+ };
180
+
181
+ } catch (error) {
182
+ sdkLogger.error(`[TLS] Failed to configure custom certificates: ${error}`);
183
+ return opts;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Warns if the current Node.js version may not support Request.dispatcher.
189
+ *
190
+ * Request.dispatcher is a Node.js-specific extension for undici integration.
191
+ * It's supported on Node.js >= 20.18.1. On older versions, the dispatcher
192
+ * property will be silently ignored, and TLS certificates may not be applied.
193
+ *
194
+ * See: https://nodejs.org/docs/latest/api/fetch.html#fetchinit-options
195
+ */
196
+ private warnIfDispatcherUnsupported(): void {
197
+ // Only check on Node.js-like environments; skip on browser/Deno
198
+ if (!isNodeLike()) {
199
+ return; // Non-Node.js runtimes don't support dispatcher anyway (expected)
200
+ }
201
+
202
+ try {
203
+ const nodeVersion = this.getNodeVersion();
204
+ if (nodeVersion && !this.isNodeVersionSupported(nodeVersion)) {
205
+ sdkLogger.warn(
206
+ `[TLS] Node.js ${nodeVersion} detected. Request.dispatcher (required for TLS support) ` +
207
+ `is available from Node.js 20.18.1+. Upgrade Node.js to ensure certificates are applied. ` +
208
+ `See: https://nodejs.org/docs/latest/api/fetch.html#fetchinit-options`
209
+ );
210
+ }
211
+ } catch (error) {
212
+ // If version detection fails, silently skip the warning
213
+ // (version detection is best-effort for user convenience)
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Extracts the Node.js version from process.versions.
219
+ *
220
+ * @returns Version string (e.g., "20.10.0") or null if not detectable
221
+ */
222
+ private getNodeVersion(): string | null {
223
+ try {
224
+ const proc = (globalThis as unknown as {
225
+ process?: { versions?: { node?: string } };
226
+ }).process;
227
+
228
+ if (proc?.versions?.node) {
229
+ return proc.versions.node;
230
+ }
231
+ } catch {
232
+ // Ignore errors; version detection is non-critical
233
+ }
234
+ return null;
235
+ }
236
+
237
+ /**
238
+ * Checks if a Node.js version is >= 20.18.1 (minimum for Request.dispatcher support).
239
+ *
240
+ * @param versionStr - Version string (e.g., "20.10.0", "21.0.0")
241
+ * @returns true if version >= 20.18.1, false otherwise
242
+ */
243
+ private isNodeVersionSupported(versionStr: string): boolean {
244
+ try {
245
+ // Handle empty or obviously invalid strings early
246
+ if (!versionStr || typeof versionStr !== 'string') {
247
+ return true; // Can't parse, assume supported (optimistic)
248
+ }
249
+
250
+ const parts = versionStr.split('.').map(v => parseInt(v, 10));
251
+ const major = parts[0];
252
+ const minor = parts[1] ?? 0;
253
+ const patch = parts[2] ?? 0;
254
+
255
+ // If major version couldn't be parsed (NaN or undefined), assume supported
256
+ if (major === undefined || Number.isNaN(major)) return true;
257
+
258
+ // Need: major > 20 OR (major === 20 AND minor > 18) OR (major === 20 AND minor === 18 AND patch >= 1)
259
+ if (major > 20) return true;
260
+ if (major === 20) {
261
+ if (minor > 18) return true;
262
+ if (minor === 18 && patch >= 1) return true;
263
+ }
264
+ return false;
265
+ } catch {
266
+ // If any parsing error occurs, assume version is supported (optimistic)
267
+ return true;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Safely reads a certificate/key file from disk with error reporting.
273
+ *
274
+ * Checks file existence before reading to provide clear warning messages.
275
+ * Returns null if file doesn't exist or read fails; logs a warning in either case.
276
+ *
277
+ * @param filePath - Absolute or relative path to the certificate/key file
278
+ * @param fileType - Human-readable description for error messages (e.g., "CA certificate", "Client cert", "Client key")
279
+ * @returns File content as UTF-8 string, or null if file doesn't exist or read fails
280
+ */
281
+ private readFileWarning(filePath: string, fileType: string): string | null {
282
+ if (!existsSync(filePath)) {
283
+ sdkLogger.warn(`[TLS] ${fileType} file not found: ${filePath}`);
284
+ return null;
285
+ }
286
+ return readFileSync(filePath, 'utf-8');
287
+ }
288
+ }
@@ -1,5 +1,7 @@
1
+ import { CertManagementHook } from "./cert-management.js";
1
2
  import { ErrorCleanerHook } from "./error-cleaner.js";
2
3
  import { TokenManagementHook } from "./token-management.js";
4
+ import { SDKIdentifierHook } from "./sdk-identifier.js";
3
5
  import type { Hooks } from "./types.js";
4
6
 
5
7
  /*
@@ -9,6 +11,10 @@ import type { Hooks } from "./types.js";
9
11
  */
10
12
 
11
13
  export function initHooks(hooks: Hooks) {
14
+ // Register cert management (TLS with undici Agent)
15
+ const certHook = new CertManagementHook();
16
+ hooks.registerSDKInitHook(certHook);
17
+
12
18
  // Register token management hooks
13
19
  const tokenHook = new TokenManagementHook();
14
20
  hooks.registerBeforeRequestHook(tokenHook);
@@ -17,4 +23,8 @@ export function initHooks(hooks: Hooks) {
17
23
  // Register error cleaning hook
18
24
  const errorCleanerHook = new ErrorCleanerHook();
19
25
  hooks.registerAfterErrorHook(errorCleanerHook);
26
+
27
+ // Register SDK identifier hook
28
+ const sdkIdentifierHook = new SDKIdentifierHook();
29
+ hooks.registerBeforeRequestHook(sdkIdentifierHook);
20
30
  }
@@ -0,0 +1,41 @@
1
+ import type { BeforeRequestContext, BeforeRequestHook } from "./types.js";
2
+
3
+ let cachedVersion: string | null = null;
4
+
5
+ function loadVersion(): string {
6
+ if (cachedVersion) {
7
+ return cachedVersion;
8
+ }
9
+
10
+ try {
11
+ // NOTES: require is being used for now, using import demands appropriate
12
+ // compiler configuration, which won't be enabled yet due to Speakeasy's
13
+ // particular way of enabling persistent edits conflicting with workflow.
14
+ // Ticket sc-56960 created for this investigation.
15
+ const packageJsonPath = require.resolve("galileo-generated/package.json");
16
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
17
+ const packageJson = require(packageJsonPath);
18
+ cachedVersion = packageJson.version;
19
+ return cachedVersion ?? "unknown";
20
+ } catch {
21
+ return "unknown";
22
+ }
23
+ }
24
+
25
+ function getSdkIdentifier(version: string): string {
26
+ return `galileo-generated/${version}`;
27
+ }
28
+
29
+ export class SDKIdentifierHook implements BeforeRequestHook {
30
+ async beforeRequest(
31
+ _hookCtx: BeforeRequestContext,
32
+ request: Request,
33
+ ): Promise<Request> {
34
+ const newRequest = request.clone();
35
+
36
+ const version = loadVersion();
37
+ const sdkIdentifier = getSdkIdentifier(version);
38
+ newRequest.headers.set("X-Galileo-SDK", sdkIdentifier);
39
+ return newRequest;
40
+ }
41
+ }