|
| 1 | +/** |
| 2 | + * MIT No Attribution |
| 3 | + * |
| 4 | + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 5 | + * |
| 6 | + * Permission is hereby granted, free of charge, to any person obtaining a copy of |
| 7 | + * the Software without restriction, including without limitation the rights to |
| 8 | + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
| 9 | + * the Software, and to permit persons to whom the Software is furnished to do so. |
| 10 | + * |
| 11 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 12 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 13 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 14 | + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 15 | + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 16 | + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
| 17 | + * SOFTWARE. |
| 18 | + */ |
| 19 | + |
| 20 | +/** |
| 21 | + * Outbound AWS SDK User-Agent solution tracking (#319) — CLI surface. |
| 22 | + * |
| 23 | + * Every AWS API call made by `bgagent` carries: |
| 24 | + * |
| 25 | + * app/uksb-wt64nei4u6/{STACKNAME} (only when config has stack_name) |
| 26 | + * md/uksb-wt64nei4u6#cli[#{PID}] |
| 27 | + * |
| 28 | + * CLI-local mirror of `cdk/src/handlers/shared/ua.ts` (the CLI package |
| 29 | + * cannot import from the CDK package — same mirroring convention as |
| 30 | + * `cli/src/types.ts`). Solution id, wire format, and sanitization rules |
| 31 | + * must stay identical; `agent/src/ua.py` is the Python counterpart. |
| 32 | + * |
| 33 | + * The component label is hardcoded (`cli`); the stack name comes from the |
| 34 | + * optional `stack_name` field in `~/.bgagent/config.json`. The trace handle |
| 35 | + * is the CLI process pid, set once at startup in `bin/bgagent.ts` and |
| 36 | + * appended per-request by the {@link withAbcaTrace} middleware. |
| 37 | + */ |
| 38 | + |
| 39 | +import { tryLoadConfig } from './config'; |
| 40 | + |
| 41 | +/** |
| 42 | + * AWS solution-tracking id for ABCA. Deploy-time counterpart (#292) lives in |
| 43 | + * the CloudFormation stack description in `cdk/src/main.ts`. |
| 44 | + */ |
| 45 | +export const SOLUTION_ID = 'uksb-wt64nei4u6'; |
| 46 | + |
| 47 | +/** Stable per-component label: this surface IS the bgagent CLI. */ |
| 48 | +const COMPONENT = 'cli'; |
| 49 | + |
| 50 | +/** App-id budget: 50-char value cap minus `uksb-wt64nei4u6/` (16) = 34. */ |
| 51 | +const STACK_NAME_MAX = 34; |
| 52 | + |
| 53 | +/** |
| 54 | + * RFC 7230 token charset; `/` and `#` deliberately excluded (structural |
| 55 | + * separators of the scheme). Mirrors the CDK and Python implementations. |
| 56 | + */ |
| 57 | +const UA_TOKEN_SAFE = /[^A-Za-z0-9!$%&'*+\-.^_`|~]/g; |
| 58 | + |
| 59 | +let currentTrace: string | undefined; |
| 60 | + |
| 61 | +/** Replace every non-UA-token char (incl. non-ASCII) with `-`. */ |
| 62 | +export function sanitizeUaValue(raw: string): string { |
| 63 | + return raw.replace(UA_TOKEN_SAFE, '-'); |
| 64 | +} |
| 65 | + |
| 66 | +/** |
| 67 | + * Client config fragment carrying the static ABCA UA segments. Spread into |
| 68 | + * every SDK client constructor: `new SecretsManagerClient({ region, ...abcaUserAgent() })`. |
| 69 | + * |
| 70 | + * Pair semantics (mirrors the CDK module): the `app/` segment is a |
| 71 | + * single-element pair so its literal `/` separators survive the SDK's |
| 72 | + * name-position escaping; the `md/` pair lets the SDK's own `name#value` |
| 73 | + * join produce the `#`. |
| 74 | + */ |
| 75 | +export function abcaUserAgent(): { customUserAgent: ([string] | [string, string])[] } { |
| 76 | + const pairs: ([string] | [string, string])[] = []; |
| 77 | + const stackName = tryLoadConfig()?.stack_name?.trim(); |
| 78 | + if (stackName) { |
| 79 | + // Sanitize FIRST, then clip, so a replaced char can't be re-split. |
| 80 | + const clipped = sanitizeUaValue(stackName).slice(0, STACK_NAME_MAX); |
| 81 | + pairs.push([`app/${SOLUTION_ID}/${clipped}`]); |
| 82 | + } |
| 83 | + pairs.push([`md/${SOLUTION_ID}`, COMPONENT]); |
| 84 | + return { customUserAgent: pairs }; |
| 85 | +} |
| 86 | + |
| 87 | +/** Set (or clear) the ambient trace handle (the CLI pid, set at startup). */ |
| 88 | +export function setAbcaTrace(handle?: string): void { |
| 89 | + currentTrace = handle || undefined; |
| 90 | +} |
| 91 | + |
| 92 | +/** Current trace handle, sanitized to UA-token-safe ASCII, or undefined. */ |
| 93 | +export function getAbcaTrace(): string | undefined { |
| 94 | + return currentTrace ? sanitizeUaValue(currentTrace) : undefined; |
| 95 | +} |
| 96 | + |
| 97 | +/** Structural view of a client middleware stack (avoids @smithy/types dep). */ |
| 98 | +interface MiddlewareStackLike { |
| 99 | + addRelativeTo(middleware: unknown, options: Record<string, unknown>): void; |
| 100 | +} |
| 101 | + |
| 102 | +/** |
| 103 | + * Append `#{TRACE}` to the outgoing User-Agent headers on every request by |
| 104 | + * splicing onto the static `md/` segment, after the SDK's own |
| 105 | + * `getUserAgentMiddleware` has rendered the headers. Mutates only the |
| 106 | + * header strings; the client and its connection pool are untouched. |
| 107 | + * No-ops on clients without a middleware stack (jest constructor mocks). |
| 108 | + */ |
| 109 | +export function withAbcaTrace<T>(client: T): T { |
| 110 | + const stack = (client as { middlewareStack?: MiddlewareStackLike }).middlewareStack; |
| 111 | + if (!stack || typeof stack.addRelativeTo !== 'function') { |
| 112 | + return client; |
| 113 | + } |
| 114 | + const md = `md/${SOLUTION_ID}#${COMPONENT}`; |
| 115 | + stack.addRelativeTo( |
| 116 | + (next: (args: unknown) => Promise<unknown>) => async (args: unknown) => { |
| 117 | + const trace = getAbcaTrace(); |
| 118 | + const request = (args as { request?: { headers?: Record<string, string> } }).request; |
| 119 | + if (trace && request?.headers) { |
| 120 | + for (const header of ['user-agent', 'x-amz-user-agent']) { |
| 121 | + const value = request.headers[header]; |
| 122 | + if (value && value.includes(md)) { |
| 123 | + request.headers[header] = value.replace(md, `${md}#${trace}`); |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + return next(args); |
| 128 | + }, |
| 129 | + { |
| 130 | + name: 'abcaUaTraceMiddleware', |
| 131 | + relation: 'after', |
| 132 | + toMiddleware: 'getUserAgentMiddleware', |
| 133 | + override: true, |
| 134 | + }, |
| 135 | + ); |
| 136 | + return client; |
| 137 | +} |
0 commit comments