@astrasyncai/verification-gateway 4.0.0 → 4.2.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/README.md +106 -29
- package/dist/adapter-interface/interface.d.mts +2 -2
- package/dist/adapter-interface/interface.d.ts +2 -2
- package/dist/adapters/express.d.mts +2 -2
- package/dist/adapters/express.d.ts +2 -2
- package/dist/adapters/express.js +398 -15
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +398 -15
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/http-pdlss.d.mts +3 -3
- package/dist/adapters/http-pdlss.d.ts +3 -3
- package/dist/adapters/http-pdlss.js.map +1 -1
- package/dist/adapters/http-pdlss.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +56 -36
- package/dist/adapters/mcp.d.ts +56 -36
- package/dist/adapters/mcp.js +220 -3
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +220 -3
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +2 -2
- package/dist/adapters/nextjs.d.ts +2 -2
- package/dist/adapters/nextjs.js +433 -18
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +433 -18
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +2 -2
- package/dist/adapters/sdk.d.ts +2 -2
- package/dist/adapters/sdk.js +2 -2
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +2 -2
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.d.mts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +13 -13
- package/dist/bin/astrasync-codex-hook.js +13 -13
- package/dist/bin/astrasync-guard.js +100 -13
- package/dist/bin/astrasync.js +168 -27
- package/dist/browser/background.js +13 -13
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +13 -13
- package/dist/browser/background.mjs.map +1 -1
- package/dist/browser/browser-adapter.d.mts +2 -2
- package/dist/browser/browser-adapter.d.ts +2 -2
- package/dist/claude-code/claude-code-adapter.d.mts +2 -2
- package/dist/claude-code/claude-code-adapter.d.ts +2 -2
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/codex/index.d.mts +3 -3
- package/dist/codex/index.d.ts +3 -3
- package/dist/codex/index.js +24 -13
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +24 -13
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/cursor-adapter.d.mts +2 -2
- package/dist/cursor/cursor-adapter.d.ts +2 -2
- package/dist/cursor/extension.d.mts +2 -2
- package/dist/cursor/extension.d.ts +2 -2
- package/dist/cursor/extension.js +13 -13
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +13 -13
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +22 -7
- package/dist/edge-config.d.ts +22 -7
- package/dist/edge-config.js.map +1 -1
- package/dist/edge-config.mjs.map +1 -1
- package/dist/edge-core/index.d.mts +425 -0
- package/dist/edge-core/index.d.ts +425 -0
- package/dist/edge-core/index.js +1482 -0
- package/dist/edge-core/index.js.map +1 -0
- package/dist/edge-core/index.mjs +1437 -0
- package/dist/edge-core/index.mjs.map +1 -0
- package/dist/{express-BZajRs4i.d.mts → express-BVd1_3FE.d.ts} +9 -7
- package/dist/{express-CjCUB1k7.d.ts → express-D_4hTn5Z.d.mts} +9 -7
- package/dist/gateway/gateway.d.mts +2 -2
- package/dist/gateway/gateway.d.ts +2 -2
- package/dist/gateway/gateway.js +13 -13
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +13 -13
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +2 -2
- package/dist/git-trigger/git-hooks.d.ts +2 -2
- package/dist/{index-adgKhujR.d.ts → index-B0YHu_SP.d.ts} +30 -27
- package/dist/{index-B0cyMZDL.d.mts → index-BPEBlOsE.d.mts} +30 -27
- package/dist/{index-BRYteOqO.d.mts → index-DQb5-_1X.d.mts} +1 -1
- package/dist/{index-BZQdVqNw.d.ts → index-T1aBoUcc.d.ts} +1 -1
- package/dist/index.d.mts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +477 -265
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +474 -265
- package/dist/index.mjs.map +1 -1
- package/dist/local-evaluator/evaluator.d.mts +2 -2
- package/dist/local-evaluator/evaluator.d.ts +2 -2
- package/dist/metadata-capture.d.mts +63 -4
- package/dist/metadata-capture.d.ts +63 -4
- package/dist/metadata-capture.js +67 -0
- package/dist/metadata-capture.js.map +1 -1
- package/dist/metadata-capture.mjs +64 -0
- package/dist/metadata-capture.mjs.map +1 -1
- package/dist/{nextjs-DeUTnOBd.d.mts → nextjs-WyeVr2Kp.d.mts} +3 -3
- package/dist/{nextjs-D_iFf5k-.d.ts → nextjs-w7RTbP1P.d.ts} +3 -3
- package/dist/platform-signatures.d.mts +5 -6
- package/dist/platform-signatures.d.ts +5 -6
- package/dist/platform-signatures.js.map +1 -1
- package/dist/platform-signatures.mjs.map +1 -1
- package/dist/registration/index.d.mts +10 -11
- package/dist/registration/index.d.ts +10 -11
- package/dist/registration/index.js.map +1 -1
- package/dist/registration/index.mjs.map +1 -1
- package/dist/{sdk-BM3mwEAq.d.ts → sdk-CbTNIkAa.d.mts} +4 -4
- package/dist/{sdk-_il1Q41f.d.mts → sdk-QeX0Z8Ho.d.ts} +4 -4
- package/dist/transport/index.d.mts +2 -2
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-CHKCgDFC.d.ts → types-BLUx92FJ.d.ts} +1 -1
- package/dist/{types-DfnP7JiM.d.mts → types-DqfPU5Bl.d.mts} +1 -1
- package/dist/{types-BcDf9Kk0.d.mts → types-r850cOt0.d.mts} +106 -88
- package/dist/{types-fTQD448H.d.ts → types-sMxBT-nM.d.ts} +106 -88
- package/dist/ui/index.d.mts +11 -7
- package/dist/ui/index.d.ts +11 -7
- package/dist/ui/index.js +13 -9
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/index.mjs +10 -8
- package/dist/ui/index.mjs.map +1 -1
- package/dist/verify.d.mts +8 -7
- package/dist/verify.d.ts +8 -7
- package/dist/verify.js +1 -1
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +1 -1
- package/dist/verify.mjs.map +1 -1
- package/package.json +8 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-
|
|
2
|
-
import '../types-
|
|
1
|
+
import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-DqfPU5Bl.mjs';
|
|
2
|
+
import '../types-r850cOt0.mjs';
|
|
3
3
|
import '../metadata-capture.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-
|
|
2
|
-
import '../types-
|
|
1
|
+
import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-BLUx92FJ.js';
|
|
2
|
+
import '../types-sMxBT-nM.js';
|
|
3
3
|
import '../metadata-capture.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -19,6 +19,20 @@
|
|
|
19
19
|
* ZERO verify-access decision latency. Detection (which few signals imply which
|
|
20
20
|
* vendor) is a separate, curated concern — see `platform-signatures.ts`.
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Version of the capture semantics (what is kept, dropped, reduced, or
|
|
24
|
+
* derived — and under which key names).
|
|
25
|
+
*
|
|
26
|
+
* Capture semantics are FROZEN within a package major version: a given
|
|
27
|
+
* `schemaVersion` always means the same field vocabulary and the same
|
|
28
|
+
* keep/drop/reduce rules. The number bumps only on a SEMANTIC change to
|
|
29
|
+
* capture (a renamed key, a changed reduction rule) — never for additive
|
|
30
|
+
* signals, and never merely because the package version moved. To tell
|
|
31
|
+
* builds apart, use the `sdkVersion` already present on the verify body;
|
|
32
|
+
* `schemaVersion` answers the different question "how do I interpret this
|
|
33
|
+
* stored metadata?".
|
|
34
|
+
*/
|
|
35
|
+
declare const CAPTURE_SCHEMA_VERSION = 1;
|
|
22
36
|
/**
|
|
23
37
|
* The captured, sanitised view of an inbound agent's request metadata. Attached
|
|
24
38
|
* to verify-access / beacon payloads as `callerMetadata.observedMetadata`.
|
|
@@ -40,11 +54,20 @@ interface ObservedMetadata {
|
|
|
40
54
|
*/
|
|
41
55
|
platformHeaders?: Record<string, string>;
|
|
42
56
|
/**
|
|
43
|
-
* Connection-layer signals
|
|
44
|
-
*
|
|
45
|
-
*
|
|
57
|
+
* Connection-layer signals (IP, ASN, country, TLS version, HTTP version,
|
|
58
|
+
* device class, TLS fingerprint). Edge adapters populate this from the
|
|
59
|
+
* platform's native connection surface — the authoritative source. SDK
|
|
60
|
+
* adapters populate it as a fallback via {@link deriveConnectionFromHeaders}
|
|
61
|
+
* when a CDN in front of the merchant injected the equivalent headers;
|
|
62
|
+
* absent when neither source had anything.
|
|
46
63
|
*/
|
|
47
64
|
connection?: Record<string, string>;
|
|
65
|
+
/**
|
|
66
|
+
* Version of the capture semantics this object was produced under — see
|
|
67
|
+
* {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with
|
|
68
|
+
* metadata captured before the field existed.
|
|
69
|
+
*/
|
|
70
|
+
schemaVersion?: number;
|
|
48
71
|
}
|
|
49
72
|
/**
|
|
50
73
|
* Result of sanitising a raw header map. `headers` is always present (possibly
|
|
@@ -83,5 +106,41 @@ declare function sanitizeHeaders(raw: Record<string, string | string[] | undefin
|
|
|
83
106
|
* want only the platform subset. Never re-run over raw (unsanitised) headers.
|
|
84
107
|
*/
|
|
85
108
|
declare function extractPlatformHeaders(sanitized: Record<string, string> | undefined | null): Record<string, string>;
|
|
109
|
+
/**
|
|
110
|
+
* Derive the connection-layer block from CDN-injected request headers.
|
|
111
|
+
*
|
|
112
|
+
* When a merchant's SDK-gated origin sits behind a CDN (Cloudflare, Fastly,
|
|
113
|
+
* Vercel, CloudFront), the CDN has already seen the connection layer and
|
|
114
|
+
* injected it as headers. This promotes those headers to the same
|
|
115
|
+
* `connection` keys the edge adapters populate natively, so SDK-only
|
|
116
|
+
* deployments still get IP / geo / TLS attribution. Precedence per key:
|
|
117
|
+
*
|
|
118
|
+
* - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >
|
|
119
|
+
* `x-real-ip` > leftmost `x-forwarded-for`
|
|
120
|
+
* - `country`: `cf-ipcountry` > `x-vercel-ip-country` >
|
|
121
|
+
* `cloudfront-viewer-country` > `fastly-geo-country`
|
|
122
|
+
* - `region` / `city`: Vercel then CloudFront viewer headers
|
|
123
|
+
* - `asn` / `tlsVersion` / `httpVersion`: `cloudfront-viewer-*`
|
|
124
|
+
* - `tlsFingerprint` / `ja4`: Cloudflare `cf-ja3-hash` / `cf-ja4`
|
|
125
|
+
*
|
|
126
|
+
* Edge adapters keep their richer native collectors — native platform
|
|
127
|
+
* signals are authoritative; this header derivation is the SDK-level
|
|
128
|
+
* fallback. Header names are matched case-insensitively. Returns undefined
|
|
129
|
+
* when no source header is present, so the `connection` key is simply
|
|
130
|
+
* omitted (matching edge behaviour).
|
|
131
|
+
*/
|
|
132
|
+
declare function deriveConnectionFromHeaders(headers: Record<string, string> | undefined | null): Record<string, string> | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* The FULL metadata capture for one request at an SDK adapter (express /
|
|
135
|
+
* nextjs / mcp): sanitised headers, the safe credential format prefix, the
|
|
136
|
+
* platform-signal subset, the CDN-derived connection block, and the capture
|
|
137
|
+
* schema version. This is the ONE builder all SDK adapters share, so their
|
|
138
|
+
* verify-access bodies agree byte-for-byte.
|
|
139
|
+
*
|
|
140
|
+
* Connection derivation runs over the RAW header map (pre-cap) — the size
|
|
141
|
+
* caps guard storage growth and must never hide a geo/IP header that
|
|
142
|
+
* happened to arrive late in an oversized map.
|
|
143
|
+
*/
|
|
144
|
+
declare function buildSdkObservedMetadata(rawHeaders: Record<string, string | string[] | undefined> | undefined | null): ObservedMetadata;
|
|
86
145
|
|
|
87
|
-
export { MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
|
|
146
|
+
export { CAPTURE_SCHEMA_VERSION, MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, buildSdkObservedMetadata, deriveConnectionFromHeaders, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
|
|
@@ -19,6 +19,20 @@
|
|
|
19
19
|
* ZERO verify-access decision latency. Detection (which few signals imply which
|
|
20
20
|
* vendor) is a separate, curated concern — see `platform-signatures.ts`.
|
|
21
21
|
*/
|
|
22
|
+
/**
|
|
23
|
+
* Version of the capture semantics (what is kept, dropped, reduced, or
|
|
24
|
+
* derived — and under which key names).
|
|
25
|
+
*
|
|
26
|
+
* Capture semantics are FROZEN within a package major version: a given
|
|
27
|
+
* `schemaVersion` always means the same field vocabulary and the same
|
|
28
|
+
* keep/drop/reduce rules. The number bumps only on a SEMANTIC change to
|
|
29
|
+
* capture (a renamed key, a changed reduction rule) — never for additive
|
|
30
|
+
* signals, and never merely because the package version moved. To tell
|
|
31
|
+
* builds apart, use the `sdkVersion` already present on the verify body;
|
|
32
|
+
* `schemaVersion` answers the different question "how do I interpret this
|
|
33
|
+
* stored metadata?".
|
|
34
|
+
*/
|
|
35
|
+
declare const CAPTURE_SCHEMA_VERSION = 1;
|
|
22
36
|
/**
|
|
23
37
|
* The captured, sanitised view of an inbound agent's request metadata. Attached
|
|
24
38
|
* to verify-access / beacon payloads as `callerMetadata.observedMetadata`.
|
|
@@ -40,11 +54,20 @@ interface ObservedMetadata {
|
|
|
40
54
|
*/
|
|
41
55
|
platformHeaders?: Record<string, string>;
|
|
42
56
|
/**
|
|
43
|
-
* Connection-layer signals
|
|
44
|
-
*
|
|
45
|
-
*
|
|
57
|
+
* Connection-layer signals (IP, ASN, country, TLS version, HTTP version,
|
|
58
|
+
* device class, TLS fingerprint). Edge adapters populate this from the
|
|
59
|
+
* platform's native connection surface — the authoritative source. SDK
|
|
60
|
+
* adapters populate it as a fallback via {@link deriveConnectionFromHeaders}
|
|
61
|
+
* when a CDN in front of the merchant injected the equivalent headers;
|
|
62
|
+
* absent when neither source had anything.
|
|
46
63
|
*/
|
|
47
64
|
connection?: Record<string, string>;
|
|
65
|
+
/**
|
|
66
|
+
* Version of the capture semantics this object was produced under — see
|
|
67
|
+
* {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with
|
|
68
|
+
* metadata captured before the field existed.
|
|
69
|
+
*/
|
|
70
|
+
schemaVersion?: number;
|
|
48
71
|
}
|
|
49
72
|
/**
|
|
50
73
|
* Result of sanitising a raw header map. `headers` is always present (possibly
|
|
@@ -83,5 +106,41 @@ declare function sanitizeHeaders(raw: Record<string, string | string[] | undefin
|
|
|
83
106
|
* want only the platform subset. Never re-run over raw (unsanitised) headers.
|
|
84
107
|
*/
|
|
85
108
|
declare function extractPlatformHeaders(sanitized: Record<string, string> | undefined | null): Record<string, string>;
|
|
109
|
+
/**
|
|
110
|
+
* Derive the connection-layer block from CDN-injected request headers.
|
|
111
|
+
*
|
|
112
|
+
* When a merchant's SDK-gated origin sits behind a CDN (Cloudflare, Fastly,
|
|
113
|
+
* Vercel, CloudFront), the CDN has already seen the connection layer and
|
|
114
|
+
* injected it as headers. This promotes those headers to the same
|
|
115
|
+
* `connection` keys the edge adapters populate natively, so SDK-only
|
|
116
|
+
* deployments still get IP / geo / TLS attribution. Precedence per key:
|
|
117
|
+
*
|
|
118
|
+
* - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >
|
|
119
|
+
* `x-real-ip` > leftmost `x-forwarded-for`
|
|
120
|
+
* - `country`: `cf-ipcountry` > `x-vercel-ip-country` >
|
|
121
|
+
* `cloudfront-viewer-country` > `fastly-geo-country`
|
|
122
|
+
* - `region` / `city`: Vercel then CloudFront viewer headers
|
|
123
|
+
* - `asn` / `tlsVersion` / `httpVersion`: `cloudfront-viewer-*`
|
|
124
|
+
* - `tlsFingerprint` / `ja4`: Cloudflare `cf-ja3-hash` / `cf-ja4`
|
|
125
|
+
*
|
|
126
|
+
* Edge adapters keep their richer native collectors — native platform
|
|
127
|
+
* signals are authoritative; this header derivation is the SDK-level
|
|
128
|
+
* fallback. Header names are matched case-insensitively. Returns undefined
|
|
129
|
+
* when no source header is present, so the `connection` key is simply
|
|
130
|
+
* omitted (matching edge behaviour).
|
|
131
|
+
*/
|
|
132
|
+
declare function deriveConnectionFromHeaders(headers: Record<string, string> | undefined | null): Record<string, string> | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* The FULL metadata capture for one request at an SDK adapter (express /
|
|
135
|
+
* nextjs / mcp): sanitised headers, the safe credential format prefix, the
|
|
136
|
+
* platform-signal subset, the CDN-derived connection block, and the capture
|
|
137
|
+
* schema version. This is the ONE builder all SDK adapters share, so their
|
|
138
|
+
* verify-access bodies agree byte-for-byte.
|
|
139
|
+
*
|
|
140
|
+
* Connection derivation runs over the RAW header map (pre-cap) — the size
|
|
141
|
+
* caps guard storage growth and must never hide a geo/IP header that
|
|
142
|
+
* happened to arrive late in an oversized map.
|
|
143
|
+
*/
|
|
144
|
+
declare function buildSdkObservedMetadata(rawHeaders: Record<string, string | string[] | undefined> | undefined | null): ObservedMetadata;
|
|
86
145
|
|
|
87
|
-
export { MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
|
|
146
|
+
export { CAPTURE_SCHEMA_VERSION, MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, buildSdkObservedMetadata, deriveConnectionFromHeaders, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
|
package/dist/metadata-capture.js
CHANGED
|
@@ -20,14 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/metadata-capture.ts
|
|
21
21
|
var metadata_capture_exports = {};
|
|
22
22
|
__export(metadata_capture_exports, {
|
|
23
|
+
CAPTURE_SCHEMA_VERSION: () => CAPTURE_SCHEMA_VERSION,
|
|
23
24
|
MAX_HEADERS: () => MAX_HEADERS,
|
|
24
25
|
MAX_HEADER_VALUE_BYTES: () => MAX_HEADER_VALUE_BYTES,
|
|
25
26
|
MAX_TOTAL_BYTES: () => MAX_TOTAL_BYTES,
|
|
27
|
+
buildSdkObservedMetadata: () => buildSdkObservedMetadata,
|
|
28
|
+
deriveConnectionFromHeaders: () => deriveConnectionFromHeaders,
|
|
26
29
|
extractApiKeyFormat: () => extractApiKeyFormat,
|
|
27
30
|
extractPlatformHeaders: () => extractPlatformHeaders,
|
|
28
31
|
sanitizeHeaders: () => sanitizeHeaders
|
|
29
32
|
});
|
|
30
33
|
module.exports = __toCommonJS(metadata_capture_exports);
|
|
34
|
+
var CAPTURE_SCHEMA_VERSION = 1;
|
|
31
35
|
var SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
32
36
|
"cookie",
|
|
33
37
|
"set-cookie",
|
|
@@ -172,11 +176,74 @@ function extractPlatformHeaders(sanitized) {
|
|
|
172
176
|
}
|
|
173
177
|
return out;
|
|
174
178
|
}
|
|
179
|
+
var CONNECTION_HEADER_SOURCES = [
|
|
180
|
+
["ip", ["cf-connecting-ip", "fastly-client-ip", "true-client-ip", "x-real-ip"]],
|
|
181
|
+
[
|
|
182
|
+
"country",
|
|
183
|
+
["cf-ipcountry", "x-vercel-ip-country", "cloudfront-viewer-country", "fastly-geo-country"]
|
|
184
|
+
],
|
|
185
|
+
["region", ["x-vercel-ip-country-region", "cloudfront-viewer-country-region"]],
|
|
186
|
+
["city", ["x-vercel-ip-city", "cloudfront-viewer-city"]],
|
|
187
|
+
["asn", ["cloudfront-viewer-asn"]],
|
|
188
|
+
["tlsVersion", ["cloudfront-viewer-tls"]],
|
|
189
|
+
["httpVersion", ["cloudfront-viewer-http-version"]],
|
|
190
|
+
// TLS fingerprints can only be computed where TLS terminates. When a CDN
|
|
191
|
+
// terminates TLS in front of the merchant, its fingerprint headers ARE the
|
|
192
|
+
// connection-layer truth — normalising them here is the permanent design.
|
|
193
|
+
["tlsFingerprint", ["cf-ja3-hash"]],
|
|
194
|
+
["ja4", ["cf-ja4"]]
|
|
195
|
+
];
|
|
196
|
+
function deriveConnectionFromHeaders(headers) {
|
|
197
|
+
if (!headers) return void 0;
|
|
198
|
+
const lower = {};
|
|
199
|
+
for (const name of Object.keys(headers)) {
|
|
200
|
+
const value = headers[name];
|
|
201
|
+
if (typeof value === "string" && value.length > 0) lower[name.toLowerCase()] = value;
|
|
202
|
+
}
|
|
203
|
+
const connection = {};
|
|
204
|
+
for (const [key, sources] of CONNECTION_HEADER_SOURCES) {
|
|
205
|
+
for (const source of sources) {
|
|
206
|
+
const value = lower[source]?.trim();
|
|
207
|
+
if (value) {
|
|
208
|
+
connection[key] = value;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!connection.ip) {
|
|
214
|
+
const xff = lower["x-forwarded-for"];
|
|
215
|
+
const client = xff?.split(",")[0]?.trim();
|
|
216
|
+
if (client) connection.ip = client;
|
|
217
|
+
}
|
|
218
|
+
return Object.keys(connection).length > 0 ? connection : void 0;
|
|
219
|
+
}
|
|
220
|
+
function buildSdkObservedMetadata(rawHeaders) {
|
|
221
|
+
const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);
|
|
222
|
+
const flat = {};
|
|
223
|
+
if (rawHeaders) {
|
|
224
|
+
for (const name of Object.keys(rawHeaders)) {
|
|
225
|
+
const value = rawHeaders[name];
|
|
226
|
+
if (value === void 0) continue;
|
|
227
|
+
flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const connection = deriveConnectionFromHeaders(flat);
|
|
231
|
+
return {
|
|
232
|
+
headers,
|
|
233
|
+
...apiKeyFormat && { apiKeyFormat },
|
|
234
|
+
...platformHeaders && { platformHeaders },
|
|
235
|
+
...connection && { connection },
|
|
236
|
+
schemaVersion: CAPTURE_SCHEMA_VERSION
|
|
237
|
+
};
|
|
238
|
+
}
|
|
175
239
|
// Annotate the CommonJS export names for ESM import in node:
|
|
176
240
|
0 && (module.exports = {
|
|
241
|
+
CAPTURE_SCHEMA_VERSION,
|
|
177
242
|
MAX_HEADERS,
|
|
178
243
|
MAX_HEADER_VALUE_BYTES,
|
|
179
244
|
MAX_TOTAL_BYTES,
|
|
245
|
+
buildSdkObservedMetadata,
|
|
246
|
+
deriveConnectionFromHeaders,
|
|
180
247
|
extractApiKeyFormat,
|
|
181
248
|
extractPlatformHeaders,
|
|
182
249
|
sanitizeHeaders
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/metadata-capture.ts"],"sourcesContent":["/**\n * Maximal metadata capture — the shared, edge-safe sanitiser.\n *\n * AstraSync's thesis is total metadata capture: every header and connection\n * signal an inbound agent presents is a signal we want to keep. This module is\n * the ONE place that decides what is safe to store verbatim, what is a secret\n * to drop, and what credential headers reduce to a safe *format prefix* (a\n * platform signal, never the secret itself).\n *\n * It runs in three places that must agree byte-for-byte:\n * - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture\n * point (full CloudFront header arrays),\n * - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,\n * - (indirectly) the backend, which stores whatever the above forward.\n *\n * Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser\n * build, and Deno. Capture is cheap: at the edge and SDK it is local string\n * work, and the backend stores it on a fire-and-forget event insert, so it adds\n * ZERO verify-access decision latency. Detection (which few signals imply which\n * vendor) is a separate, curated concern — see `platform-signatures.ts`.\n */\n\n/**\n * The captured, sanitised view of an inbound agent's request metadata. Attached\n * to verify-access / beacon payloads as `callerMetadata.observedMetadata`.\n */\nexport interface ObservedMetadata {\n /** Header name → value, secrets removed, verbatim otherwise. */\n headers: Record<string, string>;\n /**\n * Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,\n * `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no\n * credential header was present or none matched a known format.\n */\n apiKeyFormat?: string;\n /**\n * The subset of `headers` that are known platform-signal headers\n * (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),\n * pulled out for convenient detection + display. Values are the same\n * sanitised strings as in `headers`.\n */\n platformHeaders?: Record<string, string>;\n /**\n * Connection-layer signals the edge can see but the SDK cannot (IP, ASN,\n * country, TLS version/cipher, HTTP version, device class). Populated by the\n * edge adapter's `collectConnectionMetadata`; absent for SDK-only capture.\n */\n connection?: Record<string, string>;\n}\n\n/**\n * Result of sanitising a raw header map. `headers` is always present (possibly\n * empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.\n */\nexport interface SanitizeHeadersResult {\n headers: Record<string, string>;\n apiKeyFormat?: string;\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * Header names dropped entirely — genuine secrets or our own credentials that\n * describe the request's transport, not the agent. Matched case-insensitively\n * on the exact header name.\n */\nconst SECRET_HEADER_NAMES = new Set([\n 'cookie',\n 'set-cookie',\n 'x-csrf-token',\n 'proxy-authorization',\n 'x-astrasync-signature',\n 'x-astrasync-secret',\n 'x-hub-signature',\n 'x-hub-signature-256',\n]);\n\n/**\n * Header-name prefixes dropped entirely — our own credential / signing surface\n * (`x-astrasync-*`, `kya-*`) must never round-trip into stored metadata.\n */\nconst SECRET_HEADER_PREFIXES = ['x-astrasync-', 'kya-', 'kya_'];\n\n/**\n * Credential headers whose VALUE is a secret but whose FORMAT is a signal. We\n * keep the leading format prefix (up to the random tail) and drop the rest.\n */\nconst CREDENTIAL_HEADER_NAMES = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'api-key',\n]);\n\n/**\n * Header names that carry a raw signature value (drop the value; the presence\n * is not worth a bloat risk). Matched as a suffix so `x-*-signature` variants\n * are covered without an exhaustive list.\n */\nfunction isSignatureHeader(name: string): boolean {\n return name === 'signature' || name.endsWith('-signature') || name.endsWith('-signature-256');\n}\n\n/**\n * Known platform-signal headers — pulled into `platformHeaders` for convenient\n * detection + display. Exact names plus a few prefixes (`x-goog-`,\n * `x-stainless-` — the OpenAI/Anthropic SDK client fingerprint headers).\n */\nconst PLATFORM_HEADER_NAMES = new Set([\n 'openai-organization',\n 'openai-project',\n 'openai-version',\n 'openai-processing-ms',\n 'anthropic-version',\n 'anthropic-beta',\n 'x-goog-user-project',\n 'x-goog-api-client',\n 'user-agent',\n 'x-agent-card-url',\n 'x-agent-provider',\n]);\nconst PLATFORM_HEADER_PREFIXES = ['x-goog-', 'x-stainless-', 'x-anthropic-', 'x-openai-'];\n\n/**\n * Known API-key format prefixes, longest-first so `sk-ant-api03` wins over\n * `sk-ant` and `sk-`. Each is the SAFE, non-secret leading token of a\n * credential; the random tail is never captured.\n */\nconst KEY_FORMAT_PREFIXES = [\n 'sk-ant-api03',\n 'sk-ant-api',\n 'sk-ant',\n 'sk-proj',\n 'sk-svcacct',\n 'sk-or-v1',\n 'sk-or',\n 'sk-lf',\n 'sk-',\n 'gsk_',\n 'aiza',\n 'ya29',\n 'ghp_',\n 'gho_',\n 'github_pat_',\n 'xai-',\n 'pplx-',\n 'r8_',\n 'hf_',\n 'astra-',\n 'astrae-',\n];\n\n/** Size caps — the only guard against jsonb bloat from a hostile POST. */\nexport const MAX_HEADERS = 64;\nexport const MAX_HEADER_VALUE_BYTES = 1024;\nexport const MAX_TOTAL_BYTES = 16 * 1024;\n\nfunction byteLength(s: string): number {\n // Edge-safe (no Buffer): count UTF-8 bytes without allocating a Buffer.\n let bytes = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n bytes += 4;\n i++; // surrogate pair\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Extract the safe key-format prefix from a credential header value. Strips a\n * leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix\n * that the (lowercased) value starts with. Returns undefined for opaque values\n * (e.g. a bare UUID token) so we never guess a format we don't recognise.\n */\nexport function extractApiKeyFormat(rawValue: string): string | undefined {\n if (!rawValue) return undefined;\n let value = rawValue.trim();\n const spaceIdx = value.indexOf(' ');\n if (spaceIdx > 0) {\n const scheme = value.slice(0, spaceIdx).toLowerCase();\n if (scheme === 'bearer' || scheme === 'basic' || scheme === 'token') {\n value = value.slice(spaceIdx + 1).trim();\n }\n }\n const lower = value.toLowerCase();\n for (const prefix of KEY_FORMAT_PREFIXES) {\n if (lower.startsWith(prefix)) {\n // Return the prefix as-declared in the list (canonical casing).\n return prefix;\n }\n }\n return undefined;\n}\n\nfunction isSecretHeader(name: string): boolean {\n if (SECRET_HEADER_NAMES.has(name)) return true;\n if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;\n if (isSignatureHeader(name)) return true;\n return false;\n}\n\nfunction isPlatformHeader(name: string): boolean {\n if (PLATFORM_HEADER_NAMES.has(name)) return true;\n return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));\n}\n\n/**\n * Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces\n * credential headers to a safe format prefix, keeps everything else verbatim,\n * and enforces the size caps that bound jsonb growth.\n *\n * Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,\n * CloudFront's flattened headers, a plain object). Multi-value headers are\n * joined with `, ` per RFC 7230.\n */\nexport function sanitizeHeaders(\n raw: Record<string, string | string[] | undefined> | undefined | null\n): SanitizeHeadersResult {\n const headers: Record<string, string> = {};\n const platformHeaders: Record<string, string> = {};\n let apiKeyFormat: string | undefined;\n let totalBytes = 0;\n let count = 0;\n\n if (!raw) return { headers };\n\n for (const rawName of Object.keys(raw)) {\n if (count >= MAX_HEADERS) break;\n const name = rawName.toLowerCase();\n const rawValue = raw[rawName];\n if (rawValue === undefined) continue;\n const joined = Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);\n\n // Credential headers: capture the format prefix, drop the secret.\n if (CREDENTIAL_HEADER_NAMES.has(name)) {\n const fmt = extractApiKeyFormat(joined);\n if (fmt && !apiKeyFormat) apiKeyFormat = fmt;\n continue;\n }\n\n // Genuine secrets: drop entirely.\n if (isSecretHeader(name)) continue;\n\n // Everything else describing the agent: keep, bounded.\n let value = joined;\n if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {\n value = value.slice(0, MAX_HEADER_VALUE_BYTES);\n }\n const entryBytes = byteLength(name) + byteLength(value);\n if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;\n totalBytes += entryBytes;\n count++;\n headers[name] = value;\n if (isPlatformHeader(name)) platformHeaders[name] = value;\n }\n\n const result: SanitizeHeadersResult = { headers };\n if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;\n if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;\n return result;\n}\n\n/**\n * Pull the known platform-signal headers out of an ALREADY-sanitised header\n * map. `sanitizeHeaders` already computes this as a by-product; this standalone\n * form is exported for call-sites (edge, tests) that hold a sanitised map and\n * want only the platform subset. Never re-run over raw (unsanitised) headers.\n */\nexport function extractPlatformHeaders(\n sanitized: Record<string, string> | undefined | null\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!sanitized) return out;\n for (const name of Object.keys(sanitized)) {\n if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/metadata-capture.ts"],"sourcesContent":["/**\n * Maximal metadata capture — the shared, edge-safe sanitiser.\n *\n * AstraSync's thesis is total metadata capture: every header and connection\n * signal an inbound agent presents is a signal we want to keep. This module is\n * the ONE place that decides what is safe to store verbatim, what is a secret\n * to drop, and what credential headers reduce to a safe *format prefix* (a\n * platform signal, never the secret itself).\n *\n * It runs in three places that must agree byte-for-byte:\n * - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture\n * point (full CloudFront header arrays),\n * - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,\n * - (indirectly) the backend, which stores whatever the above forward.\n *\n * Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser\n * build, and Deno. Capture is cheap: at the edge and SDK it is local string\n * work, and the backend stores it on a fire-and-forget event insert, so it adds\n * ZERO verify-access decision latency. Detection (which few signals imply which\n * vendor) is a separate, curated concern — see `platform-signatures.ts`.\n */\n\n/**\n * Version of the capture semantics (what is kept, dropped, reduced, or\n * derived — and under which key names).\n *\n * Capture semantics are FROZEN within a package major version: a given\n * `schemaVersion` always means the same field vocabulary and the same\n * keep/drop/reduce rules. The number bumps only on a SEMANTIC change to\n * capture (a renamed key, a changed reduction rule) — never for additive\n * signals, and never merely because the package version moved. To tell\n * builds apart, use the `sdkVersion` already present on the verify body;\n * `schemaVersion` answers the different question \"how do I interpret this\n * stored metadata?\".\n */\nexport const CAPTURE_SCHEMA_VERSION = 1;\n\n/**\n * The captured, sanitised view of an inbound agent's request metadata. Attached\n * to verify-access / beacon payloads as `callerMetadata.observedMetadata`.\n */\nexport interface ObservedMetadata {\n /** Header name → value, secrets removed, verbatim otherwise. */\n headers: Record<string, string>;\n /**\n * Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,\n * `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no\n * credential header was present or none matched a known format.\n */\n apiKeyFormat?: string;\n /**\n * The subset of `headers` that are known platform-signal headers\n * (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),\n * pulled out for convenient detection + display. Values are the same\n * sanitised strings as in `headers`.\n */\n platformHeaders?: Record<string, string>;\n /**\n * Connection-layer signals (IP, ASN, country, TLS version, HTTP version,\n * device class, TLS fingerprint). Edge adapters populate this from the\n * platform's native connection surface — the authoritative source. SDK\n * adapters populate it as a fallback via {@link deriveConnectionFromHeaders}\n * when a CDN in front of the merchant injected the equivalent headers;\n * absent when neither source had anything.\n */\n connection?: Record<string, string>;\n /**\n * Version of the capture semantics this object was produced under — see\n * {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with\n * metadata captured before the field existed.\n */\n schemaVersion?: number;\n}\n\n/**\n * Result of sanitising a raw header map. `headers` is always present (possibly\n * empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.\n */\nexport interface SanitizeHeadersResult {\n headers: Record<string, string>;\n apiKeyFormat?: string;\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * Header names dropped entirely — genuine secrets or our own credentials that\n * describe the request's transport, not the agent. Matched case-insensitively\n * on the exact header name.\n */\nconst SECRET_HEADER_NAMES = new Set([\n 'cookie',\n 'set-cookie',\n 'x-csrf-token',\n 'proxy-authorization',\n 'x-astrasync-signature',\n 'x-astrasync-secret',\n 'x-hub-signature',\n 'x-hub-signature-256',\n]);\n\n/**\n * Header-name prefixes dropped entirely — our own credential / signing surface\n * (`x-astrasync-*`, `kya-*`) must never round-trip into stored metadata.\n */\nconst SECRET_HEADER_PREFIXES = ['x-astrasync-', 'kya-', 'kya_'];\n\n/**\n * Credential headers whose VALUE is a secret but whose FORMAT is a signal. We\n * keep the leading format prefix (up to the random tail) and drop the rest.\n */\nconst CREDENTIAL_HEADER_NAMES = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'api-key',\n]);\n\n/**\n * Header names that carry a raw signature value (drop the value; the presence\n * is not worth a bloat risk). Matched as a suffix so `x-*-signature` variants\n * are covered without an exhaustive list.\n */\nfunction isSignatureHeader(name: string): boolean {\n return name === 'signature' || name.endsWith('-signature') || name.endsWith('-signature-256');\n}\n\n/**\n * Known platform-signal headers — pulled into `platformHeaders` for convenient\n * detection + display. Exact names plus a few prefixes (`x-goog-`,\n * `x-stainless-` — the OpenAI/Anthropic SDK client fingerprint headers).\n */\nconst PLATFORM_HEADER_NAMES = new Set([\n 'openai-organization',\n 'openai-project',\n 'openai-version',\n 'openai-processing-ms',\n 'anthropic-version',\n 'anthropic-beta',\n 'x-goog-user-project',\n 'x-goog-api-client',\n 'user-agent',\n 'x-agent-card-url',\n 'x-agent-provider',\n]);\nconst PLATFORM_HEADER_PREFIXES = ['x-goog-', 'x-stainless-', 'x-anthropic-', 'x-openai-'];\n\n/**\n * Known API-key format prefixes, longest-first so `sk-ant-api03` wins over\n * `sk-ant` and `sk-`. Each is the SAFE, non-secret leading token of a\n * credential; the random tail is never captured.\n */\nconst KEY_FORMAT_PREFIXES = [\n 'sk-ant-api03',\n 'sk-ant-api',\n 'sk-ant',\n 'sk-proj',\n 'sk-svcacct',\n 'sk-or-v1',\n 'sk-or',\n 'sk-lf',\n 'sk-',\n 'gsk_',\n 'aiza',\n 'ya29',\n 'ghp_',\n 'gho_',\n 'github_pat_',\n 'xai-',\n 'pplx-',\n 'r8_',\n 'hf_',\n 'astra-',\n 'astrae-',\n];\n\n/** Size caps — the only guard against jsonb bloat from a hostile POST. */\nexport const MAX_HEADERS = 64;\nexport const MAX_HEADER_VALUE_BYTES = 1024;\nexport const MAX_TOTAL_BYTES = 16 * 1024;\n\nfunction byteLength(s: string): number {\n // Edge-safe (no Buffer): count UTF-8 bytes without allocating a Buffer.\n let bytes = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n bytes += 4;\n i++; // surrogate pair\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Extract the safe key-format prefix from a credential header value. Strips a\n * leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix\n * that the (lowercased) value starts with. Returns undefined for opaque values\n * (e.g. a bare UUID token) so we never guess a format we don't recognise.\n */\nexport function extractApiKeyFormat(rawValue: string): string | undefined {\n if (!rawValue) return undefined;\n let value = rawValue.trim();\n const spaceIdx = value.indexOf(' ');\n if (spaceIdx > 0) {\n const scheme = value.slice(0, spaceIdx).toLowerCase();\n if (scheme === 'bearer' || scheme === 'basic' || scheme === 'token') {\n value = value.slice(spaceIdx + 1).trim();\n }\n }\n const lower = value.toLowerCase();\n for (const prefix of KEY_FORMAT_PREFIXES) {\n if (lower.startsWith(prefix)) {\n // Return the prefix as-declared in the list (canonical casing).\n return prefix;\n }\n }\n return undefined;\n}\n\nfunction isSecretHeader(name: string): boolean {\n if (SECRET_HEADER_NAMES.has(name)) return true;\n if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;\n if (isSignatureHeader(name)) return true;\n return false;\n}\n\nfunction isPlatformHeader(name: string): boolean {\n if (PLATFORM_HEADER_NAMES.has(name)) return true;\n return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));\n}\n\n/**\n * Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces\n * credential headers to a safe format prefix, keeps everything else verbatim,\n * and enforces the size caps that bound jsonb growth.\n *\n * Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,\n * CloudFront's flattened headers, a plain object). Multi-value headers are\n * joined with `, ` per RFC 7230.\n */\nexport function sanitizeHeaders(\n raw: Record<string, string | string[] | undefined> | undefined | null\n): SanitizeHeadersResult {\n const headers: Record<string, string> = {};\n const platformHeaders: Record<string, string> = {};\n let apiKeyFormat: string | undefined;\n let totalBytes = 0;\n let count = 0;\n\n if (!raw) return { headers };\n\n for (const rawName of Object.keys(raw)) {\n if (count >= MAX_HEADERS) break;\n const name = rawName.toLowerCase();\n const rawValue = raw[rawName];\n if (rawValue === undefined) continue;\n const joined = Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);\n\n // Credential headers: capture the format prefix, drop the secret.\n if (CREDENTIAL_HEADER_NAMES.has(name)) {\n const fmt = extractApiKeyFormat(joined);\n if (fmt && !apiKeyFormat) apiKeyFormat = fmt;\n continue;\n }\n\n // Genuine secrets: drop entirely.\n if (isSecretHeader(name)) continue;\n\n // Everything else describing the agent: keep, bounded.\n let value = joined;\n if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {\n value = value.slice(0, MAX_HEADER_VALUE_BYTES);\n }\n const entryBytes = byteLength(name) + byteLength(value);\n if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;\n totalBytes += entryBytes;\n count++;\n headers[name] = value;\n if (isPlatformHeader(name)) platformHeaders[name] = value;\n }\n\n const result: SanitizeHeadersResult = { headers };\n if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;\n if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;\n return result;\n}\n\n/**\n * Pull the known platform-signal headers out of an ALREADY-sanitised header\n * map. `sanitizeHeaders` already computes this as a by-product; this standalone\n * form is exported for call-sites (edge, tests) that hold a sanitised map and\n * want only the platform subset. Never re-run over raw (unsanitised) headers.\n */\nexport function extractPlatformHeaders(\n sanitized: Record<string, string> | undefined | null\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!sanitized) return out;\n for (const name of Object.keys(sanitized)) {\n if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];\n }\n return out;\n}\n\n/**\n * Precedence-ordered CDN header sources for each derived `connection` key.\n * First present header wins; the special `x-forwarded-for` source takes the\n * LEFTMOST (client) entry of the comma-separated chain.\n */\nconst CONNECTION_HEADER_SOURCES: ReadonlyArray<readonly [key: string, headers: readonly string[]]> =\n [\n ['ip', ['cf-connecting-ip', 'fastly-client-ip', 'true-client-ip', 'x-real-ip']],\n [\n 'country',\n ['cf-ipcountry', 'x-vercel-ip-country', 'cloudfront-viewer-country', 'fastly-geo-country'],\n ],\n ['region', ['x-vercel-ip-country-region', 'cloudfront-viewer-country-region']],\n ['city', ['x-vercel-ip-city', 'cloudfront-viewer-city']],\n ['asn', ['cloudfront-viewer-asn']],\n ['tlsVersion', ['cloudfront-viewer-tls']],\n ['httpVersion', ['cloudfront-viewer-http-version']],\n // TLS fingerprints can only be computed where TLS terminates. When a CDN\n // terminates TLS in front of the merchant, its fingerprint headers ARE the\n // connection-layer truth — normalising them here is the permanent design.\n ['tlsFingerprint', ['cf-ja3-hash']],\n ['ja4', ['cf-ja4']],\n ];\n\n/**\n * Derive the connection-layer block from CDN-injected request headers.\n *\n * When a merchant's SDK-gated origin sits behind a CDN (Cloudflare, Fastly,\n * Vercel, CloudFront), the CDN has already seen the connection layer and\n * injected it as headers. This promotes those headers to the same\n * `connection` keys the edge adapters populate natively, so SDK-only\n * deployments still get IP / geo / TLS attribution. Precedence per key:\n *\n * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >\n * `x-real-ip` > leftmost `x-forwarded-for`\n * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >\n * `cloudfront-viewer-country` > `fastly-geo-country`\n * - `region` / `city`: Vercel then CloudFront viewer headers\n * - `asn` / `tlsVersion` / `httpVersion`: `cloudfront-viewer-*`\n * - `tlsFingerprint` / `ja4`: Cloudflare `cf-ja3-hash` / `cf-ja4`\n *\n * Edge adapters keep their richer native collectors — native platform\n * signals are authoritative; this header derivation is the SDK-level\n * fallback. Header names are matched case-insensitively. Returns undefined\n * when no source header is present, so the `connection` key is simply\n * omitted (matching edge behaviour).\n */\nexport function deriveConnectionFromHeaders(\n headers: Record<string, string> | undefined | null\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const lower: Record<string, string> = {};\n for (const name of Object.keys(headers)) {\n const value = headers[name];\n if (typeof value === 'string' && value.length > 0) lower[name.toLowerCase()] = value;\n }\n\n const connection: Record<string, string> = {};\n for (const [key, sources] of CONNECTION_HEADER_SOURCES) {\n for (const source of sources) {\n const value = lower[source]?.trim();\n if (value) {\n connection[key] = value;\n break;\n }\n }\n }\n // ip fallback: leftmost (client) hop of the x-forwarded-for chain.\n if (!connection.ip) {\n const xff = lower['x-forwarded-for'];\n const client = xff?.split(',')[0]?.trim();\n if (client) connection.ip = client;\n }\n\n return Object.keys(connection).length > 0 ? connection : undefined;\n}\n\n/**\n * The FULL metadata capture for one request at an SDK adapter (express /\n * nextjs / mcp): sanitised headers, the safe credential format prefix, the\n * platform-signal subset, the CDN-derived connection block, and the capture\n * schema version. This is the ONE builder all SDK adapters share, so their\n * verify-access bodies agree byte-for-byte.\n *\n * Connection derivation runs over the RAW header map (pre-cap) — the size\n * caps guard storage growth and must never hide a geo/IP header that\n * happened to arrive late in an oversized map.\n */\nexport function buildSdkObservedMetadata(\n rawHeaders: Record<string, string | string[] | undefined> | undefined | null\n): ObservedMetadata {\n const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);\n const flat: Record<string, string> = {};\n if (rawHeaders) {\n for (const name of Object.keys(rawHeaders)) {\n const value = rawHeaders[name];\n if (value === undefined) continue;\n flat[name.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value);\n }\n }\n const connection = deriveConnectionFromHeaders(flat);\n return {\n headers,\n ...(apiKeyFormat && { apiKeyFormat }),\n ...(platformHeaders && { platformHeaders }),\n ...(connection && { connection }),\n schemaVersion: CAPTURE_SCHEMA_VERSION,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmCO,IAAM,yBAAyB;AAsDtC,IAAM,sBAAsB,oBAAI,IAAI;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAMD,IAAM,yBAAyB,CAAC,gBAAgB,QAAQ,MAAM;AAM9D,IAAM,0BAA0B,oBAAI,IAAI;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOD,SAAS,kBAAkB,MAAuB;AAChD,SAAO,SAAS,eAAe,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,gBAAgB;AAC9F;AAOA,IAAM,wBAAwB,oBAAI,IAAI;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AACD,IAAM,2BAA2B,CAAC,WAAW,gBAAgB,gBAAgB,WAAW;AAOxF,IAAM,sBAAsB;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,cAAc;AACpB,IAAM,yBAAyB;AAC/B,IAAM,kBAAkB,KAAK;AAEpC,SAAS,WAAW,GAAmB;AAErC,MAAI,QAAQ;AACZ,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,UAAM,OAAO,EAAE,WAAW,CAAC;AAC3B,QAAI,OAAO,IAAM,UAAS;AAAA,aACjB,OAAO,KAAO,UAAS;AAAA,aACvB,QAAQ,SAAU,QAAQ,OAAQ;AACzC,eAAS;AACT;AAAA,IACF,MAAO,UAAS;AAAA,EAClB;AACA,SAAO;AACT;AAQO,SAAS,oBAAoB,UAAsC;AACxE,MAAI,CAAC,SAAU,QAAO;AACtB,MAAI,QAAQ,SAAS,KAAK;AAC1B,QAAM,WAAW,MAAM,QAAQ,GAAG;AAClC,MAAI,WAAW,GAAG;AAChB,UAAM,SAAS,MAAM,MAAM,GAAG,QAAQ,EAAE,YAAY;AACpD,QAAI,WAAW,YAAY,WAAW,WAAW,WAAW,SAAS;AACnE,cAAQ,MAAM,MAAM,WAAW,CAAC,EAAE,KAAK;AAAA,IACzC;AAAA,EACF;AACA,QAAM,QAAQ,MAAM,YAAY;AAChC,aAAW,UAAU,qBAAqB;AACxC,QAAI,MAAM,WAAW,MAAM,GAAG;AAE5B,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,eAAe,MAAuB;AAC7C,MAAI,oBAAoB,IAAI,IAAI,EAAG,QAAO;AAC1C,MAAI,uBAAuB,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,EAAG,QAAO;AACnE,MAAI,kBAAkB,IAAI,EAAG,QAAO;AACpC,SAAO;AACT;AAEA,SAAS,iBAAiB,MAAuB;AAC/C,MAAI,sBAAsB,IAAI,IAAI,EAAG,QAAO;AAC5C,SAAO,yBAAyB,KAAK,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC;AAChE;AAWO,SAAS,gBACd,KACuB;AACvB,QAAM,UAAkC,CAAC;AACzC,QAAM,kBAA0C,CAAC;AACjD,MAAI;AACJ,MAAI,aAAa;AACjB,MAAI,QAAQ;AAEZ,MAAI,CAAC,IAAK,QAAO,EAAE,QAAQ;AAE3B,aAAW,WAAW,OAAO,KAAK,GAAG,GAAG;AACtC,QAAI,SAAS,YAAa;AAC1B,UAAM,OAAO,QAAQ,YAAY;AACjC,UAAM,WAAW,IAAI,OAAO;AAC5B,QAAI,aAAa,OAAW;AAC5B,UAAM,SAAS,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK,IAAI,IAAI,OAAO,QAAQ;AAG9E,QAAI,wBAAwB,IAAI,IAAI,GAAG;AACrC,YAAM,MAAM,oBAAoB,MAAM;AACtC,UAAI,OAAO,CAAC,aAAc,gBAAe;AACzC;AAAA,IACF;AAGA,QAAI,eAAe,IAAI,EAAG;AAG1B,QAAI,QAAQ;AACZ,QAAI,WAAW,KAAK,IAAI,wBAAwB;AAC9C,cAAQ,MAAM,MAAM,GAAG,sBAAsB;AAAA,IAC/C;AACA,UAAM,aAAa,WAAW,IAAI,IAAI,WAAW,KAAK;AACtD,QAAI,aAAa,aAAa,gBAAiB;AAC/C,kBAAc;AACd;AACA,YAAQ,IAAI,IAAI;AAChB,QAAI,iBAAiB,IAAI,EAAG,iBAAgB,IAAI,IAAI;AAAA,EACtD;AAEA,QAAM,SAAgC,EAAE,QAAQ;AAChD,MAAI,aAAc,QAAO,eAAe;AACxC,MAAI,OAAO,KAAK,eAAe,EAAE,SAAS,EAAG,QAAO,kBAAkB;AACtE,SAAO;AACT;AAQO,SAAS,uBACd,WACwB;AACxB,QAAM,MAA8B,CAAC;AACrC,MAAI,CAAC,UAAW,QAAO;AACvB,aAAW,QAAQ,OAAO,KAAK,SAAS,GAAG;AACzC,QAAI,iBAAiB,KAAK,YAAY,CAAC,EAAG,KAAI,KAAK,YAAY,CAAC,IAAI,UAAU,IAAI;AAAA,EACpF;AACA,SAAO;AACT;AAOA,IAAM,4BACJ;AAAA,EACE,CAAC,MAAM,CAAC,oBAAoB,oBAAoB,kBAAkB,WAAW,CAAC;AAAA,EAC9E;AAAA,IACE;AAAA,IACA,CAAC,gBAAgB,uBAAuB,6BAA6B,oBAAoB;AAAA,EAC3F;AAAA,EACA,CAAC,UAAU,CAAC,8BAA8B,kCAAkC,CAAC;AAAA,EAC7E,CAAC,QAAQ,CAAC,oBAAoB,wBAAwB,CAAC;AAAA,EACvD,CAAC,OAAO,CAAC,uBAAuB,CAAC;AAAA,EACjC,CAAC,cAAc,CAAC,uBAAuB,CAAC;AAAA,EACxC,CAAC,eAAe,CAAC,gCAAgC,CAAC;AAAA;AAAA;AAAA;AAAA,EAIlD,CAAC,kBAAkB,CAAC,aAAa,CAAC;AAAA,EAClC,CAAC,OAAO,CAAC,QAAQ,CAAC;AACpB;AAyBK,SAAS,4BACd,SACoC;AACpC,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAgC,CAAC;AACvC,aAAW,QAAQ,OAAO,KAAK,OAAO,GAAG;AACvC,UAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,OAAM,KAAK,YAAY,CAAC,IAAI;AAAA,EACjF;AAEA,QAAM,aAAqC,CAAC;AAC5C,aAAW,CAAC,KAAK,OAAO,KAAK,2BAA2B;AACtD,eAAW,UAAU,SAAS;AAC5B,YAAM,QAAQ,MAAM,MAAM,GAAG,KAAK;AAClC,UAAI,OAAO;AACT,mBAAW,GAAG,IAAI;AAClB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,WAAW,IAAI;AAClB,UAAM,MAAM,MAAM,iBAAiB;AACnC,UAAM,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AACxC,QAAI,OAAQ,YAAW,KAAK;AAAA,EAC9B;AAEA,SAAO,OAAO,KAAK,UAAU,EAAE,SAAS,IAAI,aAAa;AAC3D;AAaO,SAAS,yBACd,YACkB;AAClB,QAAM,EAAE,SAAS,cAAc,gBAAgB,IAAI,gBAAgB,UAAU;AAC7E,QAAM,OAA+B,CAAC;AACtC,MAAI,YAAY;AACd,eAAW,QAAQ,OAAO,KAAK,UAAU,GAAG;AAC1C,YAAM,QAAQ,WAAW,IAAI;AAC7B,UAAI,UAAU,OAAW;AACzB,WAAK,KAAK,YAAY,CAAC,IAAI,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,KAAK;AAAA,IACnF;AAAA,EACF;AACA,QAAM,aAAa,4BAA4B,IAAI;AACnD,SAAO;AAAA,IACL;AAAA,IACA,GAAI,gBAAgB,EAAE,aAAa;AAAA,IACnC,GAAI,mBAAmB,EAAE,gBAAgB;AAAA,IACzC,GAAI,cAAc,EAAE,WAAW;AAAA,IAC/B,eAAe;AAAA,EACjB;AACF;","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/metadata-capture.ts
|
|
2
|
+
var CAPTURE_SCHEMA_VERSION = 1;
|
|
2
3
|
var SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
3
4
|
"cookie",
|
|
4
5
|
"set-cookie",
|
|
@@ -143,10 +144,73 @@ function extractPlatformHeaders(sanitized) {
|
|
|
143
144
|
}
|
|
144
145
|
return out;
|
|
145
146
|
}
|
|
147
|
+
var CONNECTION_HEADER_SOURCES = [
|
|
148
|
+
["ip", ["cf-connecting-ip", "fastly-client-ip", "true-client-ip", "x-real-ip"]],
|
|
149
|
+
[
|
|
150
|
+
"country",
|
|
151
|
+
["cf-ipcountry", "x-vercel-ip-country", "cloudfront-viewer-country", "fastly-geo-country"]
|
|
152
|
+
],
|
|
153
|
+
["region", ["x-vercel-ip-country-region", "cloudfront-viewer-country-region"]],
|
|
154
|
+
["city", ["x-vercel-ip-city", "cloudfront-viewer-city"]],
|
|
155
|
+
["asn", ["cloudfront-viewer-asn"]],
|
|
156
|
+
["tlsVersion", ["cloudfront-viewer-tls"]],
|
|
157
|
+
["httpVersion", ["cloudfront-viewer-http-version"]],
|
|
158
|
+
// TLS fingerprints can only be computed where TLS terminates. When a CDN
|
|
159
|
+
// terminates TLS in front of the merchant, its fingerprint headers ARE the
|
|
160
|
+
// connection-layer truth — normalising them here is the permanent design.
|
|
161
|
+
["tlsFingerprint", ["cf-ja3-hash"]],
|
|
162
|
+
["ja4", ["cf-ja4"]]
|
|
163
|
+
];
|
|
164
|
+
function deriveConnectionFromHeaders(headers) {
|
|
165
|
+
if (!headers) return void 0;
|
|
166
|
+
const lower = {};
|
|
167
|
+
for (const name of Object.keys(headers)) {
|
|
168
|
+
const value = headers[name];
|
|
169
|
+
if (typeof value === "string" && value.length > 0) lower[name.toLowerCase()] = value;
|
|
170
|
+
}
|
|
171
|
+
const connection = {};
|
|
172
|
+
for (const [key, sources] of CONNECTION_HEADER_SOURCES) {
|
|
173
|
+
for (const source of sources) {
|
|
174
|
+
const value = lower[source]?.trim();
|
|
175
|
+
if (value) {
|
|
176
|
+
connection[key] = value;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!connection.ip) {
|
|
182
|
+
const xff = lower["x-forwarded-for"];
|
|
183
|
+
const client = xff?.split(",")[0]?.trim();
|
|
184
|
+
if (client) connection.ip = client;
|
|
185
|
+
}
|
|
186
|
+
return Object.keys(connection).length > 0 ? connection : void 0;
|
|
187
|
+
}
|
|
188
|
+
function buildSdkObservedMetadata(rawHeaders) {
|
|
189
|
+
const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);
|
|
190
|
+
const flat = {};
|
|
191
|
+
if (rawHeaders) {
|
|
192
|
+
for (const name of Object.keys(rawHeaders)) {
|
|
193
|
+
const value = rawHeaders[name];
|
|
194
|
+
if (value === void 0) continue;
|
|
195
|
+
flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const connection = deriveConnectionFromHeaders(flat);
|
|
199
|
+
return {
|
|
200
|
+
headers,
|
|
201
|
+
...apiKeyFormat && { apiKeyFormat },
|
|
202
|
+
...platformHeaders && { platformHeaders },
|
|
203
|
+
...connection && { connection },
|
|
204
|
+
schemaVersion: CAPTURE_SCHEMA_VERSION
|
|
205
|
+
};
|
|
206
|
+
}
|
|
146
207
|
export {
|
|
208
|
+
CAPTURE_SCHEMA_VERSION,
|
|
147
209
|
MAX_HEADERS,
|
|
148
210
|
MAX_HEADER_VALUE_BYTES,
|
|
149
211
|
MAX_TOTAL_BYTES,
|
|
212
|
+
buildSdkObservedMetadata,
|
|
213
|
+
deriveConnectionFromHeaders,
|
|
150
214
|
extractApiKeyFormat,
|
|
151
215
|
extractPlatformHeaders,
|
|
152
216
|
sanitizeHeaders
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/metadata-capture.ts"],"sourcesContent":["/**\n * Maximal metadata capture — the shared, edge-safe sanitiser.\n *\n * AstraSync's thesis is total metadata capture: every header and connection\n * signal an inbound agent presents is a signal we want to keep. This module is\n * the ONE place that decides what is safe to store verbatim, what is a secret\n * to drop, and what credential headers reduce to a safe *format prefix* (a\n * platform signal, never the secret itself).\n *\n * It runs in three places that must agree byte-for-byte:\n * - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture\n * point (full CloudFront header arrays),\n * - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,\n * - (indirectly) the backend, which stores whatever the above forward.\n *\n * Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser\n * build, and Deno. Capture is cheap: at the edge and SDK it is local string\n * work, and the backend stores it on a fire-and-forget event insert, so it adds\n * ZERO verify-access decision latency. Detection (which few signals imply which\n * vendor) is a separate, curated concern — see `platform-signatures.ts`.\n */\n\n/**\n * The captured, sanitised view of an inbound agent's request metadata. Attached\n * to verify-access / beacon payloads as `callerMetadata.observedMetadata`.\n */\nexport interface ObservedMetadata {\n /** Header name → value, secrets removed, verbatim otherwise. */\n headers: Record<string, string>;\n /**\n * Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,\n * `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no\n * credential header was present or none matched a known format.\n */\n apiKeyFormat?: string;\n /**\n * The subset of `headers` that are known platform-signal headers\n * (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),\n * pulled out for convenient detection + display. Values are the same\n * sanitised strings as in `headers`.\n */\n platformHeaders?: Record<string, string>;\n /**\n * Connection-layer signals the edge can see but the SDK cannot (IP, ASN,\n * country, TLS version/cipher, HTTP version, device class). Populated by the\n * edge adapter's `collectConnectionMetadata`; absent for SDK-only capture.\n */\n connection?: Record<string, string>;\n}\n\n/**\n * Result of sanitising a raw header map. `headers` is always present (possibly\n * empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.\n */\nexport interface SanitizeHeadersResult {\n headers: Record<string, string>;\n apiKeyFormat?: string;\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * Header names dropped entirely — genuine secrets or our own credentials that\n * describe the request's transport, not the agent. Matched case-insensitively\n * on the exact header name.\n */\nconst SECRET_HEADER_NAMES = new Set([\n 'cookie',\n 'set-cookie',\n 'x-csrf-token',\n 'proxy-authorization',\n 'x-astrasync-signature',\n 'x-astrasync-secret',\n 'x-hub-signature',\n 'x-hub-signature-256',\n]);\n\n/**\n * Header-name prefixes dropped entirely — our own credential / signing surface\n * (`x-astrasync-*`, `kya-*`) must never round-trip into stored metadata.\n */\nconst SECRET_HEADER_PREFIXES = ['x-astrasync-', 'kya-', 'kya_'];\n\n/**\n * Credential headers whose VALUE is a secret but whose FORMAT is a signal. We\n * keep the leading format prefix (up to the random tail) and drop the rest.\n */\nconst CREDENTIAL_HEADER_NAMES = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'api-key',\n]);\n\n/**\n * Header names that carry a raw signature value (drop the value; the presence\n * is not worth a bloat risk). Matched as a suffix so `x-*-signature` variants\n * are covered without an exhaustive list.\n */\nfunction isSignatureHeader(name: string): boolean {\n return name === 'signature' || name.endsWith('-signature') || name.endsWith('-signature-256');\n}\n\n/**\n * Known platform-signal headers — pulled into `platformHeaders` for convenient\n * detection + display. Exact names plus a few prefixes (`x-goog-`,\n * `x-stainless-` — the OpenAI/Anthropic SDK client fingerprint headers).\n */\nconst PLATFORM_HEADER_NAMES = new Set([\n 'openai-organization',\n 'openai-project',\n 'openai-version',\n 'openai-processing-ms',\n 'anthropic-version',\n 'anthropic-beta',\n 'x-goog-user-project',\n 'x-goog-api-client',\n 'user-agent',\n 'x-agent-card-url',\n 'x-agent-provider',\n]);\nconst PLATFORM_HEADER_PREFIXES = ['x-goog-', 'x-stainless-', 'x-anthropic-', 'x-openai-'];\n\n/**\n * Known API-key format prefixes, longest-first so `sk-ant-api03` wins over\n * `sk-ant` and `sk-`. Each is the SAFE, non-secret leading token of a\n * credential; the random tail is never captured.\n */\nconst KEY_FORMAT_PREFIXES = [\n 'sk-ant-api03',\n 'sk-ant-api',\n 'sk-ant',\n 'sk-proj',\n 'sk-svcacct',\n 'sk-or-v1',\n 'sk-or',\n 'sk-lf',\n 'sk-',\n 'gsk_',\n 'aiza',\n 'ya29',\n 'ghp_',\n 'gho_',\n 'github_pat_',\n 'xai-',\n 'pplx-',\n 'r8_',\n 'hf_',\n 'astra-',\n 'astrae-',\n];\n\n/** Size caps — the only guard against jsonb bloat from a hostile POST. */\nexport const MAX_HEADERS = 64;\nexport const MAX_HEADER_VALUE_BYTES = 1024;\nexport const MAX_TOTAL_BYTES = 16 * 1024;\n\nfunction byteLength(s: string): number {\n // Edge-safe (no Buffer): count UTF-8 bytes without allocating a Buffer.\n let bytes = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n bytes += 4;\n i++; // surrogate pair\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Extract the safe key-format prefix from a credential header value. Strips a\n * leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix\n * that the (lowercased) value starts with. Returns undefined for opaque values\n * (e.g. a bare UUID token) so we never guess a format we don't recognise.\n */\nexport function extractApiKeyFormat(rawValue: string): string | undefined {\n if (!rawValue) return undefined;\n let value = rawValue.trim();\n const spaceIdx = value.indexOf(' ');\n if (spaceIdx > 0) {\n const scheme = value.slice(0, spaceIdx).toLowerCase();\n if (scheme === 'bearer' || scheme === 'basic' || scheme === 'token') {\n value = value.slice(spaceIdx + 1).trim();\n }\n }\n const lower = value.toLowerCase();\n for (const prefix of KEY_FORMAT_PREFIXES) {\n if (lower.startsWith(prefix)) {\n // Return the prefix as-declared in the list (canonical casing).\n return prefix;\n }\n }\n return undefined;\n}\n\nfunction isSecretHeader(name: string): boolean {\n if (SECRET_HEADER_NAMES.has(name)) return true;\n if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;\n if (isSignatureHeader(name)) return true;\n return false;\n}\n\nfunction isPlatformHeader(name: string): boolean {\n if (PLATFORM_HEADER_NAMES.has(name)) return true;\n return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));\n}\n\n/**\n * Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces\n * credential headers to a safe format prefix, keeps everything else verbatim,\n * and enforces the size caps that bound jsonb growth.\n *\n * Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,\n * CloudFront's flattened headers, a plain object). Multi-value headers are\n * joined with `, ` per RFC 7230.\n */\nexport function sanitizeHeaders(\n raw: Record<string, string | string[] | undefined> | undefined | null\n): SanitizeHeadersResult {\n const headers: Record<string, string> = {};\n const platformHeaders: Record<string, string> = {};\n let apiKeyFormat: string | undefined;\n let totalBytes = 0;\n let count = 0;\n\n if (!raw) return { headers };\n\n for (const rawName of Object.keys(raw)) {\n if (count >= MAX_HEADERS) break;\n const name = rawName.toLowerCase();\n const rawValue = raw[rawName];\n if (rawValue === undefined) continue;\n const joined = Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);\n\n // Credential headers: capture the format prefix, drop the secret.\n if (CREDENTIAL_HEADER_NAMES.has(name)) {\n const fmt = extractApiKeyFormat(joined);\n if (fmt && !apiKeyFormat) apiKeyFormat = fmt;\n continue;\n }\n\n // Genuine secrets: drop entirely.\n if (isSecretHeader(name)) continue;\n\n // Everything else describing the agent: keep, bounded.\n let value = joined;\n if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {\n value = value.slice(0, MAX_HEADER_VALUE_BYTES);\n }\n const entryBytes = byteLength(name) + byteLength(value);\n if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;\n totalBytes += entryBytes;\n count++;\n headers[name] = value;\n if (isPlatformHeader(name)) platformHeaders[name] = value;\n }\n\n const result: SanitizeHeadersResult = { headers };\n if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;\n if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;\n return result;\n}\n\n/**\n * Pull the known platform-signal headers out of an ALREADY-sanitised header\n * map. `sanitizeHeaders` already computes this as a by-product; this standalone\n * form is exported for call-sites (edge, tests) that hold a sanitised map and\n * want only the platform subset. Never re-run over raw (unsanitised) headers.\n */\nexport function extractPlatformHeaders(\n sanitized: Record<string, string> | undefined | null\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!sanitized) return out;\n for (const name of Object.keys(sanitized)) {\n if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/metadata-capture.ts"],"sourcesContent":["/**\n * Maximal metadata capture — the shared, edge-safe sanitiser.\n *\n * AstraSync's thesis is total metadata capture: every header and connection\n * signal an inbound agent presents is a signal we want to keep. This module is\n * the ONE place that decides what is safe to store verbatim, what is a secret\n * to drop, and what credential headers reduce to a safe *format prefix* (a\n * platform signal, never the secret itself).\n *\n * It runs in three places that must agree byte-for-byte:\n * - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture\n * point (full CloudFront header arrays),\n * - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,\n * - (indirectly) the backend, which stores whatever the above forward.\n *\n * Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser\n * build, and Deno. Capture is cheap: at the edge and SDK it is local string\n * work, and the backend stores it on a fire-and-forget event insert, so it adds\n * ZERO verify-access decision latency. Detection (which few signals imply which\n * vendor) is a separate, curated concern — see `platform-signatures.ts`.\n */\n\n/**\n * Version of the capture semantics (what is kept, dropped, reduced, or\n * derived — and under which key names).\n *\n * Capture semantics are FROZEN within a package major version: a given\n * `schemaVersion` always means the same field vocabulary and the same\n * keep/drop/reduce rules. The number bumps only on a SEMANTIC change to\n * capture (a renamed key, a changed reduction rule) — never for additive\n * signals, and never merely because the package version moved. To tell\n * builds apart, use the `sdkVersion` already present on the verify body;\n * `schemaVersion` answers the different question \"how do I interpret this\n * stored metadata?\".\n */\nexport const CAPTURE_SCHEMA_VERSION = 1;\n\n/**\n * The captured, sanitised view of an inbound agent's request metadata. Attached\n * to verify-access / beacon payloads as `callerMetadata.observedMetadata`.\n */\nexport interface ObservedMetadata {\n /** Header name → value, secrets removed, verbatim otherwise. */\n headers: Record<string, string>;\n /**\n * Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,\n * `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no\n * credential header was present or none matched a known format.\n */\n apiKeyFormat?: string;\n /**\n * The subset of `headers` that are known platform-signal headers\n * (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),\n * pulled out for convenient detection + display. Values are the same\n * sanitised strings as in `headers`.\n */\n platformHeaders?: Record<string, string>;\n /**\n * Connection-layer signals (IP, ASN, country, TLS version, HTTP version,\n * device class, TLS fingerprint). Edge adapters populate this from the\n * platform's native connection surface — the authoritative source. SDK\n * adapters populate it as a fallback via {@link deriveConnectionFromHeaders}\n * when a CDN in front of the merchant injected the equivalent headers;\n * absent when neither source had anything.\n */\n connection?: Record<string, string>;\n /**\n * Version of the capture semantics this object was produced under — see\n * {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with\n * metadata captured before the field existed.\n */\n schemaVersion?: number;\n}\n\n/**\n * Result of sanitising a raw header map. `headers` is always present (possibly\n * empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.\n */\nexport interface SanitizeHeadersResult {\n headers: Record<string, string>;\n apiKeyFormat?: string;\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * Header names dropped entirely — genuine secrets or our own credentials that\n * describe the request's transport, not the agent. Matched case-insensitively\n * on the exact header name.\n */\nconst SECRET_HEADER_NAMES = new Set([\n 'cookie',\n 'set-cookie',\n 'x-csrf-token',\n 'proxy-authorization',\n 'x-astrasync-signature',\n 'x-astrasync-secret',\n 'x-hub-signature',\n 'x-hub-signature-256',\n]);\n\n/**\n * Header-name prefixes dropped entirely — our own credential / signing surface\n * (`x-astrasync-*`, `kya-*`) must never round-trip into stored metadata.\n */\nconst SECRET_HEADER_PREFIXES = ['x-astrasync-', 'kya-', 'kya_'];\n\n/**\n * Credential headers whose VALUE is a secret but whose FORMAT is a signal. We\n * keep the leading format prefix (up to the random tail) and drop the rest.\n */\nconst CREDENTIAL_HEADER_NAMES = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'api-key',\n]);\n\n/**\n * Header names that carry a raw signature value (drop the value; the presence\n * is not worth a bloat risk). Matched as a suffix so `x-*-signature` variants\n * are covered without an exhaustive list.\n */\nfunction isSignatureHeader(name: string): boolean {\n return name === 'signature' || name.endsWith('-signature') || name.endsWith('-signature-256');\n}\n\n/**\n * Known platform-signal headers — pulled into `platformHeaders` for convenient\n * detection + display. Exact names plus a few prefixes (`x-goog-`,\n * `x-stainless-` — the OpenAI/Anthropic SDK client fingerprint headers).\n */\nconst PLATFORM_HEADER_NAMES = new Set([\n 'openai-organization',\n 'openai-project',\n 'openai-version',\n 'openai-processing-ms',\n 'anthropic-version',\n 'anthropic-beta',\n 'x-goog-user-project',\n 'x-goog-api-client',\n 'user-agent',\n 'x-agent-card-url',\n 'x-agent-provider',\n]);\nconst PLATFORM_HEADER_PREFIXES = ['x-goog-', 'x-stainless-', 'x-anthropic-', 'x-openai-'];\n\n/**\n * Known API-key format prefixes, longest-first so `sk-ant-api03` wins over\n * `sk-ant` and `sk-`. Each is the SAFE, non-secret leading token of a\n * credential; the random tail is never captured.\n */\nconst KEY_FORMAT_PREFIXES = [\n 'sk-ant-api03',\n 'sk-ant-api',\n 'sk-ant',\n 'sk-proj',\n 'sk-svcacct',\n 'sk-or-v1',\n 'sk-or',\n 'sk-lf',\n 'sk-',\n 'gsk_',\n 'aiza',\n 'ya29',\n 'ghp_',\n 'gho_',\n 'github_pat_',\n 'xai-',\n 'pplx-',\n 'r8_',\n 'hf_',\n 'astra-',\n 'astrae-',\n];\n\n/** Size caps — the only guard against jsonb bloat from a hostile POST. */\nexport const MAX_HEADERS = 64;\nexport const MAX_HEADER_VALUE_BYTES = 1024;\nexport const MAX_TOTAL_BYTES = 16 * 1024;\n\nfunction byteLength(s: string): number {\n // Edge-safe (no Buffer): count UTF-8 bytes without allocating a Buffer.\n let bytes = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n bytes += 4;\n i++; // surrogate pair\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Extract the safe key-format prefix from a credential header value. Strips a\n * leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix\n * that the (lowercased) value starts with. Returns undefined for opaque values\n * (e.g. a bare UUID token) so we never guess a format we don't recognise.\n */\nexport function extractApiKeyFormat(rawValue: string): string | undefined {\n if (!rawValue) return undefined;\n let value = rawValue.trim();\n const spaceIdx = value.indexOf(' ');\n if (spaceIdx > 0) {\n const scheme = value.slice(0, spaceIdx).toLowerCase();\n if (scheme === 'bearer' || scheme === 'basic' || scheme === 'token') {\n value = value.slice(spaceIdx + 1).trim();\n }\n }\n const lower = value.toLowerCase();\n for (const prefix of KEY_FORMAT_PREFIXES) {\n if (lower.startsWith(prefix)) {\n // Return the prefix as-declared in the list (canonical casing).\n return prefix;\n }\n }\n return undefined;\n}\n\nfunction isSecretHeader(name: string): boolean {\n if (SECRET_HEADER_NAMES.has(name)) return true;\n if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;\n if (isSignatureHeader(name)) return true;\n return false;\n}\n\nfunction isPlatformHeader(name: string): boolean {\n if (PLATFORM_HEADER_NAMES.has(name)) return true;\n return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));\n}\n\n/**\n * Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces\n * credential headers to a safe format prefix, keeps everything else verbatim,\n * and enforces the size caps that bound jsonb growth.\n *\n * Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,\n * CloudFront's flattened headers, a plain object). Multi-value headers are\n * joined with `, ` per RFC 7230.\n */\nexport function sanitizeHeaders(\n raw: Record<string, string | string[] | undefined> | undefined | null\n): SanitizeHeadersResult {\n const headers: Record<string, string> = {};\n const platformHeaders: Record<string, string> = {};\n let apiKeyFormat: string | undefined;\n let totalBytes = 0;\n let count = 0;\n\n if (!raw) return { headers };\n\n for (const rawName of Object.keys(raw)) {\n if (count >= MAX_HEADERS) break;\n const name = rawName.toLowerCase();\n const rawValue = raw[rawName];\n if (rawValue === undefined) continue;\n const joined = Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);\n\n // Credential headers: capture the format prefix, drop the secret.\n if (CREDENTIAL_HEADER_NAMES.has(name)) {\n const fmt = extractApiKeyFormat(joined);\n if (fmt && !apiKeyFormat) apiKeyFormat = fmt;\n continue;\n }\n\n // Genuine secrets: drop entirely.\n if (isSecretHeader(name)) continue;\n\n // Everything else describing the agent: keep, bounded.\n let value = joined;\n if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {\n value = value.slice(0, MAX_HEADER_VALUE_BYTES);\n }\n const entryBytes = byteLength(name) + byteLength(value);\n if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;\n totalBytes += entryBytes;\n count++;\n headers[name] = value;\n if (isPlatformHeader(name)) platformHeaders[name] = value;\n }\n\n const result: SanitizeHeadersResult = { headers };\n if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;\n if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;\n return result;\n}\n\n/**\n * Pull the known platform-signal headers out of an ALREADY-sanitised header\n * map. `sanitizeHeaders` already computes this as a by-product; this standalone\n * form is exported for call-sites (edge, tests) that hold a sanitised map and\n * want only the platform subset. Never re-run over raw (unsanitised) headers.\n */\nexport function extractPlatformHeaders(\n sanitized: Record<string, string> | undefined | null\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!sanitized) return out;\n for (const name of Object.keys(sanitized)) {\n if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];\n }\n return out;\n}\n\n/**\n * Precedence-ordered CDN header sources for each derived `connection` key.\n * First present header wins; the special `x-forwarded-for` source takes the\n * LEFTMOST (client) entry of the comma-separated chain.\n */\nconst CONNECTION_HEADER_SOURCES: ReadonlyArray<readonly [key: string, headers: readonly string[]]> =\n [\n ['ip', ['cf-connecting-ip', 'fastly-client-ip', 'true-client-ip', 'x-real-ip']],\n [\n 'country',\n ['cf-ipcountry', 'x-vercel-ip-country', 'cloudfront-viewer-country', 'fastly-geo-country'],\n ],\n ['region', ['x-vercel-ip-country-region', 'cloudfront-viewer-country-region']],\n ['city', ['x-vercel-ip-city', 'cloudfront-viewer-city']],\n ['asn', ['cloudfront-viewer-asn']],\n ['tlsVersion', ['cloudfront-viewer-tls']],\n ['httpVersion', ['cloudfront-viewer-http-version']],\n // TLS fingerprints can only be computed where TLS terminates. When a CDN\n // terminates TLS in front of the merchant, its fingerprint headers ARE the\n // connection-layer truth — normalising them here is the permanent design.\n ['tlsFingerprint', ['cf-ja3-hash']],\n ['ja4', ['cf-ja4']],\n ];\n\n/**\n * Derive the connection-layer block from CDN-injected request headers.\n *\n * When a merchant's SDK-gated origin sits behind a CDN (Cloudflare, Fastly,\n * Vercel, CloudFront), the CDN has already seen the connection layer and\n * injected it as headers. This promotes those headers to the same\n * `connection` keys the edge adapters populate natively, so SDK-only\n * deployments still get IP / geo / TLS attribution. Precedence per key:\n *\n * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >\n * `x-real-ip` > leftmost `x-forwarded-for`\n * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >\n * `cloudfront-viewer-country` > `fastly-geo-country`\n * - `region` / `city`: Vercel then CloudFront viewer headers\n * - `asn` / `tlsVersion` / `httpVersion`: `cloudfront-viewer-*`\n * - `tlsFingerprint` / `ja4`: Cloudflare `cf-ja3-hash` / `cf-ja4`\n *\n * Edge adapters keep their richer native collectors — native platform\n * signals are authoritative; this header derivation is the SDK-level\n * fallback. Header names are matched case-insensitively. Returns undefined\n * when no source header is present, so the `connection` key is simply\n * omitted (matching edge behaviour).\n */\nexport function deriveConnectionFromHeaders(\n headers: Record<string, string> | undefined | null\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const lower: Record<string, string> = {};\n for (const name of Object.keys(headers)) {\n const value = headers[name];\n if (typeof value === 'string' && value.length > 0) lower[name.toLowerCase()] = value;\n }\n\n const connection: Record<string, string> = {};\n for (const [key, sources] of CONNECTION_HEADER_SOURCES) {\n for (const source of sources) {\n const value = lower[source]?.trim();\n if (value) {\n connection[key] = value;\n break;\n }\n }\n }\n // ip fallback: leftmost (client) hop of the x-forwarded-for chain.\n if (!connection.ip) {\n const xff = lower['x-forwarded-for'];\n const client = xff?.split(',')[0]?.trim();\n if (client) connection.ip = client;\n }\n\n return Object.keys(connection).length > 0 ? connection : undefined;\n}\n\n/**\n * The FULL metadata capture for one request at an SDK adapter (express /\n * nextjs / mcp): sanitised headers, the safe credential format prefix, the\n * platform-signal subset, the CDN-derived connection block, and the capture\n * schema version. This is the ONE builder all SDK adapters share, so their\n * verify-access bodies agree byte-for-byte.\n *\n * Connection derivation runs over the RAW header map (pre-cap) — the size\n * caps guard storage growth and must never hide a geo/IP header that\n * happened to arrive late in an oversized map.\n */\nexport function buildSdkObservedMetadata(\n rawHeaders: Record<string, string | string[] | undefined> | undefined | null\n): ObservedMetadata {\n const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);\n const flat: Record<string, string> = {};\n if (rawHeaders) {\n for (const name of Object.keys(rawHeaders)) {\n const value = rawHeaders[name];\n if (value === undefined) continue;\n flat[name.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value);\n }\n }\n const connection = deriveConnectionFromHeaders(flat);\n return {\n headers,\n ...(apiKeyFormat && { apiKeyFormat }),\n ...(platformHeaders && { platformHeaders }),\n ...(connection && { connection }),\n schemaVersion: CAPTURE_SCHEMA_VERSION,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as next_server from 'next/server';
|
|
2
2
|
import { NextRequest } from 'next/server';
|
|
3
|
-
import { N as NextJsMiddlewareOptions } from './types-
|
|
3
|
+
import { N as NextJsMiddlewareOptions } from './types-r850cOt0.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Create Next.js middleware for agent verification.
|
|
@@ -8,8 +8,8 @@ import { N as NextJsMiddlewareOptions } from './types-BcDf9Kk0.mjs';
|
|
|
8
8
|
* v2.9.7 moved per-route policy out of merchant-side source code into the
|
|
9
9
|
* AstraSync dashboard. The middleware fetches its routes from the backend
|
|
10
10
|
* via `GET /endpoints/:counterpartyId/routes` on init and refreshes
|
|
11
|
-
* periodically — see `ExpressMiddlewareOptions` for the rationale (
|
|
12
|
-
*
|
|
11
|
+
* periodically — see `ExpressMiddlewareOptions` for the rationale (a dual
|
|
12
|
+
* config source silently conflicts with dashboard policy).
|
|
13
13
|
*/
|
|
14
14
|
declare function createMiddleware(options: NextJsMiddlewareOptions): (request: NextRequest) => Promise<next_server.NextResponse<unknown>>;
|
|
15
15
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as next_server from 'next/server';
|
|
2
2
|
import { NextRequest } from 'next/server';
|
|
3
|
-
import { N as NextJsMiddlewareOptions } from './types-
|
|
3
|
+
import { N as NextJsMiddlewareOptions } from './types-sMxBT-nM.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Create Next.js middleware for agent verification.
|
|
@@ -8,8 +8,8 @@ import { N as NextJsMiddlewareOptions } from './types-fTQD448H.js';
|
|
|
8
8
|
* v2.9.7 moved per-route policy out of merchant-side source code into the
|
|
9
9
|
* AstraSync dashboard. The middleware fetches its routes from the backend
|
|
10
10
|
* via `GET /endpoints/:counterpartyId/routes` on init and refreshes
|
|
11
|
-
* periodically — see `ExpressMiddlewareOptions` for the rationale (
|
|
12
|
-
*
|
|
11
|
+
* periodically — see `ExpressMiddlewareOptions` for the rationale (a dual
|
|
12
|
+
* config source silently conflicts with dashboard policy).
|
|
13
13
|
*/
|
|
14
14
|
declare function createMiddleware(options: NextJsMiddlewareOptions): (request: NextRequest) => Promise<next_server.NextResponse<unknown>>;
|
|
15
15
|
/**
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
* the three-tier visibility numbers drift between capture points.
|
|
11
11
|
*
|
|
12
12
|
* Pure regex — no Node built-ins, edge-safe. Detection is regex-only for
|
|
13
|
-
* v2.9 — TLS fingerprinting / behavioural signatures are deferred
|
|
14
|
-
* (recorded in adapter requirements doc).
|
|
13
|
+
* v2.9 — TLS fingerprinting / behavioural signatures are deferred.
|
|
15
14
|
*/
|
|
16
15
|
type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'unknown';
|
|
17
16
|
interface PlatformFingerprint {
|
|
@@ -25,13 +24,13 @@ interface PlatformDetectionInput {
|
|
|
25
24
|
userAgent?: string;
|
|
26
25
|
agentCardUrl?: string;
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
27
|
+
* A safe API-key format prefix observed on the caller's
|
|
29
28
|
* credential header (e.g. `sk-ant-api03`, `sk-proj`, `aiza`). A strong
|
|
30
29
|
* platform signal; never the secret. Matched against `apiKeyFormatPatterns`.
|
|
31
30
|
*/
|
|
32
31
|
apiKeyFormat?: string;
|
|
33
32
|
/**
|
|
34
|
-
*
|
|
33
|
+
* The sanitised platform-signal headers (lowercased names →
|
|
35
34
|
* values), e.g. `openai-organization`, `x-goog-user-project`. Matched
|
|
36
35
|
* against `headerPatterns` (which test on `name` and `name: value`).
|
|
37
36
|
*/
|
|
@@ -49,13 +48,13 @@ interface PlatformSignatureDef<V extends string = string> {
|
|
|
49
48
|
uaPatterns?: RegExp[];
|
|
50
49
|
agentCardPatterns?: RegExp[];
|
|
51
50
|
/**
|
|
52
|
-
*
|
|
51
|
+
* Patterns tested against an observed API-key format prefix
|
|
53
52
|
* (e.g. `/^sk-ant/i` → claude). A credential-header format is a stronger
|
|
54
53
|
* platform signal than a spoofable User-Agent.
|
|
55
54
|
*/
|
|
56
55
|
apiKeyFormatPatterns?: RegExp[];
|
|
57
56
|
/**
|
|
58
|
-
*
|
|
57
|
+
* Patterns tested against each observed platform-signal
|
|
59
58
|
* header, as both `name` and `name: value` (so `/openai-organization/i` and
|
|
60
59
|
* `/x-goog-user-project/i` match on presence).
|
|
61
60
|
*/
|