@deployport/api-services-corelib 0.1.4 → 0.3.0

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.
@@ -0,0 +1,240 @@
1
+ // Node-only configuration loader. This module is selected via the
2
+ // `#config-loader` package import when running under Node (see package.json
3
+ // `imports`). It is NEVER bundled for the browser, so it may freely import
4
+ // Node builtins. The browser gets `loader.browser.ts` instead.
5
+ //
6
+ // The resolution chain mirrors the Go corelib (configurator/config_files.go,
7
+ // configurator/configurator.go): explicit config -> DEPLOYPORT_* env ->
8
+ // ~/.deployport files (for the resolved profile) -> DefaultRegion.
9
+
10
+ import { readFile } from "node:fs/promises";
11
+ import { homedir } from "node:os";
12
+ import { parse as parseYAML } from "yaml";
13
+ import {
14
+ type AccountScope,
15
+ type Config,
16
+ type DeployportConfig,
17
+ type GetEndpointOverride,
18
+ type LoadDefaultConfig,
19
+ DEFAULT_CREDENTIALS_FILE_PATH,
20
+ DEFAULT_PROFILE,
21
+ DEFAULT_REGION,
22
+ DEFAULT_SETTINGS_FILE_PATH,
23
+ ENV_ACCESS_KEY_ID,
24
+ ENV_PROFILE,
25
+ ENV_REGION,
26
+ ENV_SECRET_ACCESS_KEY,
27
+ } from "./config.js";
28
+
29
+ // --- Parsed file shapes (match the YAML the Go SDK reads/writes) ------------
30
+
31
+ type AccountCredentials = {
32
+ accessKeyID?: string;
33
+ secretAccessKey?: string;
34
+ };
35
+
36
+ type ProfileCredentials = {
37
+ global?: AccountCredentials;
38
+ current?: AccountCredentials;
39
+ };
40
+
41
+ type CredentialsFile = {
42
+ profiles?: Record<string, ProfileCredentials | undefined>;
43
+ };
44
+
45
+ type ServiceSettings = {
46
+ endpoint?: { url?: string };
47
+ };
48
+
49
+ type ProfileSettings = {
50
+ region?: string;
51
+ services?: Record<string, ServiceSettings | undefined>;
52
+ };
53
+
54
+ type SettingsFile = {
55
+ profiles?: Record<string, ProfileSettings | undefined>;
56
+ };
57
+
58
+ // --- Public API -------------------------------------------------------------
59
+
60
+ /**
61
+ * loadDefaultConfig resolves the DeployPort config using the full default
62
+ * chain, filling only values not already set explicitly: environment variables,
63
+ * then the default config files for the resolved profile, then DEFAULT_REGION
64
+ * as a last resort. Explicit values always win. Missing files are ignored;
65
+ * malformed files throw.
66
+ */
67
+ export async function loadDefaultConfig(config?: Config): Promise<Config> {
68
+ const dp: DeployportConfig = { ...(config?.deployport ?? {}) };
69
+ const anonymous = dp.anonymous ?? false;
70
+ const scope: AccountScope = dp.accountScope ?? "auto";
71
+ const profile = resolveProfile(dp.profile);
72
+
73
+ // Credentials are treated as a unit: if the caller set either field
74
+ // explicitly, we do not auto-load them from env or files (mirrors Go's
75
+ // `Credentials == nil` gate).
76
+ const credsExplicit = dp.accessKeyID != null || dp.secretAccessKey != null;
77
+ if (!anonymous && !credsExplicit) {
78
+ const envKeyID = process.env[ENV_ACCESS_KEY_ID];
79
+ const envSecret = process.env[ENV_SECRET_ACCESS_KEY];
80
+ if (envKeyID || envSecret) {
81
+ // Env provides the credentials as a unit; files are skipped.
82
+ dp.accessKeyID = envKeyID ?? "";
83
+ dp.secretAccessKey = envSecret ?? "";
84
+ } else {
85
+ const credsFile = await loadCredentialsFile(credentialsPath(dp));
86
+ const acc = accountFor(credsFile, profile, scope);
87
+ if (acc && acc.accessKeyID) {
88
+ dp.accessKeyID = acc.accessKeyID;
89
+ dp.secretAccessKey = acc.secretAccessKey ?? "";
90
+ }
91
+ }
92
+ }
93
+
94
+ // Region: explicit -> env -> settings file -> default.
95
+ if (!dp.region) {
96
+ const envRegion = process.env[ENV_REGION];
97
+ if (envRegion) {
98
+ dp.region = envRegion;
99
+ }
100
+ }
101
+ if (!dp.region) {
102
+ const settingsFile = await loadSettingsFile(settingsPath(dp));
103
+ const region = regionFor(settingsFile, profile);
104
+ if (region) {
105
+ dp.region = region;
106
+ }
107
+ }
108
+ if (!dp.region) {
109
+ dp.region = DEFAULT_REGION;
110
+ }
111
+
112
+ return { ...config, deployport: dp };
113
+ }
114
+
115
+ /**
116
+ * getEndpointOverride reads `profiles.<profile>.services.<service>.endpoint.url`
117
+ * from the settings file, or "" if none is set. Lets a Node consumer (e.g. a
118
+ * CLI) pick a per-service endpoint while corelib stays service-agnostic.
119
+ */
120
+ export async function getEndpointOverride(
121
+ service: string,
122
+ options?: { profile?: string; settingsFilePath?: string },
123
+ ): Promise<string> {
124
+ const profile = resolveProfile(options?.profile);
125
+ const settingsFile = await loadSettingsFile(
126
+ options?.settingsFilePath ? expandPath(options.settingsFilePath) : defaultSettingsPath(),
127
+ );
128
+ return endpointFor(settingsFile, profile, service);
129
+ }
130
+
131
+ // --- Resolution helpers (mirror Go) -----------------------------------------
132
+
133
+ function resolveProfile(explicit?: string): string {
134
+ if (explicit) {
135
+ return explicit;
136
+ }
137
+ const fromEnv = process.env[ENV_PROFILE];
138
+ if (fromEnv) {
139
+ return fromEnv;
140
+ }
141
+ return DEFAULT_PROFILE;
142
+ }
143
+
144
+ function accountFor(
145
+ file: CredentialsFile,
146
+ profile: string,
147
+ scope: AccountScope,
148
+ ): AccountCredentials | undefined {
149
+ const p = file.profiles?.[profile];
150
+ if (!p) {
151
+ return undefined;
152
+ }
153
+ switch (scope) {
154
+ case "global":
155
+ return p.global;
156
+ case "current":
157
+ return p.current;
158
+ default: // "auto"
159
+ return p.current ?? p.global;
160
+ }
161
+ }
162
+
163
+ function regionFor(file: SettingsFile, profile: string): string {
164
+ return file.profiles?.[profile]?.region ?? "";
165
+ }
166
+
167
+ function endpointFor(file: SettingsFile, profile: string, service: string): string {
168
+ return file.profiles?.[profile]?.services?.[service]?.endpoint?.url ?? "";
169
+ }
170
+
171
+ // --- File loading ------------------------------------------------------------
172
+
173
+ async function loadCredentialsFile(path: string): Promise<CredentialsFile> {
174
+ return loadYamlFile<CredentialsFile>(path, "credentials");
175
+ }
176
+
177
+ async function loadSettingsFile(path: string): Promise<SettingsFile> {
178
+ return loadYamlFile<SettingsFile>(path, "settings");
179
+ }
180
+
181
+ async function loadYamlFile<T extends object>(path: string, kind: string): Promise<T> {
182
+ let data: string;
183
+ try {
184
+ data = await readFile(path, "utf8");
185
+ } catch (err) {
186
+ if (isNotFound(err)) {
187
+ return { profiles: {} } as T;
188
+ }
189
+ throw new Error(`failed to read ${kind} file: ${errMessage(err)}`);
190
+ }
191
+ try {
192
+ return (parseYAML(data) as T) ?? ({ profiles: {} } as T);
193
+ } catch (err) {
194
+ throw new Error(`failed to parse ${kind} file: ${errMessage(err)}`);
195
+ }
196
+ }
197
+
198
+ function isNotFound(err: unknown): boolean {
199
+ return (err as NodeJS.ErrnoException)?.code === "ENOENT";
200
+ }
201
+
202
+ function errMessage(err: unknown): string {
203
+ return err instanceof Error ? err.message : String(err);
204
+ }
205
+
206
+ // --- Path resolution ---------------------------------------------------------
207
+
208
+ function credentialsPath(dp: DeployportConfig): string {
209
+ return dp.credentialsFilePath
210
+ ? expandPath(dp.credentialsFilePath)
211
+ : defaultCredentialsPath();
212
+ }
213
+
214
+ function settingsPath(dp: DeployportConfig): string {
215
+ return dp.settingsFilePath ? expandPath(dp.settingsFilePath) : defaultSettingsPath();
216
+ }
217
+
218
+ function defaultCredentialsPath(): string {
219
+ return expandPath(DEFAULT_CREDENTIALS_FILE_PATH);
220
+ }
221
+
222
+ function defaultSettingsPath(): string {
223
+ return expandPath(DEFAULT_SETTINGS_FILE_PATH);
224
+ }
225
+
226
+ /** Expands a leading `~` and `$HOME`/`${HOME}` to the user's home directory. */
227
+ function expandPath(path: string): string {
228
+ const home = homedir();
229
+ return path
230
+ .replace(/^~(?=\/|$)/, home)
231
+ .replace(/\$\{HOME\}/g, home)
232
+ .replace(/\$HOME/g, home);
233
+ }
234
+
235
+ // Compile-time guard: this variant must match the shared loader signatures so
236
+ // `#config-loader` resolves identically in Node and the browser.
237
+ const _loadDefaultConfig: LoadDefaultConfig = loadDefaultConfig;
238
+ const _getEndpointOverride: GetEndpointOverride = getEndpointOverride;
239
+ void _loadDefaultConfig;
240
+ void _getEndpointOverride;
@@ -0,0 +1,243 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { getEndpointOverride, loadDefaultConfig } from "./loader.node.js";
6
+ import { loadDefaultConfig as browserLoadDefaultConfig } from "./loader.browser.js";
7
+
8
+ // Mirrors api-services-corelib-go/configurator/config_files_test.go.
9
+
10
+ const sampleCredentialsYAML = `profiles:
11
+ default:
12
+ current:
13
+ accessKeyID: AKcurrent
14
+ secretAccessKey: SKcurrent
15
+ global:
16
+ accessKeyID: AKglobal
17
+ secretAccessKey: SKglobal
18
+ `;
19
+
20
+ const sampleSettingsYAML = `profiles:
21
+ default:
22
+ region: ljnyc
23
+ services:
24
+ iam: {}
25
+ binara:
26
+ endpoint:
27
+ url: https://ljb.deployport.io
28
+ `;
29
+
30
+ const ENV_KEYS = [
31
+ "HOME",
32
+ "DEPLOYPORT_ACCESS_KEY_ID",
33
+ "DEPLOYPORT_SECRET_ACCESS_KEY",
34
+ "DEPLOYPORT_REGION",
35
+ "DEPLOYPORT_PROFILE",
36
+ ] as const;
37
+
38
+ let savedEnv: Record<string, string | undefined>;
39
+ let tmpDirs: string[];
40
+
41
+ beforeEach(() => {
42
+ savedEnv = {};
43
+ for (const k of ENV_KEYS) {
44
+ savedEnv[k] = process.env[k];
45
+ }
46
+ tmpDirs = [];
47
+ // Start every test from a known state.
48
+ delete process.env.DEPLOYPORT_ACCESS_KEY_ID;
49
+ delete process.env.DEPLOYPORT_SECRET_ACCESS_KEY;
50
+ delete process.env.DEPLOYPORT_REGION;
51
+ delete process.env.DEPLOYPORT_PROFILE;
52
+ });
53
+
54
+ afterEach(() => {
55
+ for (const k of ENV_KEYS) {
56
+ if (savedEnv[k] === undefined) {
57
+ delete process.env[k];
58
+ } else {
59
+ process.env[k] = savedEnv[k];
60
+ }
61
+ }
62
+ for (const d of tmpDirs) {
63
+ rmSync(d, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ function newTmpDir(): string {
68
+ const d = mkdtempSync(join(tmpdir(), "dpp-cfg-"));
69
+ tmpDirs.push(d);
70
+ return d;
71
+ }
72
+
73
+ // Writes credentials.yml and settings.yml under a fresh HOME/.deployport and
74
+ // points HOME at it, so the default file paths resolve there.
75
+ function writeConfigFiles(cred: string, settings: string): void {
76
+ const home = newTmpDir();
77
+ const dir = join(home, ".deployport");
78
+ mkdirSync(dir, { recursive: true });
79
+ if (cred) {
80
+ writeFileSync(join(dir, "credentials.yml"), cred);
81
+ }
82
+ if (settings) {
83
+ writeFileSync(join(dir, "settings.yml"), settings);
84
+ }
85
+ process.env.HOME = home;
86
+ }
87
+
88
+ describe("loadDefaultConfig (Node)", () => {
89
+ test("resolves credentials (auto -> current) and region from files", async () => {
90
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
91
+ const c = await loadDefaultConfig();
92
+ expect(c.deployport?.accessKeyID).toBe("AKcurrent");
93
+ expect(c.deployport?.secretAccessKey).toBe("SKcurrent");
94
+ expect(c.deployport?.region).toBe("ljnyc");
95
+ });
96
+
97
+ test("account scope global selects the global account", async () => {
98
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
99
+ const c = await loadDefaultConfig({ deployport: { accountScope: "global" } });
100
+ expect(c.deployport?.accessKeyID).toBe("AKglobal");
101
+ });
102
+
103
+ test("auto falls back to global when current is absent", async () => {
104
+ const globalOnly = `profiles:
105
+ default:
106
+ global:
107
+ accessKeyID: G
108
+ secretAccessKey: S
109
+ `;
110
+ writeConfigFiles(globalOnly, "");
111
+ const c = await loadDefaultConfig();
112
+ expect(c.deployport?.accessKeyID).toBe("G");
113
+ });
114
+
115
+ test("env overrides file", async () => {
116
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
117
+ process.env.DEPLOYPORT_ACCESS_KEY_ID = "AKenv";
118
+ process.env.DEPLOYPORT_SECRET_ACCESS_KEY = "SKenv";
119
+ process.env.DEPLOYPORT_REGION = "us-env-1";
120
+ const c = await loadDefaultConfig();
121
+ expect(c.deployport?.accessKeyID).toBe("AKenv");
122
+ expect(c.deployport?.region).toBe("us-env-1");
123
+ });
124
+
125
+ test("explicit wins over env and file", async () => {
126
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
127
+ process.env.DEPLOYPORT_ACCESS_KEY_ID = "AKenv";
128
+ process.env.DEPLOYPORT_SECRET_ACCESS_KEY = "SKenv";
129
+ process.env.DEPLOYPORT_REGION = "us-env-1";
130
+ const c = await loadDefaultConfig({
131
+ deployport: {
132
+ accessKeyID: "AKexplicit",
133
+ secretAccessKey: "SKexplicit",
134
+ region: "explicit-region",
135
+ },
136
+ });
137
+ expect(c.deployport?.accessKeyID).toBe("AKexplicit");
138
+ expect(c.deployport?.region).toBe("explicit-region");
139
+ });
140
+
141
+ test("no source -> DefaultRegion, no credentials", async () => {
142
+ writeConfigFiles("", ""); // fresh empty HOME, no files
143
+ const c = await loadDefaultConfig();
144
+ expect(c.deployport?.region).toBe("us-east-2");
145
+ expect(c.deployport?.accessKeyID).toBeUndefined();
146
+ });
147
+
148
+ test("custom file paths", async () => {
149
+ const dir = newTmpDir();
150
+ const credPath = join(dir, "creds.yml");
151
+ const setPath = join(dir, "sets.yml");
152
+ writeFileSync(credPath, sampleCredentialsYAML);
153
+ writeFileSync(setPath, sampleSettingsYAML);
154
+ process.env.HOME = newTmpDir(); // empty home so defaults resolve to nothing
155
+ const c = await loadDefaultConfig({
156
+ deployport: { credentialsFilePath: credPath, settingsFilePath: setPath },
157
+ });
158
+ expect(c.deployport?.accessKeyID).toBe("AKcurrent");
159
+ expect(c.deployport?.region).toBe("ljnyc");
160
+ });
161
+
162
+ test("anonymous skips credential auto-loading but still resolves region", async () => {
163
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
164
+ process.env.DEPLOYPORT_ACCESS_KEY_ID = "AKenv";
165
+ process.env.DEPLOYPORT_SECRET_ACCESS_KEY = "SKenv";
166
+ const c = await loadDefaultConfig({ deployport: { anonymous: true } });
167
+ expect(c.deployport?.accessKeyID).toBeUndefined();
168
+ expect(c.deployport?.region).toBe("ljnyc");
169
+ });
170
+
171
+ test("profile selection", async () => {
172
+ const cred = `profiles:
173
+ other:
174
+ current:
175
+ accessKeyID: AKother
176
+ secretAccessKey: SKother
177
+ `;
178
+ const settings = `profiles:
179
+ other:
180
+ region: other-region
181
+ `;
182
+ writeConfigFiles(cred, settings);
183
+ const c = await loadDefaultConfig({ deployport: { profile: "other" } });
184
+ expect(c.deployport?.accessKeyID).toBe("AKother");
185
+ expect(c.deployport?.region).toBe("other-region");
186
+ });
187
+
188
+ test("profile from DEPLOYPORT_PROFILE env", async () => {
189
+ const cred = `profiles:
190
+ other:
191
+ current:
192
+ accessKeyID: AKother
193
+ secretAccessKey: SKother
194
+ `;
195
+ writeConfigFiles(cred, "");
196
+ process.env.DEPLOYPORT_PROFILE = "other";
197
+ const c = await loadDefaultConfig();
198
+ expect(c.deployport?.accessKeyID).toBe("AKother");
199
+ });
200
+
201
+ test("malformed file throws", async () => {
202
+ writeConfigFiles("profiles: [unterminated", "");
203
+ await expect(loadDefaultConfig()).rejects.toThrow(/failed to parse credentials file/);
204
+ });
205
+ });
206
+
207
+ describe("getEndpointOverride (Node)", () => {
208
+ test("reads a per-service endpoint override", async () => {
209
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
210
+ expect(await getEndpointOverride("binara")).toBe("https://ljb.deployport.io");
211
+ });
212
+
213
+ test("service present but no override -> empty", async () => {
214
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
215
+ expect(await getEndpointOverride("iam")).toBe("");
216
+ });
217
+
218
+ test("unknown service / missing file -> empty", async () => {
219
+ writeConfigFiles(sampleCredentialsYAML, sampleSettingsYAML);
220
+ expect(await getEndpointOverride("unknown-service")).toBe("");
221
+ process.env.HOME = newTmpDir(); // no settings file
222
+ expect(await getEndpointOverride("binara")).toBe("");
223
+ });
224
+ });
225
+
226
+ describe("loadDefaultConfig (browser stub)", () => {
227
+ test("passes explicit config through unchanged", async () => {
228
+ const input = {
229
+ deployport: { region: "eu-west-1", accessKeyID: "AK", secretAccessKey: "SK" },
230
+ };
231
+ expect(await browserLoadDefaultConfig(input)).toEqual(input);
232
+ });
233
+
234
+ test("does not fill any defaults (no env/file access)", async () => {
235
+ const c = await browserLoadDefaultConfig({ deployport: {} });
236
+ expect(c.deployport?.region).toBeUndefined();
237
+ expect(c.deployport?.accessKeyID).toBeUndefined();
238
+ });
239
+
240
+ test("handles undefined input", async () => {
241
+ expect(await browserLoadDefaultConfig()).toEqual({});
242
+ });
243
+ });
@@ -1,15 +1,15 @@
1
1
  import { Runtime } from "@deployport/specular-runtime";
2
+ import type { Config } from "./config.js";
2
3
  type Credentials = {
3
4
  keyID: string;
4
5
  secret: string;
5
6
  };
6
- export type Config = {
7
- deployport?: {
8
- region?: string;
9
- accessKeyID?: string;
10
- secretAccessKey?: string;
11
- };
12
- };
7
+ /**
8
+ * Replaces the `<region>` placeholder in the request URL and Host header with
9
+ * the given region. Mirrors Go's `ReplaceRegionInRequest`. Runs for every
10
+ * request that has a resolved region, whether or not the request is signed.
11
+ */
12
+ export declare function replaceRegionInRequest(request: Runtime.HTTPRequest, region: string): void;
13
13
  export declare function ConfigureSignatureV1(submission: Runtime.Submission, config: Config): Promise<void>;
14
14
  export declare const getSigningKeyReal: (credentials: Credentials, shortDate: string, region: string, service: string, vendor: string) => Promise<Uint8Array>;
15
15
  /**
@@ -1,40 +1,46 @@
1
1
  import { Sha256 } from '@aws-crypto/sha256-js';
2
- import { ServiceSignatureV1 } from '../specular.js';
2
+ import { ServiceSignatureV1, SignedOperationV1 } from '../specular.js';
3
3
  const VendorCode = 'dpp';
4
+ /**
5
+ * Replaces the `<region>` placeholder in the request URL and Host header with
6
+ * the given region. Mirrors Go's `ReplaceRegionInRequest`. Runs for every
7
+ * request that has a resolved region, whether or not the request is signed.
8
+ */
9
+ export function replaceRegionInRequest(request, region) {
10
+ if (request.url) {
11
+ request.url = request.url.replaceAll("<region>", region);
12
+ }
13
+ const host = request.headers["Host"];
14
+ if (typeof host === "string") {
15
+ request.headers["Host"] = host.replaceAll("<region>", region);
16
+ }
17
+ }
4
18
  export async function ConfigureSignatureV1(submission, config) {
5
- // const { request } = submission;
6
- // const { headers } = request;
7
- // if (!headers["x-specular-signature"]) {
8
- // headers["x-specular-signature"] = "v1";
9
- // }
10
- console.log("Hello from ConfigureSignatureV1 in runtime/signer.ts!", submission);
19
+ // sign only operations that opted in via SignedOperationV1
20
+ const signedOperationV1 = submission.operation.annotations.find((a) => a instanceof SignedOperationV1);
21
+ if (!signedOperationV1) {
22
+ return;
23
+ }
11
24
  const annotations = submission.operation.resource.package.annotations;
12
- console.log("service annotations", annotations);
13
25
  const serviceSignatureV1 = annotations.find((a) => a instanceof ServiceSignatureV1);
14
26
  if (!serviceSignatureV1) {
15
27
  return;
16
28
  }
17
- console.log("signing using config", config);
18
29
  const { deployport } = config;
19
30
  if (!deployport) {
20
- console.warn("missing required deployport configuration for signing", config);
21
31
  return;
22
32
  }
23
33
  const { region, accessKeyID: keyID, secretAccessKey: secret } = deployport;
24
34
  if (!region || !keyID || !secret) {
25
- console.warn("missing required configuration for signing", deployport);
35
+ // Incomplete credentials/region: leave the request unsigned.
26
36
  return;
27
37
  }
28
38
  const { ServiceName: signingService } = serviceSignatureV1;
29
- console.log("annotations", annotations, VendorCode, signingService);
30
39
  const { longDate, shortDate } = formatDate(new Date());
31
40
  const signingScope = buildSigningScope(VendorCode, region, signingService, shortDate);
32
- console.log("signingScope", signingScope);
33
41
  const credentialPart = buildCredentialPart(keyID, signingScope);
34
- console.log("credentialPart", credentialPart);
35
42
  const authHeaderPrefix = vendorAlgorithm(VendorCode) + " " + credentialPart;
36
43
  const bodyDigest = toHex(await hashSHA256(submission.request.body));
37
- console.log("bodyDigest", bodyDigest);
38
44
  const headers = submission.request.headers;
39
45
  headers[vendorDateHeaderKey(VendorCode)] = longDate;
40
46
  headers[vendorHeaderCanonicalNameKey(VendorCode, "Service")] = signingService;
@@ -42,7 +48,6 @@ export async function ConfigureSignatureV1(submission, config) {
42
48
  headers[vendorHeaderCanonicalNameKey(VendorCode, "Operation")] = submission.operation.name;
43
49
  headers[vendorRegionHeaderKey(VendorCode)] = region;
44
50
  headers[vendorHeaderCanonicalNameKey(VendorCode, "Content-Sha256")] = bodyDigest;
45
- console.log("headers", headers);
46
51
  const canonicalHeaders = getCanonicalHeaders(VendorCode, headers);
47
52
  const credentials = {
48
53
  keyID,
@@ -53,7 +58,6 @@ export async function ConfigureSignatureV1(submission, config) {
53
58
  // const payloadHash = await getPayloadHash(request.body);
54
59
  const signature = await getSignature(VendorCode, longDate, signingScope, signingKey, createCanonicalRequest(request, canonicalHeaders, bodyDigest));
55
60
  headers["Authorization"] = [authHeaderPrefix, `SignedHeaders=${canonicalHeaders.signedHeaders}`, `Signature=${signature}`];
56
- console.log("signed headers", headers);
57
61
  }
58
62
  function createCanonicalRequest(request, canonicalHeaders, payloadHash) {
59
63
  const sortedHeaders = canonicalHeaders.names;
@@ -69,7 +73,6 @@ function getCanonicalQuery({ path }) {
69
73
  return ""; // TODO: hardcoded, we don't use query strings
70
74
  }
71
75
  function getCanonicalPath({ path }) {
72
- console.log("getCanonicalPath path", path);
73
76
  return path;
74
77
  // if (this.uriEscapePath) {
75
78
  // Non-S3 services, we normalize the path and then double URI encode it.
@@ -95,7 +98,6 @@ function getCanonicalPath({ path }) {
95
98
  // For S3, we shouldn't normalize the path. For example, object name
96
99
  // my-object//example//photo.user should not be normalized to
97
100
  // my-object/example/photo.user
98
- console.log("getCanonicalPath path (curated)", path);
99
101
  return path;
100
102
  }
101
103
  // /**
@@ -111,13 +113,11 @@ function getCanonicalPath({ path }) {
111
113
  // };
112
114
  async function getSignature(vendor, longDate, credentialScope, keyPromise, canonicalRequest) {
113
115
  const stringToSign = await createStringToSign(vendor, longDate, credentialScope, canonicalRequest);
114
- console.log("stringToSign", stringToSign);
115
116
  const hash = new Sha256(await keyPromise);
116
117
  hash.update(stringToSign);
117
118
  return toHex(await hash.digest());
118
119
  }
119
120
  async function createStringToSign(vendor, longDate, credentialScope, canonicalRequest) {
120
- console.log('canonicalRequest', canonicalRequest);
121
121
  const hash = new Sha256();
122
122
  hash.update(canonicalRequest);
123
123
  const hashedRequest = await hash.digest();
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { Runtime } from "@deployport/specular-runtime";
3
+ import { replaceRegionInRequest } from "./signer.js";
4
+
5
+ function req(url: string, headers: Record<string, string | string[]> = {}): Runtime.HTTPRequest {
6
+ return { url, headers } as unknown as Runtime.HTTPRequest;
7
+ }
8
+
9
+ describe("replaceRegionInRequest", () => {
10
+ test("substitutes <region> in the URL", () => {
11
+ const r = req("https://iam.<region>.api.deployport.io/api/User/List");
12
+ replaceRegionInRequest(r, "us-east-2");
13
+ expect(r.url).toBe("https://iam.us-east-2.api.deployport.io/api/User/List");
14
+ });
15
+
16
+ test("substitutes <region> in the Host header when present", () => {
17
+ const r = req("https://iam.<region>.api.deployport.io/api", {
18
+ Host: "iam.<region>.api.deployport.io",
19
+ });
20
+ replaceRegionInRequest(r, "eu-west-1");
21
+ expect(r.url).toBe("https://iam.eu-west-1.api.deployport.io/api");
22
+ expect(r.headers["Host"]).toBe("iam.eu-west-1.api.deployport.io");
23
+ });
24
+
25
+ test("leaves URLs without the placeholder unchanged", () => {
26
+ const r = req("https://iam.us-east-2.api.deployport.io/api");
27
+ replaceRegionInRequest(r, "us-east-2");
28
+ expect(r.url).toBe("https://iam.us-east-2.api.deployport.io/api");
29
+ });
30
+ });