@astrasyncai/adapter-lambda 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,370 @@
1
+ import { CloudFrontRequestEvent, CloudFrontRequestResult, CloudFrontRequest, CloudFrontHeaders, CloudFrontResultResponse } from 'aws-lambda';
2
+ import { EdgeConfig, EdgeMode, EdgeVerificationDepth } from '@astrasyncai/verification-gateway/edge-config';
3
+ import { PlatformFingerprint } from '@astrasyncai/verification-gateway/platform-signatures';
4
+ import { CommerceArtifactsPayload } from '@astrasyncai/verification-gateway';
5
+
6
+ /**
7
+ * Commerce Shield edge adapter (spec §7.4 / §33.5 #1) — shared types.
8
+ *
9
+ * The adapter is a thin interception shell: it classifies inbound traffic
10
+ * into three visibility tiers, applies the dashboard-configured EdgeConfig
11
+ * (mode/depth/path rules), forwards raw commerce artifacts to verify-access
12
+ * (which does ALL cryptographic verification server-side, Step 0.5), and —
13
+ * only in enforce mode — turns decisions into responses. In observe mode it
14
+ * NEVER blocks and NEVER mutates responses.
15
+ */
16
+
17
+ /**
18
+ * The three visibility tiers of the Commerce Shield live agent feed, plus
19
+ * `human` (which the shell touches as little as possible) and `skipped`
20
+ * (path-filtered — static assets etc.).
21
+ */
22
+ type TrafficTier = 'verified' | 'identified' | 'anonymous-bot' | 'human' | 'skipped';
23
+ interface Classification {
24
+ tier: TrafficTier;
25
+ /** Detected transport/credential protocol (tier `identified`/`verified`). */
26
+ protocol?: string;
27
+ /** Platform UA fingerprint when one matched. */
28
+ fingerprint?: PlatformFingerprint;
29
+ /** Raw commerce artifacts detected on the wire, ready to forward. */
30
+ artifacts?: CommerceArtifactsPayload;
31
+ /** Light, crypto-free detection detail for the beacon (kid namespace etc). */
32
+ evidence?: Record<string, unknown>;
33
+ }
34
+ /** What the shell decided (observe mode records it; enforce mode acts on it). */
35
+ type EdgeDecision = 'allow' | 'deny' | 'manual_review';
36
+ interface TierOutcome {
37
+ tier: TrafficTier;
38
+ decision: EdgeDecision;
39
+ /** Why (deny/step-up reasons, or 'observe-passthrough'). */
40
+ reasons: string[];
41
+ /** ASAT header material when a verify-access call succeeded. */
42
+ asat?: {
43
+ trustScore?: number;
44
+ agentId?: string;
45
+ developerId?: string;
46
+ accessLevel?: string;
47
+ };
48
+ sessionId?: string;
49
+ protocol?: string;
50
+ guidance?: {
51
+ message?: string;
52
+ registrationUrl?: string;
53
+ documentationUrl?: string;
54
+ };
55
+ stepUp?: {
56
+ pollUrl?: string;
57
+ expiresAt?: string;
58
+ };
59
+ correlationId?: string;
60
+ }
61
+ /** Per-request effective policy after path-rule resolution. */
62
+ interface EffectivePolicy {
63
+ mode: EdgeMode;
64
+ depth: EdgeVerificationDepth;
65
+ skip: boolean;
66
+ }
67
+ interface EdgeAdapterOptions {
68
+ /** AstraSync API base, e.g. `https://astrasync.ai/api`. */
69
+ apiBaseUrl: string;
70
+ /** The protected property's ASTRAE endpoint id. */
71
+ counterpartyId: string;
72
+ /** Canonical public URL of the protected property, e.g. `https://astrasync.shop`. */
73
+ counterpartyUrl: string;
74
+ /**
75
+ * Merchant API key (`kya_*`). Provide directly (tests/local) or via
76
+ * `apiKeySecretName` (production — AWS Secrets Manager us-east-1, since
77
+ * Lambda@Edge forbids environment variables).
78
+ */
79
+ apiKey?: string;
80
+ apiKeySecretName?: string;
81
+ /**
82
+ * Injectable secret fetcher (tests). Defaults to an
83
+ * @aws-sdk/client-secrets-manager GetSecretValue call.
84
+ */
85
+ fetchSecret?: (secretName: string) => Promise<string | undefined>;
86
+ /** Edge-config cache TTL (ms). Default 60s. */
87
+ configTtlMs?: number;
88
+ /** Time budgets (ms). */
89
+ budgets?: {
90
+ /** Fire-and-forget beacon budget. Default 300. */
91
+ beaconMs?: number;
92
+ /** verify-access budget in observe mode. Default 1500. */
93
+ verifyObserveMs?: number;
94
+ /** verify-access budget in enforce mode. Default 3000. */
95
+ verifyEnforceMs?: number;
96
+ };
97
+ /**
98
+ * Compile-time fallback config used before the first successful
99
+ * edge-config fetch and whenever the backend is unreachable with a cold
100
+ * cache. Defaults to the SDK's DEFAULT_EDGE_CONFIG (observe/classify).
101
+ */
102
+ fallbackConfig?: EdgeConfig;
103
+ /** Random source for beacon sampling — injectable for tests. */
104
+ random?: () => number;
105
+ /** Structured log sink. Defaults to console. */
106
+ log?: (level: 'info' | 'warn' | 'error', message: string, data?: Record<string, unknown>) => void;
107
+ }
108
+
109
+ /**
110
+ * Commerce Shield edge shell — Lambda@Edge origin-request handler factory
111
+ * (§7.4 / §33.5 #1).
112
+ *
113
+ * Composition per request:
114
+ * config (cached) → path policy → classify → tier pipeline → apply outcome
115
+ *
116
+ * THE invariant (tested): outside enforce mode the handler ALWAYS returns
117
+ * the origin-bound request — any error, timeout, or misconfiguration
118
+ * results in pass-through. A top-level catch guarantees it even against
119
+ * bugs in the shell itself.
120
+ */
121
+
122
+ type EdgeHandler = (event: CloudFrontRequestEvent) => Promise<CloudFrontRequestResult>;
123
+ declare function createEdgeHandler(options: EdgeAdapterOptions): EdgeHandler;
124
+
125
+ /**
126
+ * Three-tier traffic classification (§33.5 #1: verified /
127
+ * identified-but-unregistered / anonymous) plus the two non-tiers the shell
128
+ * touches as little as possible: `human` (zero network calls, zero logging)
129
+ * and `skipped` (static assets / excluded paths).
130
+ *
131
+ * Cheapest checks first — the human path must add microseconds, not
132
+ * milliseconds. Platform signatures come from the SDK so the edge and the
133
+ * backend's verify-access anonymous handler classify identically.
134
+ */
135
+
136
+ /**
137
+ * Resolve the effective mode/depth for a path from the EdgeConfig — ordered
138
+ * first-match-wins rules with case-insensitive globs; no match falls through
139
+ * to the config's top-level defaults. Static extensions are always skipped
140
+ * regardless of rules (a rule can only skip MORE, never un-skip an asset).
141
+ */
142
+ declare function resolvePolicy(config: EdgeConfig, path: string): EffectivePolicy;
143
+ /**
144
+ * Classify one request. Order (cheapest sufficient evidence wins):
145
+ * 1. X-Astra-* credentials → `verified` candidate
146
+ * 2. commerce-protocol credentials on the wire → `identified`
147
+ * 3. platform UA fingerprint (Claude/ChatGPT/Gemini/…) → `identified`
148
+ * 4. bot-shaped UA / headerless non-browser → `anonymous-bot`
149
+ * 5. everything browser-shaped → `human`
150
+ *
151
+ * `depth: 'classify'` skips step 2 (no body decode, no header sniffing
152
+ * beyond UA) — that is the zero-friction setting.
153
+ */
154
+ declare function classify(request: CloudFrontRequest, host: string, depth: 'classify' | 'authenticate' | 'authorize'): Classification;
155
+
156
+ /**
157
+ * CloudFront origin-request event helpers. The Lambda@Edge event shape puts
158
+ * headers in `{ key, value }[]` arrays keyed by LOWERCASED header name, and
159
+ * (with IncludeBody) the body arrives base64-encoded with an
160
+ * `inputTruncated` flag when it exceeded CloudFront's ~1MB exposure cap.
161
+ */
162
+
163
+ declare function getRequest(event: CloudFrontRequestEvent): CloudFrontRequest;
164
+ /** First value of a header (lowercase name), or undefined. */
165
+ declare function getHeader(request: CloudFrontRequest, name: string): string | undefined;
166
+ /**
167
+ * Flatten CloudFront headers into the `Record<string, string | string[]>`
168
+ * shape the SDK extractors and verify-access artifacts expect.
169
+ */
170
+ declare function flattenHeaders(headers: CloudFrontHeaders): Record<string, string | string[]>;
171
+ interface DecodedBody {
172
+ /** UTF-8 body text, undefined when there is no body. */
173
+ text?: string;
174
+ /** True when CloudFront truncated the body before handing it to the trigger. */
175
+ truncated: boolean;
176
+ }
177
+ declare function decodeBody(request: CloudFrontRequest): DecodedBody;
178
+ /** Full request URL as the origin will see it (scheme fixed to https). */
179
+ declare function requestUrl(request: CloudFrontRequest, host: string): string;
180
+ /**
181
+ * Set a header on the origin-bound request (overwrites all prior values).
182
+ */
183
+ declare function setHeader(request: CloudFrontRequest, name: string, value: string): void;
184
+ /**
185
+ * Strip every inbound header matching a prefix (lowercase compare). Used to
186
+ * drop spoofed `x-astrasync-*` trust headers before the shell injects its
187
+ * own — in BOTH modes, so a caller can never smuggle trust past the origin.
188
+ */
189
+ declare function stripHeadersByPrefix(request: CloudFrontRequest, prefix: string): void;
190
+ declare function jsonResponse(status: number, body: unknown, extraHeaders?: Record<string, string>): CloudFrontResultResponse;
191
+
192
+ /**
193
+ * Crypto-free commerce-protocol detection (§7.4.2 detection order).
194
+ *
195
+ * The edge only SNIFFS — header shapes, compact-JWT patterns, body field
196
+ * names — and packages what it saw as raw `CommerceArtifactsPayload` for
197
+ * verify-access, whose Step 0.5 pipeline does the authoritative parsing and
198
+ * cryptographic verification server-side. Nothing here imports jose/sd-jwt/
199
+ * signature libraries: detection must stay cheap enough to run on every
200
+ * non-human request, and verification must happen exactly once, in the
201
+ * canonical sink, where it is recorded.
202
+ */
203
+
204
+ interface DetectedCommerce {
205
+ /** Detection-order label: rfc9421-agent-pay | rfc9421-tap | rfc9421-web-bot-auth | vi-sd-jwt | ap2 | acp | ucp | mpp | x402 */
206
+ protocol: string;
207
+ artifacts: CommerceArtifactsPayload;
208
+ /** Light detection detail for beacons/audit — never secrets or full credentials. */
209
+ evidence: Record<string, unknown>;
210
+ }
211
+ /**
212
+ * §7.4.2 order (after the X-Astra native check, which classify.ts owns):
213
+ * SD-JWT/VI → RFC 9421 (Agent Pay / TAP / Web Bot Auth by kid namespace or
214
+ * Signature-Agent) → ACP HMAC → UCP session → AP2 body triple → MPP → x402.
215
+ * Returns undefined when nothing commerce-shaped is on the wire.
216
+ *
217
+ * When the body was truncated by CloudFront (`inputTruncated`), body-bound
218
+ * artifacts still forward what arrived but carry a `bodyTruncated` evidence
219
+ * flag — the backend downgrades those signatures rather than hard-failing.
220
+ */
221
+ declare function detectCommerce(request: CloudFrontRequest, host: string, body: DecodedBody): DetectedCommerce | undefined;
222
+
223
+ /**
224
+ * Depth-controlled verification per tier. The edge never verifies
225
+ * credentials itself — it forwards what it detected to verify-access, the
226
+ * canonical sole verification sink (backend Step 0.5 runs the commerce
227
+ * pipeline, records events, provisions orphans, persists sessions). What
228
+ * the depth setting controls is how much the edge LOOKS and how much it
229
+ * FORWARDS:
230
+ *
231
+ * classify — what is it? UA tiers only. Zero verify-access calls;
232
+ * beacons only.
233
+ * authenticate — who is it? + protocol detection. X-Astra agents get a
234
+ * verify-access identity check; commerce/platform traffic
235
+ * is beaconed with protocol + evidence (no signature
236
+ * verification anywhere).
237
+ * authorize — are they allowed? + artifact forwarding. X-Astra verify
238
+ * calls carry commerceArtifacts; identified traffic gets an
239
+ * anonymous verify-access call with artifacts +
240
+ * callerMetadata so the backend cryptographically proves
241
+ * and policy-evaluates once.
242
+ */
243
+
244
+ /**
245
+ * Run the tier-appropriate verification/telemetry for one classified
246
+ * request. NEVER throws; on any failure returns an `allow` outcome with the
247
+ * failure recorded in `reasons` (infra failures fail open — a presented
248
+ * credential the BACKEND rejected is a deny, but an unreachable backend is
249
+ * not a rejection).
250
+ */
251
+ declare function runTier(options: EdgeAdapterOptions, request: CloudFrontRequest, classification: Classification, policy: EffectivePolicy, apiKey: string | undefined, sampling: {
252
+ anonymousBeaconRate: number;
253
+ }): Promise<TierOutcome>;
254
+
255
+ /**
256
+ * Decision → CloudFront result wiring (§7.4 enforcement, §7.4.7 ASAT
257
+ * headers). Executed in BOTH modes so observe mode records exactly what
258
+ * enforce mode would have done — but only enforce mode is allowed to return
259
+ * anything other than the origin-bound request.
260
+ */
261
+
262
+ /** §7.4.7 ASAT trust-attestation headers injected on allowed requests. */
263
+ declare const ASAT_HEADERS: {
264
+ readonly trustScore: "X-AstraSync-Trust-Score";
265
+ readonly agentId: "X-AstraSync-Agent-Id";
266
+ readonly developerId: "X-AstraSync-Developer-Id";
267
+ readonly permissions: "X-AstraSync-Permissions";
268
+ readonly observedDecision: "X-AstraSync-Observed-Decision";
269
+ };
270
+ /**
271
+ * Apply the outcome. Always strips inbound spoofed `x-astrasync-*` headers
272
+ * first (both modes — trust headers must only ever originate here).
273
+ *
274
+ * Observe mode: returns the request untouched apart from header hygiene and
275
+ * an `X-AstraSync-Observed-Decision` stamp for origin-side log correlation.
276
+ *
277
+ * Enforce mode: ALLOW → request + ASAT headers; DENY → 403 machine-readable
278
+ * body; MANUAL_REVIEW → 202 with step-up poll info.
279
+ */
280
+ declare function applyOutcome(options: EdgeAdapterOptions, request: CloudFrontRequest, outcome: TierOutcome, policy: EffectivePolicy): CloudFrontRequestResult;
281
+
282
+ /**
283
+ * §7.4.6 — protocol-specific machine-readable messaging for enforce-mode
284
+ * responses. Agents (not humans) consume these bodies: stable error codes,
285
+ * the failure list, and registration guidance with real URLs.
286
+ */
287
+
288
+ interface DenyBody {
289
+ success: false;
290
+ error: {
291
+ code: 'UNAUTHORIZED' | 'INSUFFICIENT_ACCESS';
292
+ message: string;
293
+ protocol?: string;
294
+ failures: string[];
295
+ correlationId?: string;
296
+ guidance: {
297
+ message?: string;
298
+ registrationUrl?: string;
299
+ documentationUrl?: string;
300
+ };
301
+ };
302
+ }
303
+ declare function buildDenyBody(outcome: TierOutcome, apiBaseUrl: string): DenyBody;
304
+ interface ManualReviewBody {
305
+ success: false;
306
+ status: 'manual_review';
307
+ message: string;
308
+ stepUpApproval?: {
309
+ pollUrl?: string;
310
+ expiresAt?: string;
311
+ };
312
+ correlationId?: string;
313
+ }
314
+ declare function buildManualReviewBody(outcome: TierOutcome): ManualReviewBody;
315
+
316
+ /**
317
+ * Time-budgeted event emission.
318
+ *
319
+ * Lambda@Edge has no `waitUntil` — anything not awaited before the handler
320
+ * returns may never run (the container freezes). So every network call is
321
+ * awaited under an explicit budget and DROPPED on timeout: latency to the
322
+ * customer's traffic is bounded, and a slow AstraSync API costs telemetry,
323
+ * never page loads.
324
+ */
325
+
326
+ /** Await `promise` for at most `ms`; resolve `undefined` on timeout/error. */
327
+ declare function withBudget<T>(promise: Promise<T>, ms: number): Promise<T | undefined>;
328
+ /**
329
+ * Emit an unregistered-attempt beacon for `identified` (non-crypto depths)
330
+ * and sampled `anonymous-bot` traffic. Unauthenticated by design
331
+ * (CSRF-exempt on the backend); the backend fingerprints the UA and
332
+ * find-or-provisions orphan platform agents server-side.
333
+ */
334
+ declare function emitBeacon(options: EdgeAdapterOptions, request: CloudFrontRequest, classification: Classification, budgetMs: number): Promise<void>;
335
+ /** Deterministic-in-tests sampling gate. */
336
+ declare function sampled(rate: number, random?: () => number): boolean;
337
+
338
+ /**
339
+ * Runtime configuration for the edge shell.
340
+ *
341
+ * Lambda@Edge forbids environment variables, so configuration is layered:
342
+ * 1. Build-time constants baked into the bundle (apiBaseUrl,
343
+ * counterpartyId, counterpartyUrl, apiKeySecretName) — see
344
+ * `tsup.config.ts` define block and `infra/deploy.sh`.
345
+ * 2. The merchant API key from AWS Secrets Manager (us-east-1), fetched
346
+ * lazily once per container and cached with a 15-minute TTL so
347
+ * rotation needs no redeploy. Observe mode works fully without it —
348
+ * the beacon endpoint is unauthenticated and verify() degrades
349
+ * keyless — so a Secrets Manager outage can never take the shell down.
350
+ * 3. The dashboard EdgeConfig via the SDK's getEdgeConfig (60s TTL,
351
+ * stale-while-revalidate, enforce→observe degradation after 24h
352
+ * staleness, DEFAULT_EDGE_CONFIG fallback — all in the SDK).
353
+ */
354
+
355
+ /**
356
+ * Resolve the merchant API key. Never throws — a missing/unreachable secret
357
+ * degrades to keyless operation (observe mode is fully functional without
358
+ * it; enforce mode without a key cannot fetch a config that enforces, so it
359
+ * degrades to observe by construction).
360
+ */
361
+ declare function resolveApiKey(options: EdgeAdapterOptions): Promise<string | undefined>;
362
+ /**
363
+ * Load the effective EdgeConfig. Delegates caching/fallback/degradation to
364
+ * the SDK; adds the compile-time fallback override and never throws.
365
+ */
366
+ declare function loadEdgeConfig(options: EdgeAdapterOptions, apiKey: string | undefined): Promise<EdgeConfig>;
367
+ /** Reset module caches — tests only. */
368
+ declare function _resetConfigCaches(): void;
369
+
370
+ export { ASAT_HEADERS, type Classification, type DetectedCommerce, type EdgeAdapterOptions, type EdgeDecision, type EdgeHandler, type EffectivePolicy, type TierOutcome, type TrafficTier, _resetConfigCaches, applyOutcome, buildDenyBody, buildManualReviewBody, classify, createEdgeHandler, decodeBody, detectCommerce, emitBeacon, flattenHeaders, getHeader, getRequest, jsonResponse, loadEdgeConfig, requestUrl, resolveApiKey, resolvePolicy, runTier, sampled, setHeader, stripHeadersByPrefix, withBudget };