@bigfootai/sdk 1.17.0 → 1.19.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.
- package/dist/auth-state.d.ts +38 -0
- package/dist/auth-state.d.ts.map +1 -0
- package/dist/auth-state.js +84 -0
- package/dist/auth-state.js.map +1 -0
- package/dist/auth-state.test.d.ts +2 -0
- package/dist/auth-state.test.d.ts.map +1 -0
- package/dist/auth-state.test.js +64 -0
- package/dist/auth-state.test.js.map +1 -0
- package/dist/configuration.d.ts +114 -0
- package/dist/configuration.d.ts.map +1 -0
- package/dist/configuration.js +210 -0
- package/dist/configuration.js.map +1 -0
- package/dist/configuration.test.d.ts +2 -0
- package/dist/configuration.test.d.ts.map +1 -0
- package/dist/configuration.test.js +187 -0
- package/dist/configuration.test.js.map +1 -0
- package/dist/index.d.ts +7 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -56
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +57 -0
- package/dist/logger.js.map +1 -0
- package/dist/secrets.d.ts +21 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +101 -0
- package/dist/secrets.js.map +1 -0
- package/dist/secrets.test.d.ts +2 -0
- package/dist/secrets.test.d.ts.map +1 -0
- package/dist/secrets.test.js +107 -0
- package/dist/secrets.test.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface AuthStatePayload {
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
}
|
|
4
|
+
export interface EncodeAuthStateOptions {
|
|
5
|
+
/** Connection id from the platform; becomes `state.split('.')[0]`. */
|
|
6
|
+
connectionId: string;
|
|
7
|
+
/** Application from the platform; becomes `state.split('.')[1]`. */
|
|
8
|
+
application: string;
|
|
9
|
+
/** Values to seal, e.g. `{ verifier }`. */
|
|
10
|
+
payload: AuthStatePayload;
|
|
11
|
+
/**
|
|
12
|
+
* Secret to derive the key from. Pass `process.env.BIGFOOT_API_KEY` — the var
|
|
13
|
+
* the provider already holds and that both directions of the platform channel
|
|
14
|
+
* use.
|
|
15
|
+
*/
|
|
16
|
+
secret: string;
|
|
17
|
+
/** How long the sealed state stays valid. Defaults to ten minutes. */
|
|
18
|
+
ttlSeconds?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface DecodedAuthState {
|
|
21
|
+
connectionId: string;
|
|
22
|
+
application: string;
|
|
23
|
+
payload: AuthStatePayload;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Seals a payload into a state value the platform's `/auth/callback` can hand
|
|
27
|
+
* back. Returns `${connectionId}.${application}.${token}`, where the token is
|
|
28
|
+
* age- and tamper-evident.
|
|
29
|
+
*/
|
|
30
|
+
export declare const encodeAuthState: ({ connectionId, application, payload, secret, ttlSeconds, }: EncodeAuthStateOptions) => Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Opens a state value produced by {@link encodeAuthState}. Returns `undefined`
|
|
33
|
+
* — never throws — when the value is malformed, was sealed with a different
|
|
34
|
+
* key, has expired, or its plain parts no longer match the sealed ones, so a
|
|
35
|
+
* caller can fail the connection closed with one branch.
|
|
36
|
+
*/
|
|
37
|
+
export declare const decodeAuthState: (state: string, secret: string) => Promise<DecodedAuthState | undefined>;
|
|
38
|
+
//# sourceMappingURL=auth-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-state.d.ts","sourceRoot":"","sources":["../src/auth-state.ts"],"names":[],"mappings":"AAwCA,MAAM,WAAW,gBAAgB;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACtC,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,OAAO,EAAE,gBAAgB,CAAC;IAC1B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,gBAAgB,CAAC;CAC1B;AAWD;;;;GAIG;AACH,eAAO,MAAM,eAAe,gEAMzB,sBAAsB,KAAG,OAAO,CAAC,MAAM,CAUzC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,UAAiB,MAAM,UAAU,MAAM,KAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAqBzG,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { hkdfSync } from 'node:crypto';
|
|
2
|
+
import { EncryptJWT, jwtDecrypt } from 'jose';
|
|
3
|
+
import { BIGFOOT_API_KEY_VAR } from './platform-auth.js';
|
|
4
|
+
/**
|
|
5
|
+
* Seals the values an OAuth round trip needs into the `state` query parameter.
|
|
6
|
+
*
|
|
7
|
+
* A deployed provider may serve `/auth/connect` and `/auth/callback` from
|
|
8
|
+
* different instances, so nothing short-lived can live in process memory: a
|
|
9
|
+
* module-level constant is shared by every authorization request on that
|
|
10
|
+
* instance, and an in-process map is invisible to whichever instance the
|
|
11
|
+
* callback lands on. The SDK hands the provider's `auth.callback` the raw
|
|
12
|
+
* `state`, so the provider can carry what it needs inside that value and read
|
|
13
|
+
* it back with {@link decodeAuthState}.
|
|
14
|
+
*
|
|
15
|
+
* The SDK parses `state.split('.')`, taking `[0]` as the connection id and `[1]`
|
|
16
|
+
* as the application, so the sealed token is appended as a third part. A JWE
|
|
17
|
+
* compact serialization contains `.` of its own, so it is base64url-encoded to
|
|
18
|
+
* keep the state exactly three parts.
|
|
19
|
+
*
|
|
20
|
+
* Sealing is a `jose` JWE (`dir` + `A256GCM`), which gives a standard,
|
|
21
|
+
* reviewable token format, authenticated encryption, and `exp` validation
|
|
22
|
+
* instead of a hand-rolled one.
|
|
23
|
+
*
|
|
24
|
+
* The key is derived from the provider's existing platform key
|
|
25
|
+
* (`BIGFOOT_API_KEY`) instead of a new secret: it is already present in every
|
|
26
|
+
* deployment and stable across instances. HKDF with a distinct label keeps the
|
|
27
|
+
* derived key independent of the one that authenticates the platform channel,
|
|
28
|
+
* so a sealed state cannot be replayed as a bearer token or the reverse.
|
|
29
|
+
*
|
|
30
|
+
* Rotating `BIGFOOT_API_KEY` invalidates states already in flight. They are
|
|
31
|
+
* single-use and short-lived, so the worst case is a connection attempt the
|
|
32
|
+
* user starts again.
|
|
33
|
+
*/
|
|
34
|
+
/** Domain separation: a value derived for one purpose must never be reused for another. */
|
|
35
|
+
const KEY_INFO = 'bigfoot-auth-state/v1';
|
|
36
|
+
const DEFAULT_TTL_SECONDS = 600;
|
|
37
|
+
const KEY_LENGTH = 32;
|
|
38
|
+
const deriveKey = (secret) => {
|
|
39
|
+
if (secret.trim().length === 0) {
|
|
40
|
+
throw new Error(`Cannot seal the auth state: ${BIGFOOT_API_KEY_VAR} is not set. It is the provider's platform key and the source of the state key.`);
|
|
41
|
+
}
|
|
42
|
+
return new Uint8Array(hkdfSync('sha256', secret, '', KEY_INFO, KEY_LENGTH));
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Seals a payload into a state value the platform's `/auth/callback` can hand
|
|
46
|
+
* back. Returns `${connectionId}.${application}.${token}`, where the token is
|
|
47
|
+
* age- and tamper-evident.
|
|
48
|
+
*/
|
|
49
|
+
export const encodeAuthState = async ({ connectionId, application, payload, secret, ttlSeconds, }) => {
|
|
50
|
+
const key = deriveKey(secret);
|
|
51
|
+
const token = await new EncryptJWT({ connectionId, application, data: payload })
|
|
52
|
+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
|
|
53
|
+
.setIssuedAt()
|
|
54
|
+
.setExpirationTime(`${ttlSeconds ?? DEFAULT_TTL_SECONDS}s`)
|
|
55
|
+
.encrypt(key);
|
|
56
|
+
return `${connectionId}.${application}.${Buffer.from(token, 'utf8').toString('base64url')}`;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Opens a state value produced by {@link encodeAuthState}. Returns `undefined`
|
|
60
|
+
* — never throws — when the value is malformed, was sealed with a different
|
|
61
|
+
* key, has expired, or its plain parts no longer match the sealed ones, so a
|
|
62
|
+
* caller can fail the connection closed with one branch.
|
|
63
|
+
*/
|
|
64
|
+
export const decodeAuthState = async (state, secret) => {
|
|
65
|
+
const [connectionId, application, token] = state.split('.');
|
|
66
|
+
if (!connectionId || !application || !token) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const key = deriveKey(secret);
|
|
71
|
+
const { payload } = await jwtDecrypt(Buffer.from(token, 'base64url').toString('utf8'), key);
|
|
72
|
+
if (payload.connectionId !== connectionId || payload.application !== application) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (typeof payload.data !== 'object' || payload.data === null) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return { connectionId, application, payload: payload.data };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=auth-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-state.js","sourceRoot":"","sources":["../src/auth-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,2FAA2F;AAC3F,MAAM,QAAQ,GAAG,uBAAuB,CAAC;AACzC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAChC,MAAM,UAAU,GAAG,EAAE,CAAC;AA6BtB,MAAM,SAAS,GAAG,CAAC,MAAc,EAAc,EAAE;IAChD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CACd,+BAA+B,mBAAmB,iFAAiF,CACnI,CAAC;IACH,CAAC;IACD,OAAO,IAAI,UAAU,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;AAC7E,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,EACrC,YAAY,EACZ,WAAW,EACX,OAAO,EACP,MAAM,EACN,UAAU,GACc,EAAmB,EAAE;IAC7C,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAE9B,MAAM,KAAK,GAAG,MAAM,IAAI,UAAU,CAAC,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;SAC9E,kBAAkB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;SAClD,WAAW,EAAE;SACb,iBAAiB,CAAC,GAAG,UAAU,IAAI,mBAAmB,GAAG,CAAC;SAC1D,OAAO,CAAC,GAAG,CAAC,CAAC;IAEf,OAAO,GAAG,YAAY,IAAI,WAAW,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;AAC7F,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,KAAa,EAAE,MAAc,EAAyC,EAAE;IAC7G,MAAM,CAAC,YAAY,EAAE,WAAW,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5D,IAAI,CAAC,YAAY,IAAI,CAAC,WAAW,IAAI,CAAC,KAAK,EAAE,CAAC;QAC7C,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,CAAC;QAE5F,IAAI,OAAO,CAAC,YAAY,KAAK,YAAY,IAAI,OAAO,CAAC,WAAW,KAAK,WAAW,EAAE,CAAC;YAClF,OAAO,SAAS,CAAC;QAClB,CAAC;QACD,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YAC/D,OAAO,SAAS,CAAC;QAClB,CAAC;QAED,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,IAAwB,EAAE,CAAC;IACjF,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,SAAS,CAAC;IAClB,CAAC;AACF,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-state.test.d.ts","sourceRoot":"","sources":["../src/auth-state.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { decodeAuthState, encodeAuthState } from './auth-state.js';
|
|
3
|
+
const SECRET = 'platform-key-value';
|
|
4
|
+
const OTHER_SECRET = 'a-different-platform-key';
|
|
5
|
+
const VERIFIER = 'super-secret-code-verifier-value';
|
|
6
|
+
const encode = (overrides = {}) => encodeAuthState({
|
|
7
|
+
connectionId: 'conn-1',
|
|
8
|
+
application: 'app-1',
|
|
9
|
+
payload: { verifier: VERIFIER },
|
|
10
|
+
secret: SECRET,
|
|
11
|
+
...overrides,
|
|
12
|
+
});
|
|
13
|
+
describe('encodeAuthState', () => {
|
|
14
|
+
it('prefixes the state with the connection id and application the SDK parses', async () => {
|
|
15
|
+
const parts = (await encode()).split('.');
|
|
16
|
+
expect(parts[0]).toBe('conn-1');
|
|
17
|
+
expect(parts[1]).toBe('app-1');
|
|
18
|
+
expect(parts).toHaveLength(3);
|
|
19
|
+
});
|
|
20
|
+
it('keeps the sealed token free of the delimiter the SDK splits on', async () => {
|
|
21
|
+
expect((await encode()).split('.')[2]).not.toContain('.');
|
|
22
|
+
});
|
|
23
|
+
it('does not leave the payload in the clear', async () => {
|
|
24
|
+
expect(await encode()).not.toContain(VERIFIER);
|
|
25
|
+
});
|
|
26
|
+
it('produces a different token every time (fresh iv)', async () => {
|
|
27
|
+
expect(await encode()).not.toBe(await encode());
|
|
28
|
+
});
|
|
29
|
+
it('rejects an empty secret', async () => {
|
|
30
|
+
await expect(encode({ secret: '' })).rejects.toThrow(/BIGFOOT_API_KEY/);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('decodeAuthState', () => {
|
|
34
|
+
it('round-trips the payload', async () => {
|
|
35
|
+
await expect(decodeAuthState(await encode(), SECRET)).resolves.toEqual({
|
|
36
|
+
connectionId: 'conn-1',
|
|
37
|
+
application: 'app-1',
|
|
38
|
+
payload: { verifier: VERIFIER },
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('returns undefined for a state sealed with another secret', async () => {
|
|
42
|
+
await expect(decodeAuthState(await encode(), OTHER_SECRET)).resolves.toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
it('returns undefined when the sealed token is tampered with', async () => {
|
|
45
|
+
const [connectionId, application, token] = (await encode()).split('.');
|
|
46
|
+
const flipped = `${token.slice(0, -1)}${token.endsWith('A') ? 'B' : 'A'}`;
|
|
47
|
+
await expect(decodeAuthState(`${connectionId}.${application}.${flipped}`, SECRET)).resolves.toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
it('returns undefined when the plain parts are swapped for another connection', async () => {
|
|
50
|
+
const [, application, token] = (await encode()).split('.');
|
|
51
|
+
await expect(decodeAuthState(`conn-2.${application}.${token}`, SECRET)).resolves.toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
it('returns undefined once the state has expired', async () => {
|
|
54
|
+
await expect(decodeAuthState(await encode({ ttlSeconds: -1 }), SECRET)).resolves.toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
it('returns undefined for a state with no sealed part', async () => {
|
|
57
|
+
await expect(decodeAuthState('conn-1.app-1', SECRET)).resolves.toBeUndefined();
|
|
58
|
+
await expect(decodeAuthState('', SECRET)).resolves.toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
it('returns undefined for a token that is not a JWE', async () => {
|
|
61
|
+
await expect(decodeAuthState('conn-1.app-1.not-a-jwe', SECRET)).resolves.toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
//# sourceMappingURL=auth-state.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-state.test.js","sourceRoot":"","sources":["../src/auth-state.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type Logger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a provider's configuration before the HTTP server starts, whichever
|
|
4
|
+
* runtime the provider is running on.
|
|
5
|
+
*
|
|
6
|
+
* - **ECS** injects the values as environment variables through the task
|
|
7
|
+
* definition's `secrets` block. The task agent resolves the Secrets Manager
|
|
8
|
+
* reference at task start, so the value never lands in Terraform state or in
|
|
9
|
+
* the service definition. This module finds them already in `process.env`.
|
|
10
|
+
* - **Lambda** has no equivalent injection, so the image carries the AWS
|
|
11
|
+
* Parameters and Secrets Lambda Extension. It serves the secret as JSON over
|
|
12
|
+
* `http://localhost:2773` and caches it (five minutes by default). This module
|
|
13
|
+
* reads it from there and merges the keys into `process.env`.
|
|
14
|
+
*
|
|
15
|
+
* A provider therefore makes one call — `await loadConfiguration({ required })`
|
|
16
|
+
* — and does not know which runtime it is on. `SECRETS_MANAGER_SECRET_ID` names
|
|
17
|
+
* the secret to read on Lambda; it is not a secret value and may live in the
|
|
18
|
+
* function configuration.
|
|
19
|
+
*
|
|
20
|
+
* Missing values fail closed: if a required key is absent after resolution (or
|
|
21
|
+
* the secret cannot be read or parsed) startup throws, naming the key but never
|
|
22
|
+
* the value.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Names the Secrets Manager secret holding the provider's secret-shaped
|
|
26
|
+
* configuration, as a flat JSON object of string key/value pairs. Unset locally,
|
|
27
|
+
* and set on Lambda in the function configuration. On ECS the values are injected
|
|
28
|
+
* directly and this variable carries no meaning, so it may be left unset.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SECRET_ID_VAR = "SECRETS_MANAGER_SECRET_ID";
|
|
31
|
+
/** Set by the Lambda runtime. Its presence is how the SDK tells Lambda from ECS. */
|
|
32
|
+
export declare const LAMBDA_FUNCTION_NAME_VAR = "AWS_LAMBDA_FUNCTION_NAME";
|
|
33
|
+
/**
|
|
34
|
+
* The token the extension requires on every request. The Lambda runtime sets it
|
|
35
|
+
* in the execution environment; it authenticates the caller as this function and
|
|
36
|
+
* is never the secret value itself.
|
|
37
|
+
*/
|
|
38
|
+
export declare const EXTENSION_TOKEN_VAR = "AWS_SESSION_TOKEN";
|
|
39
|
+
/** Port of the extension's local HTTP server. Defaults in the extension to 2773. */
|
|
40
|
+
export declare const EXTENSION_PORT_VAR = "PARAMETERS_SECRETS_EXTENSION_HTTP_PORT";
|
|
41
|
+
export declare const DEFAULT_EXTENSION_PORT = 2773;
|
|
42
|
+
/**
|
|
43
|
+
* A configuration requirement. A bare string names one required key; an array
|
|
44
|
+
* names alternatives, and the requirement is met when any one of them is
|
|
45
|
+
* present (`['AIRTABLE_CLIENT_SECRET', 'AIRTABLE_PKCE_SECRET']`).
|
|
46
|
+
*/
|
|
47
|
+
export type ConfigurationRequirement = string | readonly string[];
|
|
48
|
+
export interface LoadConfigurationOptions {
|
|
49
|
+
/**
|
|
50
|
+
* The keys the provider cannot run without. Anything absent after resolution
|
|
51
|
+
* aborts startup. Each entry is one key, or a set of alternatives.
|
|
52
|
+
*/
|
|
53
|
+
required?: readonly ConfigurationRequirement[];
|
|
54
|
+
/**
|
|
55
|
+
* The secret to read on Lambda. Defaults to `SECRETS_MANAGER_SECRET_ID`.
|
|
56
|
+
* Exposed for tests and for a provider that reads more than one secret.
|
|
57
|
+
*/
|
|
58
|
+
secretId?: string;
|
|
59
|
+
/** Overrides the default logger. */
|
|
60
|
+
logger?: Logger;
|
|
61
|
+
/** The number of times to try the extension before giving up. Defaults to 5. */
|
|
62
|
+
attempts?: number;
|
|
63
|
+
/** Delay between extension attempts, in milliseconds. Defaults to 200. */
|
|
64
|
+
retryDelayMs?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Reads the raw `SecretString` for a secret id from the extension. Injectable
|
|
67
|
+
* so the tests exercise the resolution logic with no network and no Lambda.
|
|
68
|
+
*/
|
|
69
|
+
fetchSecret?: (secretId: string) => Promise<string>;
|
|
70
|
+
}
|
|
71
|
+
/** Reads the raw `SecretString` for a secret id from somewhere. */
|
|
72
|
+
export type SecretFetcher = (secretId: string) => Promise<string>;
|
|
73
|
+
/**
|
|
74
|
+
* Reads a secret from the extension, retrying the retryable failures. The
|
|
75
|
+
* extension only reports ready after the runtime bootstraps, so the first read
|
|
76
|
+
* after a cold start can race it.
|
|
77
|
+
*/
|
|
78
|
+
export declare const readSecretFromExtension: (secretId: string, { attempts, retryDelayMs }?: {
|
|
79
|
+
attempts?: number;
|
|
80
|
+
retryDelayMs?: number;
|
|
81
|
+
}) => Promise<string>;
|
|
82
|
+
/** Reads the raw `SecretString` for a secret id from the extension. */
|
|
83
|
+
export declare const fetchSecretFromExtension: SecretFetcher;
|
|
84
|
+
/**
|
|
85
|
+
* Parses the secret payload into key/value pairs, rejecting anything that is not
|
|
86
|
+
* a flat JSON object of strings. Error messages describe the shape problem only —
|
|
87
|
+
* they never quote the payload, which would put secret material into logs and
|
|
88
|
+
* stack traces.
|
|
89
|
+
*/
|
|
90
|
+
export declare const parseSecretPayload: (raw: string) => Record<string, string>;
|
|
91
|
+
/**
|
|
92
|
+
* Merges resolved values into the environment. Secrets Manager is authoritative:
|
|
93
|
+
* a stale value left behind in the task definition must never win over the
|
|
94
|
+
* managed one, so this overwrites rather than filling in gaps.
|
|
95
|
+
*/
|
|
96
|
+
export declare const applyConfiguration: (values: Record<string, string>, env?: NodeJS.ProcessEnv) => void;
|
|
97
|
+
/**
|
|
98
|
+
* The requirements still unmet, each named by its alternatives ("A or B").
|
|
99
|
+
* Names only — never values.
|
|
100
|
+
*/
|
|
101
|
+
export declare const findMissingConfiguration: (required: readonly ConfigurationRequirement[], env?: NodeJS.ProcessEnv) => string[];
|
|
102
|
+
/** True when the process is running as a Lambda function. */
|
|
103
|
+
export declare const isLambda: (env?: NodeJS.ProcessEnv) => boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Resolves the provider's configuration and fails closed when it is incomplete.
|
|
106
|
+
*
|
|
107
|
+
* On Lambda the secret named by `SECRETS_MANAGER_SECRET_ID` is read through the
|
|
108
|
+
* extension and merged into `process.env`, so every provider continues to read
|
|
109
|
+
* its settings from the environment. On ECS the values are already injected
|
|
110
|
+
* there. Either way a missing required key throws, naming the key but never the
|
|
111
|
+
* value.
|
|
112
|
+
*/
|
|
113
|
+
export declare const loadConfiguration: (options?: LoadConfigurationOptions) => Promise<void>;
|
|
114
|
+
//# sourceMappingURL=configuration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["../src/configuration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAuB,MAAM,aAAa,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,aAAa,8BAA8B,CAAC;AAEzD,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,sBAAsB,CAAC;AAEvD,oFAAoF;AACpF,eAAO,MAAM,kBAAkB,2CAA2C,CAAC;AAE3E,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC;AAElE,MAAM,WAAW,wBAAwB;IACxC;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,wBAAwB,EAAE,CAAC;IAC/C;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oCAAoC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD;AASD,mEAAmE;AACnE,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAiDlE;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,aACzB,MAAM,+BACsB;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,KAChF,OAAO,CAAC,MAAM,CAyBhB,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,wBAAwB,EAAE,aAA+D,CAAC;AAEvG;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAqBrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,WAAY,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAAO,MAAM,CAAC,UAAU,KAAiB,IAIzG,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,aAC1B,SAAS,wBAAwB,EAAE,QACxC,MAAM,CAAC,UAAU,KACpB,MAAM,EAOR,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,QAAQ,SAAS,MAAM,CAAC,UAAU,KAAiB,OACR,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,aAAmB,wBAAwB,KAAQ,OAAO,CAAC,IAAI,CAwC5F,CAAC"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { logger as sdkLogger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a provider's configuration before the HTTP server starts, whichever
|
|
4
|
+
* runtime the provider is running on.
|
|
5
|
+
*
|
|
6
|
+
* - **ECS** injects the values as environment variables through the task
|
|
7
|
+
* definition's `secrets` block. The task agent resolves the Secrets Manager
|
|
8
|
+
* reference at task start, so the value never lands in Terraform state or in
|
|
9
|
+
* the service definition. This module finds them already in `process.env`.
|
|
10
|
+
* - **Lambda** has no equivalent injection, so the image carries the AWS
|
|
11
|
+
* Parameters and Secrets Lambda Extension. It serves the secret as JSON over
|
|
12
|
+
* `http://localhost:2773` and caches it (five minutes by default). This module
|
|
13
|
+
* reads it from there and merges the keys into `process.env`.
|
|
14
|
+
*
|
|
15
|
+
* A provider therefore makes one call — `await loadConfiguration({ required })`
|
|
16
|
+
* — and does not know which runtime it is on. `SECRETS_MANAGER_SECRET_ID` names
|
|
17
|
+
* the secret to read on Lambda; it is not a secret value and may live in the
|
|
18
|
+
* function configuration.
|
|
19
|
+
*
|
|
20
|
+
* Missing values fail closed: if a required key is absent after resolution (or
|
|
21
|
+
* the secret cannot be read or parsed) startup throws, naming the key but never
|
|
22
|
+
* the value.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Names the Secrets Manager secret holding the provider's secret-shaped
|
|
26
|
+
* configuration, as a flat JSON object of string key/value pairs. Unset locally,
|
|
27
|
+
* and set on Lambda in the function configuration. On ECS the values are injected
|
|
28
|
+
* directly and this variable carries no meaning, so it may be left unset.
|
|
29
|
+
*/
|
|
30
|
+
export const SECRET_ID_VAR = 'SECRETS_MANAGER_SECRET_ID';
|
|
31
|
+
/** Set by the Lambda runtime. Its presence is how the SDK tells Lambda from ECS. */
|
|
32
|
+
export const LAMBDA_FUNCTION_NAME_VAR = 'AWS_LAMBDA_FUNCTION_NAME';
|
|
33
|
+
/**
|
|
34
|
+
* The token the extension requires on every request. The Lambda runtime sets it
|
|
35
|
+
* in the execution environment; it authenticates the caller as this function and
|
|
36
|
+
* is never the secret value itself.
|
|
37
|
+
*/
|
|
38
|
+
export const EXTENSION_TOKEN_VAR = 'AWS_SESSION_TOKEN';
|
|
39
|
+
/** Port of the extension's local HTTP server. Defaults in the extension to 2773. */
|
|
40
|
+
export const EXTENSION_PORT_VAR = 'PARAMETERS_SECRETS_EXTENSION_HTTP_PORT';
|
|
41
|
+
export const DEFAULT_EXTENSION_PORT = 2773;
|
|
42
|
+
/**
|
|
43
|
+
* Raised when the extension is reachable but not yet serving — the connection
|
|
44
|
+
* was refused, or it answered as unavailable. Retried; every other failure is
|
|
45
|
+
* terminal.
|
|
46
|
+
*/
|
|
47
|
+
class ExtensionUnavailableError extends Error {
|
|
48
|
+
}
|
|
49
|
+
const isRetryableStatus = (status) => status === 429 || status >= 500;
|
|
50
|
+
/**
|
|
51
|
+
* One round trip to the extension. Distinguishes "not ready yet" (retryable)
|
|
52
|
+
* from "this request will never succeed" (terminal), and never quotes the
|
|
53
|
+
* secret or the token in a message.
|
|
54
|
+
*/
|
|
55
|
+
const readSecretOnce = async (secretId, token, port) => {
|
|
56
|
+
const url = `http://localhost:${port}/secretsmanager/get?secretId=${encodeURIComponent(secretId)}`;
|
|
57
|
+
let response;
|
|
58
|
+
try {
|
|
59
|
+
response = await fetch(url, {
|
|
60
|
+
headers: { 'X-Aws-Parameters-Secrets-Token': token },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
65
|
+
throw new ExtensionUnavailableError(`the extension is not reachable yet: ${reason}`);
|
|
66
|
+
}
|
|
67
|
+
if (isRetryableStatus(response.status)) {
|
|
68
|
+
throw new ExtensionUnavailableError(`the extension answered HTTP ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`the extension rejected the request for secret "${secretId}" with HTTP ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
let body;
|
|
74
|
+
try {
|
|
75
|
+
body = await response.json();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error('the extension returned a response that is not JSON');
|
|
79
|
+
}
|
|
80
|
+
if (typeof body !== 'object' ||
|
|
81
|
+
body === null ||
|
|
82
|
+
!('SecretString' in body) ||
|
|
83
|
+
typeof body.SecretString !== 'string') {
|
|
84
|
+
throw new Error('the secret has no SecretString value (binary secrets are not supported)');
|
|
85
|
+
}
|
|
86
|
+
return body.SecretString;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Reads a secret from the extension, retrying the retryable failures. The
|
|
90
|
+
* extension only reports ready after the runtime bootstraps, so the first read
|
|
91
|
+
* after a cold start can race it.
|
|
92
|
+
*/
|
|
93
|
+
export const readSecretFromExtension = async (secretId, { attempts = 5, retryDelayMs = 200 } = {}) => {
|
|
94
|
+
const token = process.env[EXTENSION_TOKEN_VAR];
|
|
95
|
+
if (!token || token.trim().length === 0) {
|
|
96
|
+
throw new Error(`${EXTENSION_TOKEN_VAR} is not set, so the AWS Parameters and Secrets Lambda Extension cannot be authenticated. It is set by the Lambda runtime and must not be configured by hand.`);
|
|
97
|
+
}
|
|
98
|
+
const port = Number(process.env[EXTENSION_PORT_VAR]) || DEFAULT_EXTENSION_PORT;
|
|
99
|
+
let lastError;
|
|
100
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
101
|
+
try {
|
|
102
|
+
return await readSecretOnce(secretId, token, port);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!(error instanceof ExtensionUnavailableError) || attempt === attempts) {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
lastError = error;
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs * attempt));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Unreachable: the loop either returns or throws. Kept for the type checker.
|
|
113
|
+
throw lastError instanceof Error ? lastError : new Error('the extension could not be read');
|
|
114
|
+
};
|
|
115
|
+
/** Reads the raw `SecretString` for a secret id from the extension. */
|
|
116
|
+
export const fetchSecretFromExtension = (secretId) => readSecretFromExtension(secretId);
|
|
117
|
+
/**
|
|
118
|
+
* Parses the secret payload into key/value pairs, rejecting anything that is not
|
|
119
|
+
* a flat JSON object of strings. Error messages describe the shape problem only —
|
|
120
|
+
* they never quote the payload, which would put secret material into logs and
|
|
121
|
+
* stack traces.
|
|
122
|
+
*/
|
|
123
|
+
export const parseSecretPayload = (raw) => {
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(raw);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new Error('the secret is not valid JSON (expected a flat object, e.g. {"DATABASE_URL": "..."})');
|
|
130
|
+
}
|
|
131
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
132
|
+
throw new Error('the secret must be a flat JSON object of string key/value pairs');
|
|
133
|
+
}
|
|
134
|
+
const values = {};
|
|
135
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
136
|
+
if (typeof value !== 'string') {
|
|
137
|
+
throw new Error(`the secret value for "${key}" must be a string`);
|
|
138
|
+
}
|
|
139
|
+
values[key] = value;
|
|
140
|
+
}
|
|
141
|
+
return values;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Merges resolved values into the environment. Secrets Manager is authoritative:
|
|
145
|
+
* a stale value left behind in the task definition must never win over the
|
|
146
|
+
* managed one, so this overwrites rather than filling in gaps.
|
|
147
|
+
*/
|
|
148
|
+
export const applyConfiguration = (values, env = process.env) => {
|
|
149
|
+
for (const [key, value] of Object.entries(values)) {
|
|
150
|
+
env[key] = value;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const isPresent = (key, env) => (env[key] ?? '').trim().length > 0;
|
|
154
|
+
/**
|
|
155
|
+
* The requirements still unmet, each named by its alternatives ("A or B").
|
|
156
|
+
* Names only — never values.
|
|
157
|
+
*/
|
|
158
|
+
export const findMissingConfiguration = (required, env = process.env) => {
|
|
159
|
+
const alternativesOf = (requirement) => typeof requirement === 'string' ? [requirement] : requirement;
|
|
160
|
+
return required
|
|
161
|
+
.filter((requirement) => !alternativesOf(requirement).some((key) => isPresent(key, env)))
|
|
162
|
+
.map((requirement) => alternativesOf(requirement).join(' or '));
|
|
163
|
+
};
|
|
164
|
+
/** True when the process is running as a Lambda function. */
|
|
165
|
+
export const isLambda = (env = process.env) => (env[LAMBDA_FUNCTION_NAME_VAR] ?? '').trim().length > 0;
|
|
166
|
+
/**
|
|
167
|
+
* Resolves the provider's configuration and fails closed when it is incomplete.
|
|
168
|
+
*
|
|
169
|
+
* On Lambda the secret named by `SECRETS_MANAGER_SECRET_ID` is read through the
|
|
170
|
+
* extension and merged into `process.env`, so every provider continues to read
|
|
171
|
+
* its settings from the environment. On ECS the values are already injected
|
|
172
|
+
* there. Either way a missing required key throws, naming the key but never the
|
|
173
|
+
* value.
|
|
174
|
+
*/
|
|
175
|
+
export const loadConfiguration = async (options = {}) => {
|
|
176
|
+
const log = options.logger ?? sdkLogger;
|
|
177
|
+
const secretId = (options.secretId ?? process.env[SECRET_ID_VAR])?.trim();
|
|
178
|
+
if (secretId && isLambda()) {
|
|
179
|
+
log.info({ secretId }, 'Reading provider configuration from the AWS Parameters and Secrets Lambda Extension');
|
|
180
|
+
let raw;
|
|
181
|
+
try {
|
|
182
|
+
raw = await (options.fetchSecret
|
|
183
|
+
? options.fetchSecret(secretId)
|
|
184
|
+
: readSecretFromExtension(secretId, {
|
|
185
|
+
attempts: options.attempts,
|
|
186
|
+
retryDelayMs: options.retryDelayMs,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
191
|
+
throw new Error(`Unable to read configuration from secret "${secretId}": ${reason}`);
|
|
192
|
+
}
|
|
193
|
+
let values;
|
|
194
|
+
try {
|
|
195
|
+
values = parseSecretPayload(raw);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
199
|
+
throw new Error(`The secret "${secretId}" could not be used: ${reason}`);
|
|
200
|
+
}
|
|
201
|
+
applyConfiguration(values);
|
|
202
|
+
// Key names only — never values.
|
|
203
|
+
log.info({ keys: Object.keys(values).sort(), secretId }, 'Loaded provider configuration');
|
|
204
|
+
}
|
|
205
|
+
const missing = findMissingConfiguration(options.required ?? []);
|
|
206
|
+
if (missing.length > 0) {
|
|
207
|
+
throw new Error(`The provider configuration is incomplete; missing: ${missing.join(', ')}. Supply these values in the environment, or on Lambda via the secret named by ${SECRET_ID_VAR}.`);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
//# sourceMappingURL=configuration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configuration.js","sourceRoot":"","sources":["../src/configuration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,MAAM,IAAI,SAAS,EAAE,MAAM,aAAa,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAEzD,oFAAoF;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,0BAA0B,CAAC;AAEnE;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,mBAAmB,CAAC;AAEvD,oFAAoF;AACpF,MAAM,CAAC,MAAM,kBAAkB,GAAG,wCAAwC,CAAC;AAE3E,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AAiC3C;;;;GAIG;AACH,MAAM,yBAA0B,SAAQ,KAAK;CAAG;AAKhD,MAAM,iBAAiB,GAAG,CAAC,MAAc,EAAW,EAAE,CAAC,MAAM,KAAK,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC;AAEvF;;;;GAIG;AACH,MAAM,cAAc,GAAG,KAAK,EAAE,QAAgB,EAAE,KAAa,EAAE,IAAY,EAAmB,EAAE;IAC/F,MAAM,GAAG,GAAG,oBAAoB,IAAI,gCAAgC,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAEnG,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACJ,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAC3B,OAAO,EAAE,EAAE,gCAAgC,EAAE,KAAK,EAAE;SACpD,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,yBAAyB,CAAC,uCAAuC,MAAM,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,IAAI,iBAAiB,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,yBAAyB,CAAC,+BAA+B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACvF,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,kDAAkD,QAAQ,eAAe,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC7G,CAAC;IAED,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACJ,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACvE,CAAC;IAED,IACC,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACb,CAAC,CAAC,cAAc,IAAI,IAAI,CAAC;QACzB,OAAQ,IAAmC,CAAC,YAAY,KAAK,QAAQ,EACpE,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,yEAAyE,CAAC,CAAC;IAC5F,CAAC;IAED,OAAQ,IAAiC,CAAC,YAAY,CAAC;AACxD,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,EAC3C,QAAgB,EAChB,EAAE,QAAQ,GAAG,CAAC,EAAE,YAAY,GAAG,GAAG,KAAmD,EAAE,EACrE,EAAE;IACpB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC/C,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CACd,GAAG,mBAAmB,8JAA8J,CACpL,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,sBAAsB,CAAC;IAE/E,IAAI,SAAkB,CAAC;IACvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QACzD,IAAI,CAAC;YACJ,OAAO,MAAM,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACpD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,CAAC,KAAK,YAAY,yBAAyB,CAAC,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC3E,MAAM,KAAK,CAAC;YACb,CAAC;YACD,SAAS,GAAG,KAAK,CAAC;YAClB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC;QAC7E,CAAC;IACF,CAAC;IAED,6EAA6E;IAC7E,MAAM,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;AAC7F,CAAC,CAAC;AAEF,uEAAuE;AACvE,MAAM,CAAC,MAAM,wBAAwB,GAAkB,CAAC,QAAQ,EAAE,EAAE,CAAC,uBAAuB,CAAC,QAAQ,CAAC,CAAC;AAEvG;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAA0B,EAAE;IACzE,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CAAC,qFAAqF,CAAC,CAAC;IACxG,CAAC;IAED,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5E,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IACpF,CAAC;IAED,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,oBAAoB,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACrB,CAAC;IAED,OAAO,MAAM,CAAC;AACf,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,MAA8B,EAAE,MAAyB,OAAO,CAAC,GAAG,EAAQ,EAAE;IAChH,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACnD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAClB,CAAC;AACF,CAAC,CAAC;AAEF,MAAM,SAAS,GAAG,CAAC,GAAW,EAAE,GAAsB,EAAW,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAEvG;;;GAGG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACvC,QAA6C,EAC7C,MAAyB,OAAO,CAAC,GAAG,EACzB,EAAE;IACb,MAAM,cAAc,GAAG,CAAC,WAAqC,EAAqB,EAAE,CACnF,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IAE/D,OAAO,QAAQ;SACb,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;SACxF,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;AAClE,CAAC,CAAC;AAEF,6DAA6D;AAC7D,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAW,EAAE,CACzE,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EAAE,UAAoC,EAAE,EAAiB,EAAE;IAChG,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,IAAI,SAAS,CAAC;IACxC,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAE1E,IAAI,QAAQ,IAAI,QAAQ,EAAE,EAAE,CAAC;QAC5B,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,EAAE,qFAAqF,CAAC,CAAC;QAE9G,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACJ,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,WAAW;gBAC/B,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC;gBAC/B,CAAC,CAAC,uBAAuB,CAAC,QAAQ,EAAE;oBAClC,QAAQ,EAAE,OAAO,CAAC,QAAQ;oBAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;iBAClC,CAAC,CAAC,CAAC;QACP,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,MAAM,IAAI,KAAK,CAAC,6CAA6C,QAAQ,MAAM,MAAM,EAAE,CAAC,CAAC;QACtF,CAAC;QAED,IAAI,MAA8B,CAAC;QACnC,IAAI,CAAC;YACJ,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,MAAM,IAAI,KAAK,CAAC,eAAe,QAAQ,wBAAwB,MAAM,EAAE,CAAC,CAAC;QAC1E,CAAC;QAED,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAE3B,iCAAiC;QACjC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,+BAA+B,CAAC,CAAC;IAC3F,CAAC;IAED,MAAM,OAAO,GAAG,wBAAwB,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IACjE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACd,sDAAsD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,kFAAkF,aAAa,GAAG,CAC1K,CAAC;IACH,CAAC;AACF,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configuration.test.d.ts","sourceRoot":"","sources":["../src/configuration.test.ts"],"names":[],"mappings":""}
|