@pipeline-builder/pipeline-events 4.0.10 → 4.0.12

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/lib/auth.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Trade the stored key for a short-lived platform token, cached per warm
3
+ * container until it is 80% through its life. Throws on any failure, which fails
4
+ * the batch so SQS retries it — an unauthenticated ship is never an option.
5
+ */
6
+ export declare function getAuthToken(): Promise<string>;
7
+ /**
8
+ * Drop everything the container holds about its credential after a 401/403 from
9
+ * the API: the token, and the KEY — the token-renew Lambda rotates the secret
10
+ * underneath us, so a warm container that only re-exchanged would keep
11
+ * presenting the retired key forever. An env-provided key cannot change without
12
+ * a new container, so it is kept.
13
+ */
14
+ export declare function invalidateCredential(): void;
package/lib/auth.js ADDED
@@ -0,0 +1,99 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Reporting-API authentication. The stored credential is an OPAQUE
5
+ * service-account key (`pb_sa_…`), traded at platform for a short-lived JWT.
6
+ * Nothing is verified here: an opaque key has no claims and no signature, and
7
+ * the exchange re-reads the account, its org and the key's own state every time,
8
+ * which is what makes a revocation take effect within one token lifetime.
9
+ *
10
+ * Contract: cache the token in the warm container, refresh EARLY (before
11
+ * expiry) so a batch never presents a token that dies mid-flight, and fail the
12
+ * batch on an exchange failure rather than shipping events unauthenticated.
13
+ */
14
+ import { createPlatformCredential } from '@pipeline-builder/pipeline-core/lib/handlers/platform-credential.js';
15
+ import { log } from './util.js';
16
+ /** Refresh at this fraction of the token's life — never at the last moment. */
17
+ const TOKEN_REFRESH_FRACTION = 0.8;
18
+ /** Floor on the cached lifetime, so a surprisingly short TTL can't cause a
19
+ * refresh storm (one exchange per batch at worst). */
20
+ const MIN_TOKEN_CACHE_MS = 30_000;
21
+ /** Per-request timeout for the exchange call. */
22
+ const EXCHANGE_TIMEOUT_MS = 5000;
23
+ /**
24
+ * The service-account key: `PLATFORM_ACCESS_KEY` if set, otherwise the
25
+ * `password` field of the Secrets Manager secret named by `PLATFORM_SECRET_NAME`.
26
+ * A stored JWT is refused loudly, naming the fix, instead of failing later as an
27
+ * opaque 401 from the reporting API.
28
+ */
29
+ const credential = createPlatformCredential({
30
+ secretName: () => process.env.PLATFORM_SECRET_NAME,
31
+ envKeyVar: 'PLATFORM_ACCESS_KEY',
32
+ });
33
+ let cachedToken = null;
34
+ let cachedTokenExpiresAt = 0;
35
+ /**
36
+ * Trade the stored key for a short-lived platform token, cached per warm
37
+ * container until it is 80% through its life. Throws on any failure, which fails
38
+ * the batch so SQS retries it — an unauthenticated ship is never an option.
39
+ */
40
+ export async function getAuthToken() {
41
+ if (cachedToken && Date.now() < cachedTokenExpiresAt)
42
+ return cachedToken;
43
+ const baseUrl = (process.env.PLATFORM_BASE_URL || '').replace(/\/+$/, '');
44
+ if (!baseUrl)
45
+ throw new Error('PLATFORM_BASE_URL environment variable is required');
46
+ const key = await credential.getKey();
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), EXCHANGE_TIMEOUT_MS);
49
+ let res;
50
+ try {
51
+ res = await fetch(`${baseUrl}/api/auth/token/exchange`, {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
54
+ body: JSON.stringify({ key }),
55
+ signal: controller.signal,
56
+ });
57
+ }
58
+ finally {
59
+ clearTimeout(timer);
60
+ }
61
+ if (!res.ok) {
62
+ // A rejected key is the one failure an operator has to act on, and platform
63
+ // deliberately answers every refusal the same way — so say what to check
64
+ // rather than echoing an undifferentiated 401.
65
+ if (res.status === 401 || res.status === 403) {
66
+ throw new Error(`Access-key exchange refused (${res.status}) — the stored key is unknown, revoked, expired, `
67
+ + 'its service account is disabled, or this address is outside the key’s IP allowlist');
68
+ }
69
+ throw new Error(`Access-key exchange failed: ${res.status}`);
70
+ }
71
+ const body = await res.json().catch(() => ({}));
72
+ const accessToken = body.data?.accessToken;
73
+ if (!accessToken)
74
+ throw new Error('Access-key exchange returned no access token');
75
+ const ttlMs = (typeof body.data?.expiresIn === 'number' && body.data.expiresIn > 0 ? body.data.expiresIn : 300) * 1000;
76
+ cachedToken = accessToken;
77
+ cachedTokenExpiresAt = Date.now() + Math.max(MIN_TOKEN_CACHE_MS, ttlMs * TOKEN_REFRESH_FRACTION);
78
+ log.info('Exchanged the stored service-account key for a platform token');
79
+ return accessToken;
80
+ }
81
+ /**
82
+ * Drop everything the container holds about its credential after a 401/403 from
83
+ * the API: the token, and the KEY — the token-renew Lambda rotates the secret
84
+ * underneath us, so a warm container that only re-exchanged would keep
85
+ * presenting the retired key forever. An env-provided key cannot change without
86
+ * a new container, so it is kept.
87
+ */
88
+ export function invalidateCredential() {
89
+ cachedToken = null;
90
+ cachedTokenExpiresAt = 0;
91
+ credential.invalidate();
92
+ }
93
+ /** @internal Test-only: forget every cached credential. */
94
+ export function _resetAuthForTests() {
95
+ cachedToken = null;
96
+ cachedTokenExpiresAt = 0;
97
+ credential.reset();
98
+ }
99
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXV0aC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9hdXRoLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEM7Ozs7Ozs7Ozs7R0FVRztBQUVILE9BQU8sRUFBRSx3QkFBd0IsRUFBRSxNQUFNLHFFQUFxRSxDQUFDO0FBQy9HLE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFFaEMsK0VBQStFO0FBQy9FLE1BQU0sc0JBQXNCLEdBQUcsR0FBRyxDQUFDO0FBQ25DO3VEQUN1RDtBQUN2RCxNQUFNLGtCQUFrQixHQUFHLE1BQU0sQ0FBQztBQUNsQyxpREFBaUQ7QUFDakQsTUFBTSxtQkFBbUIsR0FBRyxJQUFJLENBQUM7QUFFakM7Ozs7O0dBS0c7QUFDSCxNQUFNLFVBQVUsR0FBRyx3QkFBd0IsQ0FBQztJQUMxQyxVQUFVLEVBQUUsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxvQkFBb0I7SUFDbEQsU0FBUyxFQUFFLHFCQUFxQjtDQUNqQyxDQUFDLENBQUM7QUFFSCxJQUFJLFdBQVcsR0FBa0IsSUFBSSxDQUFDO0FBQ3RDLElBQUksb0JBQW9CLEdBQUcsQ0FBQyxDQUFDO0FBRTdCOzs7O0dBSUc7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLFlBQVk7SUFDaEMsSUFBSSxXQUFXLElBQUksSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLG9CQUFvQjtRQUFFLE9BQU8sV0FBVyxDQUFDO0lBRXpFLE1BQU0sT0FBTyxHQUFHLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsSUFBSSxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQzFFLElBQUksQ0FBQyxPQUFPO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyxvREFBb0QsQ0FBQyxDQUFDO0lBQ3BGLE1BQU0sR0FBRyxHQUFHLE1BQU0sVUFBVSxDQUFDLE1BQU0sRUFBRSxDQUFDO0lBRXRDLE1BQU0sVUFBVSxHQUFHLElBQUksZUFBZSxFQUFFLENBQUM7SUFDekMsTUFBTSxLQUFLLEdBQUcsVUFBVSxDQUFDLEdBQUcsRUFBRSxDQUFDLFVBQVUsQ0FBQyxLQUFLLEVBQUUsRUFBRSxtQkFBbUIsQ0FBQyxDQUFDO0lBQ3hFLElBQUksR0FBYSxDQUFDO0lBQ2xCLElBQUksQ0FBQztRQUNILEdBQUcsR0FBRyxNQUFNLEtBQUssQ0FBQyxHQUFHLE9BQU8sMEJBQTBCLEVBQUU7WUFDdEQsTUFBTSxFQUFFLE1BQU07WUFDZCxPQUFPLEVBQUUsRUFBRSxjQUFjLEVBQUUsa0JBQWtCLEVBQUUsUUFBUSxFQUFFLGtCQUFrQixFQUFFO1lBQzdFLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLEVBQUUsR0FBRyxFQUFFLENBQUM7WUFDN0IsTUFBTSxFQUFFLFVBQVUsQ0FBQyxNQUFNO1NBQzFCLENBQUMsQ0FBQztJQUNMLENBQUM7WUFBUyxDQUFDO1FBQ1QsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0lBQ3RCLENBQUM7SUFFRCxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUUsRUFBRSxDQUFDO1FBQ1osNEVBQTRFO1FBQzVFLHlFQUF5RTtRQUN6RSwrQ0FBK0M7UUFDL0MsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLEdBQUcsSUFBSSxHQUFHLENBQUMsTUFBTSxLQUFLLEdBQUcsRUFBRSxDQUFDO1lBQzdDLE1BQU0sSUFBSSxLQUFLLENBQ2IsZ0NBQWdDLEdBQUcsQ0FBQyxNQUFNLG1EQUFtRDtrQkFDM0Ysb0ZBQW9GLENBQ3ZGLENBQUM7UUFDSixDQUFDO1FBQ0QsTUFBTSxJQUFJLEtBQUssQ0FBQywrQkFBK0IsR0FBRyxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUM7SUFDL0QsQ0FBQztJQUVELE1BQU0sSUFBSSxHQUFHLE1BQU0sR0FBRyxDQUFDLElBQUksRUFBRSxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUE0RCxDQUFDO0lBQzNHLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLEVBQUUsV0FBVyxDQUFDO0lBQzNDLElBQUksQ0FBQyxXQUFXO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyw4Q0FBOEMsQ0FBQyxDQUFDO0lBRWxGLE1BQU0sS0FBSyxHQUFHLENBQUMsT0FBTyxJQUFJLENBQUMsSUFBSSxFQUFFLFNBQVMsS0FBSyxRQUFRLElBQUksSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLEdBQUcsSUFBSSxDQUFDO0lBQ3ZILFdBQVcsR0FBRyxXQUFXLENBQUM7SUFDMUIsb0JBQW9CLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsa0JBQWtCLEVBQUUsS0FBSyxHQUFHLHNCQUFzQixDQUFDLENBQUM7SUFDakcsR0FBRyxDQUFDLElBQUksQ0FBQywrREFBK0QsQ0FBQyxDQUFDO0lBQzFFLE9BQU8sV0FBVyxDQUFDO0FBQ3JCLENBQUM7QUFFRDs7Ozs7O0dBTUc7QUFDSCxNQUFNLFVBQVUsb0JBQW9CO0lBQ2xDLFdBQVcsR0FBRyxJQUFJLENBQUM7SUFDbkIsb0JBQW9CLEdBQUcsQ0FBQyxDQUFDO0lBQ3pCLFVBQVUsQ0FBQyxVQUFVLEVBQUUsQ0FBQztBQUMxQixDQUFDO0FBRUQsMkRBQTJEO0FBQzNELE1BQU0sVUFBVSxrQkFBa0I7SUFDaEMsV0FBVyxHQUFHLElBQUksQ0FBQztJQUNuQixvQkFBb0IsR0FBRyxDQUFDLENBQUM7SUFDekIsVUFBVSxDQUFDLEtBQUssRUFBRSxDQUFDO0FBQ3JCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvLyBDb3B5cmlnaHQgMjAyNiBQaXBlbGluZSBCdWlsZGVyIENvbnRyaWJ1dG9yc1xuLy8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjBcblxuLyoqXG4gKiBSZXBvcnRpbmctQVBJIGF1dGhlbnRpY2F0aW9uLiBUaGUgc3RvcmVkIGNyZWRlbnRpYWwgaXMgYW4gT1BBUVVFXG4gKiBzZXJ2aWNlLWFjY291bnQga2V5IChgcGJfc2Ff4oCmYCksIHRyYWRlZCBhdCBwbGF0Zm9ybSBmb3IgYSBzaG9ydC1saXZlZCBKV1QuXG4gKiBOb3RoaW5nIGlzIHZlcmlmaWVkIGhlcmU6IGFuIG9wYXF1ZSBrZXkgaGFzIG5vIGNsYWltcyBhbmQgbm8gc2lnbmF0dXJlLCBhbmRcbiAqIHRoZSBleGNoYW5nZSByZS1yZWFkcyB0aGUgYWNjb3VudCwgaXRzIG9yZyBhbmQgdGhlIGtleSdzIG93biBzdGF0ZSBldmVyeSB0aW1lLFxuICogd2hpY2ggaXMgd2hhdCBtYWtlcyBhIHJldm9jYXRpb24gdGFrZSBlZmZlY3Qgd2l0aGluIG9uZSB0b2tlbiBsaWZldGltZS5cbiAqXG4gKiBDb250cmFjdDogY2FjaGUgdGhlIHRva2VuIGluIHRoZSB3YXJtIGNvbnRhaW5lciwgcmVmcmVzaCBFQVJMWSAoYmVmb3JlXG4gKiBleHBpcnkpIHNvIGEgYmF0Y2ggbmV2ZXIgcHJlc2VudHMgYSB0b2tlbiB0aGF0IGRpZXMgbWlkLWZsaWdodCwgYW5kIGZhaWwgdGhlXG4gKiBiYXRjaCBvbiBhbiBleGNoYW5nZSBmYWlsdXJlIHJhdGhlciB0aGFuIHNoaXBwaW5nIGV2ZW50cyB1bmF1dGhlbnRpY2F0ZWQuXG4gKi9cblxuaW1wb3J0IHsgY3JlYXRlUGxhdGZvcm1DcmVkZW50aWFsIH0gZnJvbSAnQHBpcGVsaW5lLWJ1aWxkZXIvcGlwZWxpbmUtY29yZS9saWIvaGFuZGxlcnMvcGxhdGZvcm0tY3JlZGVudGlhbC5qcyc7XG5pbXBvcnQgeyBsb2cgfSBmcm9tICcuL3V0aWwuanMnO1xuXG4vKiogUmVmcmVzaCBhdCB0aGlzIGZyYWN0aW9uIG9mIHRoZSB0b2tlbidzIGxpZmUg4oCUIG5ldmVyIGF0IHRoZSBsYXN0IG1vbWVudC4gKi9cbmNvbnN0IFRPS0VOX1JFRlJFU0hfRlJBQ1RJT04gPSAwLjg7XG4vKiogRmxvb3Igb24gdGhlIGNhY2hlZCBsaWZldGltZSwgc28gYSBzdXJwcmlzaW5nbHkgc2hvcnQgVFRMIGNhbid0IGNhdXNlIGFcbiAqICByZWZyZXNoIHN0b3JtIChvbmUgZXhjaGFuZ2UgcGVyIGJhdGNoIGF0IHdvcnN0KS4gKi9cbmNvbnN0IE1JTl9UT0tFTl9DQUNIRV9NUyA9IDMwXzAwMDtcbi8qKiBQZXItcmVxdWVzdCB0aW1lb3V0IGZvciB0aGUgZXhjaGFuZ2UgY2FsbC4gKi9cbmNvbnN0IEVYQ0hBTkdFX1RJTUVPVVRfTVMgPSA1MDAwO1xuXG4vKipcbiAqIFRoZSBzZXJ2aWNlLWFjY291bnQga2V5OiBgUExBVEZPUk1fQUNDRVNTX0tFWWAgaWYgc2V0LCBvdGhlcndpc2UgdGhlXG4gKiBgcGFzc3dvcmRgIGZpZWxkIG9mIHRoZSBTZWNyZXRzIE1hbmFnZXIgc2VjcmV0IG5hbWVkIGJ5IGBQTEFURk9STV9TRUNSRVRfTkFNRWAuXG4gKiBBIHN0b3JlZCBKV1QgaXMgcmVmdXNlZCBsb3VkbHksIG5hbWluZyB0aGUgZml4LCBpbnN0ZWFkIG9mIGZhaWxpbmcgbGF0ZXIgYXMgYW5cbiAqIG9wYXF1ZSA0MDEgZnJvbSB0aGUgcmVwb3J0aW5nIEFQSS5cbiAqL1xuY29uc3QgY3JlZGVudGlhbCA9IGNyZWF0ZVBsYXRmb3JtQ3JlZGVudGlhbCh7XG4gIHNlY3JldE5hbWU6ICgpID0+IHByb2Nlc3MuZW52LlBMQVRGT1JNX1NFQ1JFVF9OQU1FLFxuICBlbnZLZXlWYXI6ICdQTEFURk9STV9BQ0NFU1NfS0VZJyxcbn0pO1xuXG5sZXQgY2FjaGVkVG9rZW46IHN0cmluZyB8IG51bGwgPSBudWxsO1xubGV0IGNhY2hlZFRva2VuRXhwaXJlc0F0ID0gMDtcblxuLyoqXG4gKiBUcmFkZSB0aGUgc3RvcmVkIGtleSBmb3IgYSBzaG9ydC1saXZlZCBwbGF0Zm9ybSB0b2tlbiwgY2FjaGVkIHBlciB3YXJtXG4gKiBjb250YWluZXIgdW50aWwgaXQgaXMgODAlIHRocm91Z2ggaXRzIGxpZmUuIFRocm93cyBvbiBhbnkgZmFpbHVyZSwgd2hpY2ggZmFpbHNcbiAqIHRoZSBiYXRjaCBzbyBTUVMgcmV0cmllcyBpdCDigJQgYW4gdW5hdXRoZW50aWNhdGVkIHNoaXAgaXMgbmV2ZXIgYW4gb3B0aW9uLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZ2V0QXV0aFRva2VuKCk6IFByb21pc2U8c3RyaW5nPiB7XG4gIGlmIChjYWNoZWRUb2tlbiAmJiBEYXRlLm5vdygpIDwgY2FjaGVkVG9rZW5FeHBpcmVzQXQpIHJldHVybiBjYWNoZWRUb2tlbjtcblxuICBjb25zdCBiYXNlVXJsID0gKHByb2Nlc3MuZW52LlBMQVRGT1JNX0JBU0VfVVJMIHx8ICcnKS5yZXBsYWNlKC9cXC8rJC8sICcnKTtcbiAgaWYgKCFiYXNlVXJsKSB0aHJvdyBuZXcgRXJyb3IoJ1BMQVRGT1JNX0JBU0VfVVJMIGVudmlyb25tZW50IHZhcmlhYmxlIGlzIHJlcXVpcmVkJyk7XG4gIGNvbnN0IGtleSA9IGF3YWl0IGNyZWRlbnRpYWwuZ2V0S2V5KCk7XG5cbiAgY29uc3QgY29udHJvbGxlciA9IG5ldyBBYm9ydENvbnRyb2xsZXIoKTtcbiAgY29uc3QgdGltZXIgPSBzZXRUaW1lb3V0KCgpID0+IGNvbnRyb2xsZXIuYWJvcnQoKSwgRVhDSEFOR0VfVElNRU9VVF9NUyk7XG4gIGxldCByZXM6IFJlc3BvbnNlO1xuICB0cnkge1xuICAgIHJlcyA9IGF3YWl0IGZldGNoKGAke2Jhc2VVcmx9L2FwaS9hdXRoL3Rva2VuL2V4Y2hhbmdlYCwge1xuICAgICAgbWV0aG9kOiAnUE9TVCcsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicsICdBY2NlcHQnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICAgIGJvZHk6IEpTT04uc3RyaW5naWZ5KHsga2V5IH0pLFxuICAgICAgc2lnbmFsOiBjb250cm9sbGVyLnNpZ25hbCxcbiAgICB9KTtcbiAgfSBmaW5hbGx5IHtcbiAgICBjbGVhclRpbWVvdXQodGltZXIpO1xuICB9XG5cbiAgaWYgKCFyZXMub2spIHtcbiAgICAvLyBBIHJlamVjdGVkIGtleSBpcyB0aGUgb25lIGZhaWx1cmUgYW4gb3BlcmF0b3IgaGFzIHRvIGFjdCBvbiwgYW5kIHBsYXRmb3JtXG4gICAgLy8gZGVsaWJlcmF0ZWx5IGFuc3dlcnMgZXZlcnkgcmVmdXNhbCB0aGUgc2FtZSB3YXkg4oCUIHNvIHNheSB3aGF0IHRvIGNoZWNrXG4gICAgLy8gcmF0aGVyIHRoYW4gZWNob2luZyBhbiB1bmRpZmZlcmVudGlhdGVkIDQwMS5cbiAgICBpZiAocmVzLnN0YXR1cyA9PT0gNDAxIHx8IHJlcy5zdGF0dXMgPT09IDQwMykge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICBgQWNjZXNzLWtleSBleGNoYW5nZSByZWZ1c2VkICgke3Jlcy5zdGF0dXN9KSDigJQgdGhlIHN0b3JlZCBrZXkgaXMgdW5rbm93biwgcmV2b2tlZCwgZXhwaXJlZCwgYFxuICAgICAgICArICdpdHMgc2VydmljZSBhY2NvdW50IGlzIGRpc2FibGVkLCBvciB0aGlzIGFkZHJlc3MgaXMgb3V0c2lkZSB0aGUga2V54oCZcyBJUCBhbGxvd2xpc3QnLFxuICAgICAgKTtcbiAgICB9XG4gICAgdGhyb3cgbmV3IEVycm9yKGBBY2Nlc3Mta2V5IGV4Y2hhbmdlIGZhaWxlZDogJHtyZXMuc3RhdHVzfWApO1xuICB9XG5cbiAgY29uc3QgYm9keSA9IGF3YWl0IHJlcy5qc29uKCkuY2F0Y2goKCkgPT4gKHt9KSkgYXMgeyBkYXRhPzogeyBhY2Nlc3NUb2tlbj86IHN0cmluZzsgZXhwaXJlc0luPzogbnVtYmVyIH0gfTtcbiAgY29uc3QgYWNjZXNzVG9rZW4gPSBib2R5LmRhdGE/LmFjY2Vzc1Rva2VuO1xuICBpZiAoIWFjY2Vzc1Rva2VuKSB0aHJvdyBuZXcgRXJyb3IoJ0FjY2Vzcy1rZXkgZXhjaGFuZ2UgcmV0dXJuZWQgbm8gYWNjZXNzIHRva2VuJyk7XG5cbiAgY29uc3QgdHRsTXMgPSAodHlwZW9mIGJvZHkuZGF0YT8uZXhwaXJlc0luID09PSAnbnVtYmVyJyAmJiBib2R5LmRhdGEuZXhwaXJlc0luID4gMCA/IGJvZHkuZGF0YS5leHBpcmVzSW4gOiAzMDApICogMTAwMDtcbiAgY2FjaGVkVG9rZW4gPSBhY2Nlc3NUb2tlbjtcbiAgY2FjaGVkVG9rZW5FeHBpcmVzQXQgPSBEYXRlLm5vdygpICsgTWF0aC5tYXgoTUlOX1RPS0VOX0NBQ0hFX01TLCB0dGxNcyAqIFRPS0VOX1JFRlJFU0hfRlJBQ1RJT04pO1xuICBsb2cuaW5mbygnRXhjaGFuZ2VkIHRoZSBzdG9yZWQgc2VydmljZS1hY2NvdW50IGtleSBmb3IgYSBwbGF0Zm9ybSB0b2tlbicpO1xuICByZXR1cm4gYWNjZXNzVG9rZW47XG59XG5cbi8qKlxuICogRHJvcCBldmVyeXRoaW5nIHRoZSBjb250YWluZXIgaG9sZHMgYWJvdXQgaXRzIGNyZWRlbnRpYWwgYWZ0ZXIgYSA0MDEvNDAzIGZyb21cbiAqIHRoZSBBUEk6IHRoZSB0b2tlbiwgYW5kIHRoZSBLRVkg4oCUIHRoZSB0b2tlbi1yZW5ldyBMYW1iZGEgcm90YXRlcyB0aGUgc2VjcmV0XG4gKiB1bmRlcm5lYXRoIHVzLCBzbyBhIHdhcm0gY29udGFpbmVyIHRoYXQgb25seSByZS1leGNoYW5nZWQgd291bGQga2VlcFxuICogcHJlc2VudGluZyB0aGUgcmV0aXJlZCBrZXkgZm9yZXZlci4gQW4gZW52LXByb3ZpZGVkIGtleSBjYW5ub3QgY2hhbmdlIHdpdGhvdXRcbiAqIGEgbmV3IGNvbnRhaW5lciwgc28gaXQgaXMga2VwdC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGludmFsaWRhdGVDcmVkZW50aWFsKCk6IHZvaWQge1xuICBjYWNoZWRUb2tlbiA9IG51bGw7XG4gIGNhY2hlZFRva2VuRXhwaXJlc0F0ID0gMDtcbiAgY3JlZGVudGlhbC5pbnZhbGlkYXRlKCk7XG59XG5cbi8qKiBAaW50ZXJuYWwgVGVzdC1vbmx5OiBmb3JnZXQgZXZlcnkgY2FjaGVkIGNyZWRlbnRpYWwuICovXG5leHBvcnQgZnVuY3Rpb24gX3Jlc2V0QXV0aEZvclRlc3RzKCk6IHZvaWQge1xuICBjYWNoZWRUb2tlbiA9IG51bGw7XG4gIGNhY2hlZFRva2VuRXhwaXJlc0F0ID0gMDtcbiAgY3JlZGVudGlhbC5yZXNldCgpO1xufVxuIl19
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Throttled, best-effort post-success work: emit per-org ingest-health and self-heal
3
+ * the DLQ. Never throws. `forwarded`/`lastEventAt` accumulate PER ORG across batches
4
+ * between health posts; `dropped` is the shared fleet DLQ depth snapshot. A per-org
5
+ * counter is zeroed only after that org's health POST is confirmed 2xx.
6
+ */
7
+ export declare function reportHealthAndRedrive(baseUrl: string, mainQueueArn: string | undefined): Promise<void>;
8
+ /** Count a forwarded batch toward each resolved org's delivery-health report. */
9
+ export declare function recordForwarded(events: ReadonlyArray<{
10
+ orgId: string | null;
11
+ completedAt?: string;
12
+ startedAt?: string;
13
+ }>): void;
package/lib/health.js ADDED
@@ -0,0 +1,191 @@
1
+ // Copyright 2026 Pipeline Builder Contributors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { getAuthToken } from './auth.js';
4
+ import { BoundedMap, CACHE_MAX_ENTRIES, loadSdk, log } from './util.js';
5
+ let sqsMod;
6
+ let sqsClientsByRegion;
7
+ async function sqsClient(region) {
8
+ if (!sqsMod)
9
+ sqsMod = await loadSdk('@aws-sdk/client-sqs');
10
+ if (!sqsClientsByRegion)
11
+ sqsClientsByRegion = new Map();
12
+ let client = sqsClientsByRegion.get(region);
13
+ if (!client) {
14
+ client = new sqsMod.SQSClient({ region });
15
+ sqsClientsByRegion.set(region, client);
16
+ }
17
+ return client;
18
+ }
19
+ // Delivery-health counters are attributed PER RESOLVED ORG (the forwarder is a fleet
20
+ // Lambda serving many orgs; a single global counter would mis-attribute one org's
21
+ // throughput to another). Keyed by orgId ('' bucket = pipeline with no OrgId tag).
22
+ // The DLQ depth (`dropped`) is genuinely fleet-global — one shared DLQ — so it is a
23
+ // single snapshot posted alongside each org's health, documented as the shared depth.
24
+ const forwardedByOrg = new BoundedMap(CACHE_MAX_ENTRIES);
25
+ const lastEventAtByOrg = new BoundedMap(CACHE_MAX_ENTRIES);
26
+ let lastHealthAt = 0;
27
+ let lastRedriveAt = 0;
28
+ const HEALTH_THROTTLE_MS = 60 * 1000;
29
+ const REDRIVE_THROTTLE_MS = 5 * 60 * 1000;
30
+ // Poison-message ceiling: if the oldest DLQ message is older than this, we STOP
31
+ // redriving it (DLQ→main→fail→DLQ would loop forever). Such messages are left for an
32
+ // alarm / manual inspection instead of churning the fleet indefinitely.
33
+ const MAX_DLQ_REDRIVE_AGE_SEC = 6 * 60 * 60; // 6 hours
34
+ /** Parse the SQS trigger ARN and derive the main + DLQ ARNs and the DLQ URL. */
35
+ function deriveQueues(mainQueueArn) {
36
+ if (!mainQueueArn)
37
+ return undefined;
38
+ // arn:aws:sqs:{region}:{account}:{name}
39
+ const parts = mainQueueArn.split(':');
40
+ if (parts.length < 6 || parts[2] !== 'sqs')
41
+ return undefined;
42
+ const [, , , region, account, name] = parts;
43
+ const dlqArn = process.env.EVENT_DLQ_ARN || `arn:aws:sqs:${region}:${account}:${name}-dlq`;
44
+ const dlqName = dlqArn.split(':').pop() ?? `${name}-dlq`;
45
+ const dlqUrl = `https://sqs.${region}.amazonaws.com/${account}/${dlqName}`;
46
+ return { region, mainArn: mainQueueArn, dlqArn, dlqUrl };
47
+ }
48
+ /** Snapshot the DLQ depth + the age (seconds) of its oldest message (for the
49
+ * poison-message redrive ceiling). Missing/NaN attributes fall back to 0. */
50
+ async function dlqStats(region, dlqUrl) {
51
+ if (!sqsMod)
52
+ sqsMod = await loadSdk('@aws-sdk/client-sqs');
53
+ const client = await sqsClient(region);
54
+ const out = await client.send(new sqsMod.GetQueueAttributesCommand({
55
+ QueueUrl: dlqUrl,
56
+ AttributeNames: ['ApproximateNumberOfMessages', 'ApproximateAgeOfOldestMessage'],
57
+ }));
58
+ const n = Number(out.Attributes?.ApproximateNumberOfMessages ?? '0');
59
+ const age = Number(out.Attributes?.ApproximateAgeOfOldestMessage ?? '0');
60
+ return {
61
+ depth: Number.isFinite(n) ? n : 0,
62
+ oldestAgeSec: Number.isFinite(age) ? age : 0,
63
+ };
64
+ }
65
+ async function hasActiveMoveTask(region, dlqArn) {
66
+ if (!sqsMod)
67
+ sqsMod = await loadSdk('@aws-sdk/client-sqs');
68
+ const client = await sqsClient(region);
69
+ const out = await client.send(new sqsMod.ListMessageMoveTasksCommand({ SourceArn: dlqArn }));
70
+ return (out.Results ?? []).some(t => t.Status === 'RUNNING');
71
+ }
72
+ async function startMoveTask(region, dlqArn, mainArn) {
73
+ if (!sqsMod)
74
+ sqsMod = await loadSdk('@aws-sdk/client-sqs');
75
+ const client = await sqsClient(region);
76
+ await client.send(new sqsMod.StartMessageMoveTaskCommand({ SourceArn: dlqArn, DestinationArn: mainArn }));
77
+ }
78
+ /** POST one org's ingest-health signal. Returns true only on a confirmed 2xx so the
79
+ * caller resets that org's `forwarded` counter ONLY when the signal was accepted
80
+ * (a dropped/failed POST must not silently zero the accumulated count). */
81
+ async function postIngestHealth(baseUrl, body) {
82
+ try {
83
+ const token = await getAuthToken();
84
+ const res = await fetch(`${baseUrl}/api/reports/ingest-health`, {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
87
+ body: JSON.stringify(body),
88
+ });
89
+ if (!res.ok) {
90
+ log.warn('ingest-health POST returned non-2xx (ignored)', { status: res.status, orgId: body.orgId });
91
+ return false;
92
+ }
93
+ return true;
94
+ }
95
+ catch (err) {
96
+ log.warn('ingest-health POST failed (ignored)', { error: String(err), orgId: body.orgId });
97
+ return false;
98
+ }
99
+ }
100
+ /**
101
+ * Throttled, best-effort post-success work: emit per-org ingest-health and self-heal
102
+ * the DLQ. Never throws. `forwarded`/`lastEventAt` accumulate PER ORG across batches
103
+ * between health posts; `dropped` is the shared fleet DLQ depth snapshot. A per-org
104
+ * counter is zeroed only after that org's health POST is confirmed 2xx.
105
+ */
106
+ export async function reportHealthAndRedrive(baseUrl, mainQueueArn) {
107
+ const now = Date.now();
108
+ if (now - lastHealthAt < HEALTH_THROTTLE_MS)
109
+ return;
110
+ lastHealthAt = now;
111
+ try {
112
+ const q = deriveQueues(mainQueueArn);
113
+ let depth = 0;
114
+ let oldestAgeSec = 0;
115
+ if (q) {
116
+ try {
117
+ const stats = await dlqStats(q.region, q.dlqUrl);
118
+ depth = stats.depth;
119
+ oldestAgeSec = stats.oldestAgeSec;
120
+ }
121
+ catch (err) {
122
+ log.warn('DLQ stats check failed (ignored)', { error: err?.name ?? String(err) });
123
+ }
124
+ }
125
+ // Post one health signal per resolved org, attributing its own forwarded count +
126
+ // last event time. `dropped` (the shared fleet DLQ depth) rides along on each.
127
+ // Snapshot the keys first so we don't mutate the map while iterating.
128
+ const orgKeys = new Set([...forwardedByOrg.keys(), ...lastEventAtByOrg.keys()]);
129
+ if (orgKeys.size === 0)
130
+ orgKeys.add(''); // nothing forwarded yet → still report depth
131
+ for (const key of orgKeys) {
132
+ const forwarded = forwardedByOrg.get(key) ?? 0;
133
+ const lastEventAt = lastEventAtByOrg.get(key);
134
+ const ok = await postIngestHealth(baseUrl, {
135
+ ...(key ? { orgId: key } : {}),
136
+ forwarded,
137
+ dropped: depth,
138
+ lastEventAt,
139
+ });
140
+ // Reset the forwarded counter ONLY on a confirmed 2xx — a failed/dropped POST
141
+ // keeps the count so the next tick re-reports it instead of losing it.
142
+ if (ok)
143
+ forwardedByOrg.set(key, 0);
144
+ }
145
+ if (q && depth > 0 && now - lastRedriveAt >= REDRIVE_THROTTLE_MS) {
146
+ // Poison-message ceiling: never redrive a DLQ whose oldest message has aged
147
+ // past the ceiling — that message keeps failing and would loop DLQ→main→DLQ.
148
+ if (oldestAgeSec > MAX_DLQ_REDRIVE_AGE_SEC) {
149
+ log.warn('DLQ oldest message exceeds poison-age ceiling — skipping redrive (needs manual attention)', {
150
+ dlqArn: q.dlqArn, dropped: depth, oldestAgeSec, ceilingSec: MAX_DLQ_REDRIVE_AGE_SEC,
151
+ });
152
+ }
153
+ else {
154
+ try {
155
+ if (!(await hasActiveMoveTask(q.region, q.dlqArn))) {
156
+ lastRedriveAt = now;
157
+ await startMoveTask(q.region, q.dlqArn, q.mainArn);
158
+ log.info('Started self-healing DLQ→main redrive', { dlqArn: q.dlqArn, dropped: depth, oldestAgeSec });
159
+ }
160
+ }
161
+ catch (err) {
162
+ log.warn('DLQ redrive failed (ignored)', { error: err?.name ?? String(err) });
163
+ }
164
+ }
165
+ }
166
+ }
167
+ catch (err) {
168
+ log.warn('health/redrive step failed (ignored)', { error: String(err) });
169
+ }
170
+ }
171
+ /** Count a forwarded batch toward each resolved org's delivery-health report. */
172
+ export function recordForwarded(events) {
173
+ for (const e of events) {
174
+ const key = e.orgId ?? '';
175
+ forwardedByOrg.set(key, (forwardedByOrg.get(key) ?? 0) + 1);
176
+ const at = e.completedAt || e.startedAt;
177
+ if (at) {
178
+ const prev = lastEventAtByOrg.get(key);
179
+ if (!prev || at > prev)
180
+ lastEventAtByOrg.set(key, at);
181
+ }
182
+ }
183
+ }
184
+ /** @internal Test-only: forget health counters and throttles. */
185
+ export function _resetHealthForTests() {
186
+ forwardedByOrg.clear();
187
+ lastEventAtByOrg.clear();
188
+ lastHealthAt = 0;
189
+ lastRedriveAt = 0;
190
+ }
191
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVhbHRoLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL2hlYWx0aC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSwrQ0FBK0M7QUFDL0Msc0NBQXNDO0FBRXRDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDekMsT0FBTyxFQUFFLFVBQVUsRUFBRSxpQkFBaUIsRUFBRSxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBeUJ4RSxJQUFJLE1BQTZCLENBQUM7QUFDbEMsSUFBSSxrQkFBMEQsQ0FBQztBQUMvRCxLQUFLLFVBQVUsU0FBUyxDQUFDLE1BQWM7SUFDckMsSUFBSSxDQUFDLE1BQU07UUFBRSxNQUFNLEdBQUcsTUFBTSxPQUFPLENBQVkscUJBQXFCLENBQUMsQ0FBQztJQUN0RSxJQUFJLENBQUMsa0JBQWtCO1FBQUUsa0JBQWtCLEdBQUcsSUFBSSxHQUFHLEVBQUUsQ0FBQztJQUN4RCxJQUFJLE1BQU0sR0FBRyxrQkFBa0IsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDNUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ1osTUFBTSxHQUFHLElBQUksTUFBTSxDQUFDLFNBQVMsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDLENBQUM7UUFDMUMsa0JBQWtCLENBQUMsR0FBRyxDQUFDLE1BQU0sRUFBRSxNQUFNLENBQUMsQ0FBQztJQUN6QyxDQUFDO0lBQ0QsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQztBQUVELHFGQUFxRjtBQUNyRixrRkFBa0Y7QUFDbEYsbUZBQW1GO0FBQ25GLG9GQUFvRjtBQUNwRixzRkFBc0Y7QUFDdEYsTUFBTSxjQUFjLEdBQUcsSUFBSSxVQUFVLENBQWlCLGlCQUFpQixDQUFDLENBQUM7QUFDekUsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLFVBQVUsQ0FBaUIsaUJBQWlCLENBQUMsQ0FBQztBQUMzRSxJQUFJLFlBQVksR0FBRyxDQUFDLENBQUM7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Q0FBQyxJQUFJLE1BQU0sRUFBRSxDQUFDO1FBQ3ZCLE1BQU0sR0FBRyxHQUFHLENBQUMsQ0FBQyxLQUFLLElBQUksRUFBRSxDQUFDO1FBQzFCLGNBQWMsQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLENBQUMsY0FBYyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQztRQUM1RCxNQUFNLEVBQUUsR0FBRyxDQUFDLENBQUMsV0FBVyxJQUFJLENBQUMsQ0FBQyxTQUFTLENBQUM7UUFDeEMsSUFBSSxFQUFFLEVBQUUsQ0FBQztZQUNQLE1BQU0sSUFBSSxHQUFHLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQztZQUN2QyxJQUFJLENBQUMsSUFBSSxJQUFJLEVBQUUsR0FBRyxJQUFJO2dCQUFFLGdCQUFnQixDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7UUFDeEQsQ0FBQztJQUNILENBQUM7QUFDSCxDQUFDO0FBRUQsaUVBQWlFO0FBQ2pFLE1BQU0sVUFBVSxvQkFBb0I7SUFDbEMsY0FBYyxDQUFDLEtBQUssRUFBRSxDQUFDO0lBQ3ZCLGdCQUFnQixDQUFDLEtBQUssRUFBRSxDQUFDO0lBQ3pCLFlBQVksR0FBRyxDQUFDLENBQUM7SUFDakIsYUFBYSxHQUFHLENBQUMsQ0FBQztBQUNwQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbmltcG9ydCB7IGdldEF1dGhUb2tlbiB9IGZyb20gJy4vYXV0aC5qcyc7XG5pbXBvcnQgeyBCb3VuZGVkTWFwLCBDQUNIRV9NQVhfRU5UUklFUywgbG9hZFNkaywgbG9nIH0gZnJvbSAnLi91dGlsLmpzJztcblxuLy8gRGVsaXZlcnkgaGVhbHRoICsgc2VsZi1oZWFsaW5nIERMUSByZWRyaXZlLlxuLy8gQWZ0ZXIgYSBTVUNDRVNTRlVMIGJhdGNoIFBPU1Qgd2UgKHRocm90dGxlZCwgYmVzdC1lZmZvcnQpOlxuLy8gICAxLiBQT1NUIGFuIGluZ2VzdC1oZWFsdGggc2lnbmFsIHRvIC9hcGkvcmVwb3J0cy9pbmdlc3QtaGVhbHRoIHNvIHRoZSBSZXBvcnRzIFVJXG4vLyAgICAgIGNhbiBzaG93IGZsb3dpbmcgLyBzdGFsZSAvIGRyb3BwaW5nLlxuLy8gICAyLiBJZiB0aGUgRExRIGlzIG5vbi1lbXB0eSwgbm8gbW92ZSB0YXNrIGlzIGFscmVhZHkgcnVubmluZywgQU5EIHRoZSBvbGRlc3QgRExRXG4vLyAgICAgIG1lc3NhZ2UgaXMgeW91bmdlciB0aGFuIHRoZSBwb2lzb24tYWdlIGNlaWxpbmcsIGtpY2sgb2ZmIGFuIFNRU1xuLy8gICAgICBNZXNzYWdlTW92ZVRhc2sgKERMUSDihpIgbWFpbiBxdWV1ZSkgc28gcmV0cnlhYmxlIGZhaWx1cmVzIHNlbGYtaGVhbCB3aXRob3V0IGFcbi8vICAgICAgbmV3IEFXUyBzZXJ2aWNlLiBUaGUgcmVwb3J0aW5nIGluZ2VzdCdzIHBhcnRpYWwtdW5pcXVlIGluZGV4IGRlZHVwZXMgYW55XG4vLyAgICAgIHJlZGVsaXZlcmVkIGV2ZW50LCBzbyBhIHJlZHJpdmUgY2FuJ3QgZG91YmxlLWNvdW50LlxuLy8gR3VhcmRzOiBvbmx5IG9uIHN1Y2Nlc3MsIG9uZS1hdC1hLXRpbWUgKExpc3RNZXNzYWdlTW92ZVRhc2tzKSwgYSBwb2lzb24tYWdlIGNlaWxpbmdcbi8vIChzbyBhIG1lc3NhZ2UgdGhhdCBhbHdheXMgZmFpbHMgY2FuJ3QgbG9vcCBETFHihpJtYWlu4oaSRExRIGZvcmV2ZXIpLCBhbmQgdGhyb3R0bGVkIHZpYVxuLy8gbW9kdWxlIHRpbWVzdGFtcHMuIE5FVkVSIHRocm93cyDigJQgYSBmYWlsdXJlIGhlcmUgbXVzdCBub3QgZmFpbCB0aGUgYmF0Y2guXG4vL1xuLy8gSUFNIHJlcXVpcmVkIChkb2N1bWVudCBvbmx5IOKAlCBncmFudGVkIG9uIHRoZSBldmVudHMgc3RhY2spOlxuLy8gICBzcXM6R2V0UXVldWVBdHRyaWJ1dGVzLCBzcXM6TGlzdE1lc3NhZ2VNb3ZlVGFza3MsIHNxczpTdGFydE1lc3NhZ2VNb3ZlVGFzayAob24gdGhlIERMUSlcblxuaW50ZXJmYWNlIFNxc0NsaWVudExpa2UgeyBzZW5kKGNvbW1hbmQ6IHVua25vd24pOiBQcm9taXNlPHVua25vd24+IH1cbmludGVyZmFjZSBTcXNNb2R1bGUge1xuICBTUVNDbGllbnQ6IG5ldyAoY29uZmlnOiB7IHJlZ2lvbjogc3RyaW5nIH0pID0+IFNxc0NsaWVudExpa2U7XG4gIEdldFF1ZXVlQXR0cmlidXRlc0NvbW1hbmQ6IG5ldyAoaW5wdXQ6IHsgUXVldWVVcmw6IHN0cmluZzsgQXR0cmlidXRlTmFtZXM6IHN0cmluZ1tdIH0pID0+IHVua25vd247XG4gIExpc3RNZXNzYWdlTW92ZVRhc2tzQ29tbWFuZDogbmV3IChpbnB1dDogeyBTb3VyY2VBcm46IHN0cmluZyB9KSA9PiB1bmtub3duO1xuICBTdGFydE1lc3NhZ2VNb3ZlVGFza0NvbW1hbmQ6IG5ldyAoaW5wdXQ6IHsgU291cmNlQXJuOiBzdHJpbmc7IERlc3RpbmF0aW9uQXJuPzogc3RyaW5nIH0pID0+IHVua25vd247XG59XG5sZXQgc3FzTW9kOiBTcXNNb2R1bGUgfCB1bmRlZmluZWQ7XG5sZXQgc3FzQ2xpZW50c0J5UmVnaW9uOiBNYXA8c3RyaW5nLCBTcXNDbGllbnRMaWtlPiB8IHVuZGVmaW5lZDtcbmFzeW5jIGZ1bmN0aW9uIHNxc0NsaWVudChyZWdpb246IHN0cmluZyk6IFByb21pc2U8U3FzQ2xpZW50TGlrZT4ge1xuICBpZiAoIXNxc01vZCkgc3FzTW9kID0gYXdhaXQgbG9hZFNkazxTcXNNb2R1bGU+KCdAYXdzLXNkay9jbGllbnQtc3FzJyk7XG4gIGlmICghc3FzQ2xpZW50c0J5UmVnaW9uKSBzcXNDbGllbnRzQnlSZWdpb24gPSBuZXcgTWFwKCk7XG4gIGxldCBjbGllbnQgPSBzcXNDbGllbnRzQnlSZWdpb24uZ2V0KHJlZ2lvbik7XG4gIGlmICghY2xpZW50KSB7XG4gICAgY2xpZW50ID0gbmV3IHNxc01vZC5TUVNDbGllbnQoeyByZWdpb24gfSk7XG4gICAgc3FzQ2xpZW50c0J5UmVnaW9uLnNldChyZWdpb24sIGNsaWVudCk7XG4gIH1cbiAgcmV0dXJuIGNsaWVudDtcbn1cblxuLy8gRGVsaXZlcnktaGVhbHRoIGNvdW50ZXJzIGFyZSBhdHRyaWJ1dGVkIFBFUiBSRVNPTFZFRCBPUkcgKHRoZSBmb3J3YXJkZXIgaXMgYSBmbGVldFxuLy8gTGFtYmRhIHNlcnZpbmcgbWFueSBvcmdzOyBhIHNpbmdsZSBnbG9iYWwgY291bnRlciB3b3VsZCBtaXMtYXR0cmlidXRlIG9uZSBvcmcnc1xuLy8gdGhyb3VnaHB1dCB0byBhbm90aGVyKS4gS2V5ZWQgYnkgb3JnSWQgKCcnIGJ1Y2tldCA9IHBpcGVsaW5lIHdpdGggbm8gT3JnSWQgdGFnKS5cbi8vIFRoZSBETFEgZGVwdGggKGBkcm9wcGVkYCkgaXMgZ2VudWluZWx5IGZsZWV0LWdsb2JhbCDigJQgb25lIHNoYXJlZCBETFEg4oCUIHNvIGl0IGlzIGFcbi8vIHNpbmdsZSBzbmFwc2hvdCBwb3N0ZWQgYWxvbmdzaWRlIGVhY2ggb3JnJ3MgaGVhbHRoLCBkb2N1bWVudGVkIGFzIHRoZSBzaGFyZWQgZGVwdGguXG5jb25zdCBmb3J3YXJkZWRCeU9yZyA9IG5ldyBCb3VuZGVkTWFwPHN0cmluZywgbnVtYmVyPihDQUNIRV9NQVhfRU5UUklFUyk7XG5jb25zdCBsYXN0RXZlbnRBdEJ5T3JnID0gbmV3IEJvdW5kZWRNYXA8c3RyaW5nLCBzdHJpbmc+KENBQ0hFX01BWF9FTlRSSUVTKTtcbmxldCBsYXN0SGVhbHRoQXQgPSAwO1xubGV0IGxhc3RSZWRyaXZlQXQgPSAwO1xuY29uc3QgSEVBTFRIX1RIUk9UVExFX01TID0gNjAgKiAxMDAwO1xuY29uc3QgUkVEUklWRV9USFJPVFRMRV9NUyA9IDUgKiA2MCAqIDEwMDA7XG4vLyBQb2lzb24tbWVzc2FnZSBjZWlsaW5nOiBpZiB0aGUgb2xkZXN0IERMUSBtZXNzYWdlIGlzIG9sZGVyIHRoYW4gdGhpcywgd2UgU1RPUFxuLy8gcmVkcml2aW5nIGl0IChETFHihpJtYWlu4oaSZmFpbOKGkkRMUSB3b3VsZCBsb29wIGZvcmV2ZXIpLiBTdWNoIG1lc3NhZ2VzIGFyZSBsZWZ0IGZvciBhblxuLy8gYWxhcm0gLyBtYW51YWwgaW5zcGVjdGlvbiBpbnN0ZWFkIG9mIGNodXJuaW5nIHRoZSBmbGVldCBpbmRlZmluaXRlbHkuXG5jb25zdCBNQVhfRExRX1JFRFJJVkVfQUdFX1NFQyA9IDYgKiA2MCAqIDYwOyAvLyA2IGhvdXJzXG5cbi8qKiBQYXJzZSB0aGUgU1FTIHRyaWdnZXIgQVJOIGFuZCBkZXJpdmUgdGhlIG1haW4gKyBETFEgQVJOcyBhbmQgdGhlIERMUSBVUkwuICovXG5mdW5jdGlvbiBkZXJpdmVRdWV1ZXMobWFpblF1ZXVlQXJuOiBzdHJpbmcgfCB1bmRlZmluZWQpOiB7IHJlZ2lvbjogc3RyaW5nOyBtYWluQXJuOiBzdHJpbmc7IGRscUFybjogc3RyaW5nOyBkbHFVcmw6IHN0cmluZyB9IHwgdW5kZWZpbmVkIHtcbiAgaWYgKCFtYWluUXVldWVBcm4pIHJldHVybiB1bmRlZmluZWQ7XG4gIC8vIGFybjphd3M6c3FzOntyZWdpb259OnthY2NvdW50fTp7bmFtZX1cbiAgY29uc3QgcGFydHMgPSBtYWluUXVldWVBcm4uc3BsaXQoJzonKTtcbiAgaWYgKHBhcnRzLmxlbmd0aCA8IDYgfHwgcGFydHNbMl0gIT09ICdzcXMnKSByZXR1cm4gdW5kZWZpbmVkO1xuICBjb25zdCBbLCAsICwgcmVnaW9uLCBhY2NvdW50LCBuYW1lXSA9IHBhcnRzO1xuICBjb25zdCBkbHFBcm4gPSBwcm9jZXNzLmVudi5FVkVOVF9ETFFfQVJOIHx8IGBhcm46YXdzOnNxczoke3JlZ2lvbn06JHthY2NvdW50fToke25hbWV9LWRscWA7XG4gIGNvbnN0IGRscU5hbWUgPSBkbHFBcm4uc3BsaXQoJzonKS5wb3AoKSA/PyBgJHtuYW1lfS1kbHFgO1xuICBjb25zdCBkbHFVcmwgPSBgaHR0cHM6Ly9zcXMuJHtyZWdpb259LmFtYXpvbmF3cy5jb20vJHthY2NvdW50fS8ke2RscU5hbWV9YDtcbiAgcmV0dXJuIHsgcmVnaW9uLCBtYWluQXJuOiBtYWluUXVldWVBcm4sIGRscUFybiwgZGxxVXJsIH07XG59XG5cbi8qKiBTbmFwc2hvdCB0aGUgRExRIGRlcHRoICsgdGhlIGFnZSAoc2Vjb25kcykgb2YgaXRzIG9sZGVzdCBtZXNzYWdlIChmb3IgdGhlXG4gKiAgcG9pc29uLW1lc3NhZ2UgcmVkcml2ZSBjZWlsaW5nKS4gTWlzc2luZy9OYU4gYXR0cmlidXRlcyBmYWxsIGJhY2sgdG8gMC4gKi9cbmFzeW5jIGZ1bmN0aW9uIGRscVN0YXRzKHJlZ2lvbjogc3RyaW5nLCBkbHFVcmw6IHN0cmluZyk6IFByb21pc2U8eyBkZXB0aDogbnVtYmVyOyBvbGRlc3RBZ2VTZWM6IG51bWJlciB9PiB7XG4gIGlmICghc3FzTW9kKSBzcXNNb2QgPSBhd2FpdCBsb2FkU2RrPFNxc01vZHVsZT4oJ0Bhd3Mtc2RrL2NsaWVudC1zcXMnKTtcbiAgY29uc3QgY2xpZW50ID0gYXdhaXQgc3FzQ2xpZW50KHJlZ2lvbik7XG4gIGNvbnN0IG91dCA9IGF3YWl0IGNsaWVudC5zZW5kKG5ldyBzcXNNb2QuR2V0UXVldWVBdHRyaWJ1dGVzQ29tbWFuZCh7XG4gICAgUXVldWVVcmw6IGRscVVybCxcbiAgICBBdHRyaWJ1dGVOYW1lczogWydBcHByb3hpbWF0ZU51bWJlck9mTWVzc2FnZXMnLCAnQXBwcm94aW1hdGVBZ2VPZk9sZGVzdE1lc3NhZ2UnXSxcbiAgfSkpIGFzIHsgQXR0cmlidXRlcz86IHsgQXBwcm94aW1hdGVOdW1iZXJPZk1lc3NhZ2VzPzogc3RyaW5nOyBBcHByb3hpbWF0ZUFnZU9mT2xkZXN0TWVzc2FnZT86IHN0cmluZyB9IH07XG4gIGNvbnN0IG4gPSBOdW1iZXIob3V0LkF0dHJpYnV0ZXM/LkFwcHJveGltYXRlTnVtYmVyT2ZNZXNzYWdlcyA/PyAnMCcpO1xuICBjb25zdCBhZ2UgPSBOdW1iZXIob3V0LkF0dHJpYnV0ZXM/LkFwcHJveGltYXRlQWdlT2ZPbGRlc3RNZXNzYWdlID8/ICcwJyk7XG4gIHJldHVybiB7XG4gICAgZGVwdGg6IE51bWJlci5pc0Zpbml0ZShuKSA/IG4gOiAwLFxuICAgIG9sZGVzdEFnZVNlYzogTnVtYmVyLmlzRmluaXRlKGFnZSkgPyBhZ2UgOiAwLFxuICB9O1xufVxuXG5hc3luYyBmdW5jdGlvbiBoYXNBY3RpdmVNb3ZlVGFzayhyZWdpb246IHN0cmluZywgZGxxQXJuOiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgaWYgKCFzcXNNb2QpIHNxc01vZCA9IGF3YWl0IGxvYWRTZGs8U3FzTW9kdWxlPignQGF3cy1zZGsvY2xpZW50LXNxcycpO1xuICBjb25zdCBjbGllbnQgPSBhd2FpdCBzcXNDbGllbnQocmVnaW9uKTtcbiAgY29uc3Qgb3V0ID0gYXdhaXQgY2xpZW50LnNlbmQobmV3IHNxc01vZC5MaXN0TWVzc2FnZU1vdmVUYXNrc0NvbW1hbmQoeyBTb3VyY2VBcm46IGRscUFybiB9KSkgYXMge1xuICAgIFJlc3VsdHM/OiBBcnJheTx7IFN0YXR1cz86IHN0cmluZyB9PjtcbiAgfTtcbiAgcmV0dXJuIChvdXQuUmVzdWx0cyA/PyBbXSkuc29tZSh0ID0+IHQuU3RhdHVzID09PSAnUlVOTklORycpO1xufVxuXG5hc3luYyBmdW5jdGlvbiBzdGFydE1vdmVUYXNrKHJlZ2lvbjogc3RyaW5nLCBkbHFBcm46IHN0cmluZywgbWFpbkFybjogc3RyaW5nKTogUHJvbWlzZTx2b2lkPiB7XG4gIGlmICghc3FzTW9kKSBzcXNNb2QgPSBhd2FpdCBsb2FkU2RrPFNxc01vZHVsZT4oJ0Bhd3Mtc2RrL2NsaWVudC1zcXMnKTtcbiAgY29uc3QgY2xpZW50ID0gYXdhaXQgc3FzQ2xpZW50KHJlZ2lvbik7XG4gIGF3YWl0IGNsaWVudC5zZW5kKG5ldyBzcXNNb2QuU3RhcnRNZXNzYWdlTW92ZVRhc2tDb21tYW5kKHsgU291cmNlQXJuOiBkbHFBcm4sIERlc3RpbmF0aW9uQXJuOiBtYWluQXJuIH0pKTtcbn1cblxuLyoqIFBPU1Qgb25lIG9yZydzIGluZ2VzdC1oZWFsdGggc2lnbmFsLiBSZXR1cm5zIHRydWUgb25seSBvbiBhIGNvbmZpcm1lZCAyeHggc28gdGhlXG4gKiAgY2FsbGVyIHJlc2V0cyB0aGF0IG9yZydzIGBmb3J3YXJkZWRgIGNvdW50ZXIgT05MWSB3aGVuIHRoZSBzaWduYWwgd2FzIGFjY2VwdGVkXG4gKiAgKGEgZHJvcHBlZC9mYWlsZWQgUE9TVCBtdXN0IG5vdCBzaWxlbnRseSB6ZXJvIHRoZSBhY2N1bXVsYXRlZCBjb3VudCkuICovXG5hc3luYyBmdW5jdGlvbiBwb3N0SW5nZXN0SGVhbHRoKGJhc2VVcmw6IHN0cmluZywgYm9keTogeyBvcmdJZD86IHN0cmluZzsgZm9yd2FyZGVkOiBudW1iZXI7IGRyb3BwZWQ6IG51bWJlcjsgbGFzdEV2ZW50QXQ/OiBzdHJpbmcgfSk6IFByb21pc2U8Ym9vbGVhbj4ge1xuICB0cnkge1xuICAgIGNvbnN0IHRva2VuID0gYXdhaXQgZ2V0QXV0aFRva2VuKCk7XG4gICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goYCR7YmFzZVVybH0vYXBpL3JlcG9ydHMvaW5nZXN0LWhlYWx0aGAsIHtcbiAgICAgIG1ldGhvZDogJ1BPU1QnLFxuICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nLCAnQXV0aG9yaXphdGlvbic6IGBCZWFyZXIgJHt0b2tlbn1gIH0sXG4gICAgICBib2R5OiBKU09OLnN0cmluZ2lmeShib2R5KSxcbiAgICB9KTtcbiAgICBpZiAoIXJlcy5vaykge1xuICAgICAgbG9nLndhcm4oJ2luZ2VzdC1oZWFsdGggUE9TVCByZXR1cm5lZCBub24tMnh4IChpZ25vcmVkKScsIHsgc3RhdHVzOiByZXMuc3RhdHVzLCBvcmdJZDogYm9keS5vcmdJZCB9KTtcbiAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG4gIH0gY2F0Y2ggKGVycikge1xuICAgIGxvZy53YXJuKCdpbmdlc3QtaGVhbHRoIFBPU1QgZmFpbGVkIChpZ25vcmVkKScsIHsgZXJyb3I6IFN0cmluZyhlcnIpLCBvcmdJZDogYm9keS5vcmdJZCB9KTtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cbn1cblxuLyoqXG4gKiBUaHJvdHRsZWQsIGJlc3QtZWZmb3J0IHBvc3Qtc3VjY2VzcyB3b3JrOiBlbWl0IHBlci1vcmcgaW5nZXN0LWhlYWx0aCBhbmQgc2VsZi1oZWFsXG4gKiB0aGUgRExRLiBOZXZlciB0aHJvd3MuIGBmb3J3YXJkZWRgL2BsYXN0RXZlbnRBdGAgYWNjdW11bGF0ZSBQRVIgT1JHIGFjcm9zcyBiYXRjaGVzXG4gKiBiZXR3ZWVuIGhlYWx0aCBwb3N0czsgYGRyb3BwZWRgIGlzIHRoZSBzaGFyZWQgZmxlZXQgRExRIGRlcHRoIHNuYXBzaG90LiBBIHBlci1vcmdcbiAqIGNvdW50ZXIgaXMgemVyb2VkIG9ubHkgYWZ0ZXIgdGhhdCBvcmcncyBoZWFsdGggUE9TVCBpcyBjb25maXJtZWQgMnh4LlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVwb3J0SGVhbHRoQW5kUmVkcml2ZShiYXNlVXJsOiBzdHJpbmcsIG1haW5RdWV1ZUFybjogc3RyaW5nIHwgdW5kZWZpbmVkKTogUHJvbWlzZTx2b2lkPiB7XG4gIGNvbnN0IG5vdyA9IERhdGUubm93KCk7XG4gIGlmIChub3cgLSBsYXN0SGVhbHRoQXQgPCBIRUFMVEhfVEhST1RUTEVfTVMpIHJldHVybjtcbiAgbGFzdEhlYWx0aEF0ID0gbm93O1xuICB0cnkge1xuICAgIGNvbnN0IHEgPSBkZXJpdmVRdWV1ZXMobWFpblF1ZXVlQXJuKTtcbiAgICBsZXQgZGVwdGggPSAwO1xuICAgIGxldCBvbGRlc3RBZ2VTZWMgPSAwO1xuICAgIGlmIChxKSB7XG4gICAgICB0cnkge1xuICAgICAgICBjb25zdCBzdGF0cyA9IGF3YWl0IGRscVN0YXRzKHEucmVnaW9uLCBxLmRscVVybCk7XG4gICAgICAgIGRlcHRoID0gc3RhdHMuZGVwdGg7XG4gICAgICAgIG9sZGVzdEFnZVNlYyA9IHN0YXRzLm9sZGVzdEFnZVNlYztcbiAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICBsb2cud2FybignRExRIHN0YXRzIGNoZWNrIGZhaWxlZCAoaWdub3JlZCknLCB7IGVycm9yOiAoZXJyIGFzIHsgbmFtZT86IHN0cmluZyB9KT8ubmFtZSA/PyBTdHJpbmcoZXJyKSB9KTtcbiAgICAgIH1cbiAgICB9XG5cbiAgICAvLyBQb3N0IG9uZSBoZWFsdGggc2lnbmFsIHBlciByZXNvbHZlZCBvcmcsIGF0dHJpYnV0aW5nIGl0cyBvd24gZm9yd2FyZGVkIGNvdW50ICtcbiAgICAvLyBsYXN0IGV2ZW50IHRpbWUuIGBkcm9wcGVkYCAodGhlIHNoYXJlZCBmbGVldCBETFEgZGVwdGgpIHJpZGVzIGFsb25nIG9uIGVhY2guXG4gICAgLy8gU25hcHNob3QgdGhlIGtleXMgZmlyc3Qgc28gd2UgZG9uJ3QgbXV0YXRlIHRoZSBtYXAgd2hpbGUgaXRlcmF0aW5nLlxuICAgIGNvbnN0IG9yZ0tleXMgPSBuZXcgU2V0KFsuLi5mb3J3YXJkZWRCeU9yZy5rZXlzKCksIC4uLmxhc3RFdmVudEF0QnlPcmcua2V5cygpXSk7XG4gICAgaWYgKG9yZ0tleXMuc2l6ZSA9PT0gMCkgb3JnS2V5cy5hZGQoJycpOyAvLyBub3RoaW5nIGZvcndhcmRlZCB5ZXQg4oaSIHN0aWxsIHJlcG9ydCBkZXB0aFxuICAgIGZvciAoY29uc3Qga2V5IG9mIG9yZ0tleXMpIHtcbiAgICAgIGNvbnN0IGZvcndhcmRlZCA9IGZvcndhcmRlZEJ5T3JnLmdldChrZXkpID8/IDA7XG4gICAgICBjb25zdCBsYXN0RXZlbnRBdCA9IGxhc3RFdmVudEF0QnlPcmcuZ2V0KGtleSk7XG4gICAgICBjb25zdCBvayA9IGF3YWl0IHBvc3RJbmdlc3RIZWFsdGgoYmFzZVVybCwge1xuICAgICAgICAuLi4oa2V5ID8geyBvcmdJZDoga2V5IH0gOiB7fSksXG4gICAgICAgIGZvcndhcmRlZCxcbiAgICAgICAgZHJvcHBlZDogZGVwdGgsXG4gICAgICAgIGxhc3RFdmVudEF0LFxuICAgICAgfSk7XG4gICAgICAvLyBSZXNldCB0aGUgZm9yd2FyZGVkIGNvdW50ZXIgT05MWSBvbiBhIGNvbmZpcm1lZCAyeHgg4oCUIGEgZmFpbGVkL2Ryb3BwZWQgUE9TVFxuICAgICAgLy8ga2VlcHMgdGhlIGNvdW50IHNvIHRoZSBuZXh0IHRpY2sgcmUtcmVwb3J0cyBpdCBpbnN0ZWFkIG9mIGxvc2luZyBpdC5cbiAgICAgIGlmIChvaykgZm9yd2FyZGVkQnlPcmcuc2V0KGtleSwgMCk7XG4gICAgfVxuXG4gICAgaWYgKHEgJiYgZGVwdGggPiAwICYmIG5vdyAtIGxhc3RSZWRyaXZlQXQgPj0gUkVEUklWRV9USFJPVFRMRV9NUykge1xuICAgICAgLy8gUG9pc29uLW1lc3NhZ2UgY2VpbGluZzogbmV2ZXIgcmVkcml2ZSBhIERMUSB3aG9zZSBvbGRlc3QgbWVzc2FnZSBoYXMgYWdlZFxuICAgICAgLy8gcGFzdCB0aGUgY2VpbGluZyDigJQgdGhhdCBtZXNzYWdlIGtlZXBzIGZhaWxpbmcgYW5kIHdvdWxkIGxvb3AgRExR4oaSbWFpbuKGkkRMUS5cbiAgICAgIGlmIChvbGRlc3RBZ2VTZWMgPiBNQVhfRExRX1JFRFJJVkVfQUdFX1NFQykge1xuICAgICAgICBsb2cud2FybignRExRIG9sZGVzdCBtZXNzYWdlIGV4Y2VlZHMgcG9pc29uLWFnZSBjZWlsaW5nIOKAlCBza2lwcGluZyByZWRyaXZlIChuZWVkcyBtYW51YWwgYXR0ZW50aW9uKScsIHtcbiAgICAgICAgICBkbHFBcm46IHEuZGxxQXJuLCBkcm9wcGVkOiBkZXB0aCwgb2xkZXN0QWdlU2VjLCBjZWlsaW5nU2VjOiBNQVhfRExRX1JFRFJJVkVfQUdFX1NFQyxcbiAgICAgICAgfSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0cnkge1xuICAgICAgICAgIGlmICghKGF3YWl0IGhhc0FjdGl2ZU1vdmVUYXNrKHEucmVnaW9uLCBxLmRscUFybikpKSB7XG4gICAgICAgICAgICBsYXN0UmVkcml2ZUF0ID0gbm93O1xuICAgICAgICAgICAgYXdhaXQgc3RhcnRNb3ZlVGFzayhxLnJlZ2lvbiwgcS5kbHFBcm4sIHEubWFpbkFybik7XG4gICAgICAgICAgICBsb2cuaW5mbygnU3RhcnRlZCBzZWxmLWhlYWxpbmcgRExR4oaSbWFpbiByZWRyaXZlJywgeyBkbHFBcm46IHEuZGxxQXJuLCBkcm9wcGVkOiBkZXB0aCwgb2xkZXN0QWdlU2VjIH0pO1xuICAgICAgICAgIH1cbiAgICAgICAgfSBjYXRjaCAoZXJyKSB7XG4gICAgICAgICAgbG9nLndhcm4oJ0RMUSByZWRyaXZlIGZhaWxlZCAoaWdub3JlZCknLCB7IGVycm9yOiAoZXJyIGFzIHsgbmFtZT86IHN0cmluZyB9KT8ubmFtZSA/PyBTdHJpbmcoZXJyKSB9KTtcbiAgICAgICAgfVxuICAgICAgfVxuICAgIH1cbiAgfSBjYXRjaCAoZXJyKSB7XG4gICAgbG9nLndhcm4oJ2hlYWx0aC9yZWRyaXZlIHN0ZXAgZmFpbGVkIChpZ25vcmVkKScsIHsgZXJyb3I6IFN0cmluZyhlcnIpIH0pO1xuICB9XG59XG5cbi8qKiBDb3VudCBhIGZvcndhcmRlZCBiYXRjaCB0b3dhcmQgZWFjaCByZXNvbHZlZCBvcmcncyBkZWxpdmVyeS1oZWFsdGggcmVwb3J0LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJlY29yZEZvcndhcmRlZChldmVudHM6IFJlYWRvbmx5QXJyYXk8eyBvcmdJZDogc3RyaW5nIHwgbnVsbDsgY29tcGxldGVkQXQ/OiBzdHJpbmc7IHN0YXJ0ZWRBdD86IHN0cmluZyB9Pik6IHZvaWQge1xuICBmb3IgKGNvbnN0IGUgb2YgZXZlbnRzKSB7XG4gICAgY29uc3Qga2V5ID0gZS5vcmdJZCA/PyAnJztcbiAgICBmb3J3YXJkZWRCeU9yZy5zZXQoa2V5LCAoZm9yd2FyZGVkQnlPcmcuZ2V0KGtleSkgPz8gMCkgKyAxKTtcbiAgICBjb25zdCBhdCA9IGUuY29tcGxldGVkQXQgfHwgZS5zdGFydGVkQXQ7XG4gICAgaWYgKGF0KSB7XG4gICAgICBjb25zdCBwcmV2ID0gbGFzdEV2ZW50QXRCeU9yZy5nZXQoa2V5KTtcbiAgICAgIGlmICghcHJldiB8fCBhdCA+IHByZXYpIGxhc3RFdmVudEF0QnlPcmcuc2V0KGtleSwgYXQpO1xuICAgIH1cbiAgfVxufVxuXG4vKiogQGludGVybmFsIFRlc3Qtb25seTogZm9yZ2V0IGhlYWx0aCBjb3VudGVycyBhbmQgdGhyb3R0bGVzLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIF9yZXNldEhlYWx0aEZvclRlc3RzKCk6IHZvaWQge1xuICBmb3J3YXJkZWRCeU9yZy5jbGVhcigpO1xuICBsYXN0RXZlbnRBdEJ5T3JnLmNsZWFyKCk7XG4gIGxhc3RIZWFsdGhBdCA9IDA7XG4gIGxhc3RSZWRyaXZlQXQgPSAwO1xufVxuIl19
package/lib/index.d.ts CHANGED
@@ -1,2 +1,27 @@
1
1
  import type { SQSEvent } from 'aws-lambda';
2
+ /**
3
+ * Pipeline event ingestion Lambda handler.
4
+ *
5
+ * Receives CodePipeline/CodeBuild events from SQS (sourced by EventBridge),
6
+ * parses them into a normalized format, and POSTs them to the reporting service
7
+ * via PLATFORM_BASE_URL.
8
+ *
9
+ * Authentication (service-account keys):
10
+ * The Lambda holds an opaque SERVICE-ACCOUNT KEY (`pb_sa_…`), never a JWT, and
11
+ * trades it at platform's `/auth/token/exchange` for a 5-minute token per batch.
12
+ * It verifies NOTHING locally: an opaque key carries no claims to check, and the
13
+ * exchange is the authority (a revoked key stops working within one token
14
+ * lifetime, which is the entire point of the shape). The key is minted by
15
+ * `pipeline-manager infra store-token` and rotated in place by the token-renew
16
+ * Lambda, so the secret's value changes underneath a warm container — which is
17
+ * why an auth failure re-reads the SECRET, not just the token.
18
+ *
19
+ * Environment variables:
20
+ * - PLATFORM_BASE_URL — Base URL of the platform
21
+ * - PLATFORM_ACCESS_KEY — the `pb_sa_…` key set directly (no Secrets Manager call), or
22
+ * - PLATFORM_SECRET_NAME — Secrets Manager secret containing { password: <pb_sa_ key> }
23
+ * - EVENT_DLQ_ARN — (optional) override for the dead-letter queue ARN used by the
24
+ * self-healing redrive; defaults to `<main-queue-arn>-dlq` derived from the SQS
25
+ * trigger's eventSourceARN.
26
+ */
2
27
  export declare const handler: (event: SQSEvent) => Promise<void>;