@walkeros/core 4.4.0 → 4.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @walkeros/core
2
2
 
3
+ ## 4.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 63845bb: The Express source's `async` option now resolves per HTTP method: a
8
+ boolean still applies to the whole source, and a record like `{ "GET": true }`
9
+ or `{ "POST": false }` overrides one method while the other keeps its default.
10
+ The default changed: GET is now synchronous, so a step such as the file
11
+ transformer or a cache can serve real content instead of the tracking GIF,
12
+ while POST keeps the fast respond-first acknowledgement. To restore
13
+ respond-first GET set `async: true` or `async: { "GET": true }`; configs that
14
+ set `async: false` only to fix asset serving can drop it.
15
+
16
+ ### Patch Changes
17
+
18
+ - 79cdcb0: Server sources answer rejected client input with 4xx JSON instead of
19
+ unhandled errors or 500s: unparseable bodies return 400 at the HTTP boundary,
20
+ and invalid events return 400 with the reason wherever the response is still
21
+ open (synchronous handling; in the express default respond-first mode the 200
22
+ ack has already been sent, so the rejection surfaces as a warn and a counter
23
+ instead) (`push` resolves `{ok: false, invalid: true, error}`). Genuine
24
+ pipeline failures return 500 instead of `success: true`. Invalid input now
25
+ counts on `collector.status.sources.<id>.rejected` instead of inflating
26
+ `status.failed`. The express source no longer sends `X-Powered-By` and now
27
+ sets `X-Content-Type-Options: nosniff` on every response.
28
+ - 756b571: Server sources now share one request scope and one event envelope, so
29
+ `config.ingest` paths and POST body forms behave identically on Express,
30
+ Fetch, Lambda and Cloud Functions. Batches and bare arrays are accepted
31
+ everywhere, and a destination before-chain fan-out no longer drops all but the
32
+ first event. Breaking: AWS `requestContext.*` moves under `raw.*`, Express
33
+ drops `protocol` and `hostname`, and Fetch `{ fn }` header mappings become
34
+ `{ key: 'headers.*' }`. See the migration guide.
35
+
3
36
  ## 4.4.0
4
37
 
5
38
  ### Minor Changes
package/dist/dev.d.mts CHANGED
@@ -3990,7 +3990,7 @@ declare const ConfigSchema$3: z.ZodObject<{
3990
3990
  }>]>>;
3991
3991
  handler: z.ZodOptional<z.ZodAny>;
3992
3992
  }, z.core.$strip>>;
3993
- async: z.ZodOptional<z.ZodBoolean>;
3993
+ async: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>;
3994
3994
  setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
3995
3995
  ingest: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>;
3996
3996
  disabled: z.ZodOptional<z.ZodBoolean>;
@@ -4110,7 +4110,7 @@ declare const PartialConfigSchema$2: z.ZodObject<{
4110
4110
  }>]>>;
4111
4111
  handler: z.ZodOptional<z.ZodAny>;
4112
4112
  }, z.core.$strip>>>;
4113
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4113
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4114
4114
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4115
4115
  ingest: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>>;
4116
4116
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -4246,7 +4246,7 @@ declare const InstanceSchema: z.ZodObject<{
4246
4246
  }>]>>;
4247
4247
  handler: z.ZodOptional<z.ZodAny>;
4248
4248
  }, z.core.$strip>>;
4249
- async: z.ZodOptional<z.ZodBoolean>;
4249
+ async: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>;
4250
4250
  setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
4251
4251
  ingest: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>;
4252
4252
  disabled: z.ZodOptional<z.ZodBoolean>;
@@ -4391,7 +4391,7 @@ declare const InitSourceSchema: z.ZodObject<{
4391
4391
  }>]>>;
4392
4392
  handler: z.ZodOptional<z.ZodAny>;
4393
4393
  }, z.core.$strip>>>;
4394
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4394
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4395
4395
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4396
4396
  ingest: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>>;
4397
4397
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -4534,7 +4534,7 @@ declare const InitSourcesSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
4534
4534
  }>]>>;
4535
4535
  handler: z.ZodOptional<z.ZodAny>;
4536
4536
  }, z.core.$strip>>>;
4537
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4537
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4538
4538
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4539
4539
  ingest: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>>;
4540
4540
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -7681,6 +7681,14 @@ interface SourceStatus {
7681
7681
  count: number;
7682
7682
  lastAt?: number;
7683
7683
  duration: number;
7684
+ /**
7685
+ * Monotonic count of inbound input rejected before entering the pipeline:
7686
+ * the source bumps it for HTTP-boundary rejections (unparseable body,
7687
+ * oversized payload, unsupported charset), the collector bumps it for
7688
+ * events rejected as invalid at the pipeline gate. Absent means no
7689
+ * rejections were tracked.
7690
+ */
7691
+ rejected?: number;
7684
7692
  }
7685
7693
  interface DestinationStatus {
7686
7694
  count: number;
@@ -8300,6 +8308,10 @@ interface PushResult {
8300
8308
  failed?: Record<string, Ref>;
8301
8309
  /** Event was intentionally not forwarded: a transformer chain stopped it. */
8302
8310
  dropped?: boolean;
8311
+ /** Event was rejected as invalid input: a client/producer fault, not a pipeline failure. */
8312
+ invalid?: boolean;
8313
+ /** Human-readable failure reason when ok is false. */
8314
+ error?: string;
8303
8315
  }
8304
8316
 
8305
8317
  type AnyFunction<P extends unknown[] = never[], R = unknown> = (...args: P) => R;
@@ -8749,17 +8761,18 @@ interface Config$1<T extends TypesGeneric$1 = Types$1> extends Config$7<Mapping<
8749
8761
  /**
8750
8762
  * Respond-first acknowledgement for response-producing server sources.
8751
8763
  *
8752
- * When a source produces an HTTP response (express today; future fetch /
8753
- * lambda), `async: true` (the default for such sources) responds 2xx
8754
- * ("accepted") before the event is delivered to the collector, so the
8755
- * client is not blocked on backend delivery. `async: false` waits for
8756
- * delivery to settle before responding. A 2xx means "accepted", not
8757
- * "delivered".
8764
+ * `true` responds 2xx ("accepted") before the event is delivered to the
8765
+ * collector, so the client is not blocked on backend delivery; `false`
8766
+ * waits for delivery to settle before responding and lets the response
8767
+ * reflect the outcome. A record configures this per source-defined key:
8768
+ * the express source keys it by HTTP method (`GET`/`POST`) and defaults
8769
+ * to `{ GET: false, POST: true }`, so a step can serve real content on
8770
+ * GET while POST acks fast. A 2xx means "accepted", not "delivered".
8758
8771
  *
8759
8772
  * Browser and dataLayer sources have no HTTP response to defer and ignore
8760
- * this flag. The default is per source type.
8773
+ * this flag. Defaults are per source type.
8761
8774
  */
8762
- async?: boolean;
8775
+ async?: boolean | Record<string, boolean>;
8763
8776
  /** Mark as primary source; its push function becomes the exported `elb` from startFlow. */
8764
8777
  primary?: boolean;
8765
8778
  /** Defer source initialization until these collector events fire (e.g., `['consent']`). */
@@ -8771,14 +8784,15 @@ interface Config$1<T extends TypesGeneric$1 = Types$1> extends Config$7<Mapping<
8771
8784
  setup?: boolean | SetupOptions$1<T>;
8772
8785
  /**
8773
8786
  * Ingest metadata extraction mapping.
8774
- * Extracts values from raw request objects (Express req, Lambda event, etc.)
8775
- * using walkerOS mapping syntax. Extracted data flows to transformers/destinations.
8787
+ * Extracts values from the normalized `Scope` the source built, using
8788
+ * walkerOS mapping syntax. Extracted data flows to transformers and
8789
+ * destinations. Paths resolve against the scope's fields, with no prefix.
8776
8790
  *
8777
8791
  * @example
8778
8792
  * ingest: {
8779
- * ip: 'req.ip',
8780
- * ua: 'req.headers.user-agent',
8781
- * origin: 'req.headers.origin'
8793
+ * ip: 'ip',
8794
+ * ua: 'headers.user-agent',
8795
+ * origin: 'headers.origin'
8782
8796
  * }
8783
8797
  */
8784
8798
  ingest?: Data$1;
@@ -8837,6 +8851,36 @@ type ScopeEnv<T extends TypesGeneric$1 = Types$1> = Env$1<T> & {
8837
8851
  /** Respond function bound to this scope (undefined for scopes without a response). */
8838
8852
  respond?: RespondFn;
8839
8853
  };
8854
+ /**
8855
+ * The normalized shape every source builds from its platform's request before
8856
+ * calling withScope. Downstream config.ingest mappings, transformers and
8857
+ * destinations read this shape and nothing else, so a mapping written once
8858
+ * resolves the same on every source.
8859
+ *
8860
+ * A value a platform does not supply is never guessed. `ip` is the one optional
8861
+ * field and is simply absent there; the required fields carry a documented
8862
+ * empty value instead (`url` is `''` when the platform cannot form one). `raw`
8863
+ * carries the untouched platform object for the cases normalization
8864
+ * deliberately does not cover.
8865
+ */
8866
+ interface Scope {
8867
+ /** Uppercase HTTP method. */
8868
+ method: string;
8869
+ /** Absolute request URL when the platform knows it, else ''. */
8870
+ url: string;
8871
+ /** Pathname only, no query string, leading slash. */
8872
+ path: string;
8873
+ /** Query parameters. Repeated keys joined with ','. */
8874
+ query: Record<string, string>;
8875
+ /** Header bag. Keys lowercased. Repeated values joined with ', '. */
8876
+ headers: Record<string, string>;
8877
+ /** Parsed body when it parses as JSON, the raw string when it does not, undefined when there is none. */
8878
+ body: unknown;
8879
+ /** Client IP as the platform reports it. Absent when the platform does not report one. */
8880
+ ip?: string;
8881
+ /** The untouched platform object. Escape hatch, never read by walkerOS itself. */
8882
+ raw: unknown;
8883
+ }
8840
8884
  /**
8841
8885
  * Context provided to source init function.
8842
8886
  * Extends base context with source-specific properties.
@@ -8846,29 +8890,32 @@ interface Context$1<T extends TypesGeneric$1 = Types$1> extends Base<Partial<Con
8846
8890
  /**
8847
8891
  * Bind ingest and respond to a single scope of work (e.g. one inbound
8848
8892
  * HTTP request, one queue message). Builds a fresh `Ingest` from the
8849
- * raw input via `config.ingest` mapping, wires the per-scope `respond`,
8850
- * and invokes `body(scopeEnv)` with a push function that captures both.
8893
+ * normalized scope via `config.ingest` mapping, wires the per-scope
8894
+ * `respond`, and invokes `body(scopeEnv)` with a push function that
8895
+ * captures both.
8851
8896
  *
8852
- * Server sources call this once per inbound request:
8897
+ * Server sources normalize their platform's request into a `Scope` first,
8898
+ * then call this once per inbound request:
8853
8899
  *
8854
8900
  * ```ts
8855
- * await context.withScope(req, createRespond(sender), async (env) => {
8856
- * await env.push(parsedData);
8901
+ * const scope = buildScope(req);
8902
+ *
8903
+ * await context.withScope(scope, createRespond(sender), async (env) => {
8904
+ * for (const event of toEventList(scope.body)) await env.push(event);
8857
8905
  * });
8858
8906
  * ```
8859
8907
  *
8860
8908
  * Browser sources with a single tab-lifetime scope may skip `withScope`
8861
8909
  * and use `env.push` directly.
8862
8910
  *
8863
- * @param rawScope - Raw input for `config.ingest` mapping (Express req,
8864
- * Lambda event, fetch Request, etc.). Pass `undefined` if no ingest
8865
- * mapping applies.
8911
+ * @param rawScope - The normalized scope the source built from its
8912
+ * platform's request. Pass `undefined` if no ingest mapping applies.
8866
8913
  * @param respond - Per-scope respond function, or `undefined` if the
8867
8914
  * scope produces no response.
8868
8915
  * @param body - Async callback receiving the per-scope env.
8869
8916
  * @returns The body's return value.
8870
8917
  */
8871
- withScope: <R>(rawScope: unknown, respond: RespondFn | undefined, body: (env: ScopeEnv<T>) => Promise<R>) => Promise<R>;
8918
+ withScope: <R>(rawScope: Scope | undefined, respond: RespondFn | undefined, body: (env: ScopeEnv<T>) => Promise<R>) => Promise<R>;
8872
8919
  }
8873
8920
  type Init$1<T extends TypesGeneric$1 = Types$1> = (context: Context$1<T>) => Instance$1<T> | Promise<Instance$1<T>>;
8874
8921
  type InitSource<T extends TypesGeneric$1 = Types$1> = {
package/dist/dev.d.ts CHANGED
@@ -3990,7 +3990,7 @@ declare const ConfigSchema$3: z.ZodObject<{
3990
3990
  }>]>>;
3991
3991
  handler: z.ZodOptional<z.ZodAny>;
3992
3992
  }, z.core.$strip>>;
3993
- async: z.ZodOptional<z.ZodBoolean>;
3993
+ async: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>;
3994
3994
  setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
3995
3995
  ingest: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>;
3996
3996
  disabled: z.ZodOptional<z.ZodBoolean>;
@@ -4110,7 +4110,7 @@ declare const PartialConfigSchema$2: z.ZodObject<{
4110
4110
  }>]>>;
4111
4111
  handler: z.ZodOptional<z.ZodAny>;
4112
4112
  }, z.core.$strip>>>;
4113
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4113
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4114
4114
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4115
4115
  ingest: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>>;
4116
4116
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -4246,7 +4246,7 @@ declare const InstanceSchema: z.ZodObject<{
4246
4246
  }>]>>;
4247
4247
  handler: z.ZodOptional<z.ZodAny>;
4248
4248
  }, z.core.$strip>>;
4249
- async: z.ZodOptional<z.ZodBoolean>;
4249
+ async: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>;
4250
4250
  setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
4251
4251
  ingest: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>;
4252
4252
  disabled: z.ZodOptional<z.ZodBoolean>;
@@ -4391,7 +4391,7 @@ declare const InitSourceSchema: z.ZodObject<{
4391
4391
  }>]>>;
4392
4392
  handler: z.ZodOptional<z.ZodAny>;
4393
4393
  }, z.core.$strip>>>;
4394
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4394
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4395
4395
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4396
4396
  ingest: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>>;
4397
4397
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -4534,7 +4534,7 @@ declare const InitSourcesSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
4534
4534
  }>]>>;
4535
4535
  handler: z.ZodOptional<z.ZodAny>;
4536
4536
  }, z.core.$strip>>>;
4537
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4537
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4538
4538
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4539
4539
  ingest: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>, z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>]>>>;
4540
4540
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -7681,6 +7681,14 @@ interface SourceStatus {
7681
7681
  count: number;
7682
7682
  lastAt?: number;
7683
7683
  duration: number;
7684
+ /**
7685
+ * Monotonic count of inbound input rejected before entering the pipeline:
7686
+ * the source bumps it for HTTP-boundary rejections (unparseable body,
7687
+ * oversized payload, unsupported charset), the collector bumps it for
7688
+ * events rejected as invalid at the pipeline gate. Absent means no
7689
+ * rejections were tracked.
7690
+ */
7691
+ rejected?: number;
7684
7692
  }
7685
7693
  interface DestinationStatus {
7686
7694
  count: number;
@@ -8300,6 +8308,10 @@ interface PushResult {
8300
8308
  failed?: Record<string, Ref>;
8301
8309
  /** Event was intentionally not forwarded: a transformer chain stopped it. */
8302
8310
  dropped?: boolean;
8311
+ /** Event was rejected as invalid input: a client/producer fault, not a pipeline failure. */
8312
+ invalid?: boolean;
8313
+ /** Human-readable failure reason when ok is false. */
8314
+ error?: string;
8303
8315
  }
8304
8316
 
8305
8317
  type AnyFunction<P extends unknown[] = never[], R = unknown> = (...args: P) => R;
@@ -8749,17 +8761,18 @@ interface Config$1<T extends TypesGeneric$1 = Types$1> extends Config$7<Mapping<
8749
8761
  /**
8750
8762
  * Respond-first acknowledgement for response-producing server sources.
8751
8763
  *
8752
- * When a source produces an HTTP response (express today; future fetch /
8753
- * lambda), `async: true` (the default for such sources) responds 2xx
8754
- * ("accepted") before the event is delivered to the collector, so the
8755
- * client is not blocked on backend delivery. `async: false` waits for
8756
- * delivery to settle before responding. A 2xx means "accepted", not
8757
- * "delivered".
8764
+ * `true` responds 2xx ("accepted") before the event is delivered to the
8765
+ * collector, so the client is not blocked on backend delivery; `false`
8766
+ * waits for delivery to settle before responding and lets the response
8767
+ * reflect the outcome. A record configures this per source-defined key:
8768
+ * the express source keys it by HTTP method (`GET`/`POST`) and defaults
8769
+ * to `{ GET: false, POST: true }`, so a step can serve real content on
8770
+ * GET while POST acks fast. A 2xx means "accepted", not "delivered".
8758
8771
  *
8759
8772
  * Browser and dataLayer sources have no HTTP response to defer and ignore
8760
- * this flag. The default is per source type.
8773
+ * this flag. Defaults are per source type.
8761
8774
  */
8762
- async?: boolean;
8775
+ async?: boolean | Record<string, boolean>;
8763
8776
  /** Mark as primary source; its push function becomes the exported `elb` from startFlow. */
8764
8777
  primary?: boolean;
8765
8778
  /** Defer source initialization until these collector events fire (e.g., `['consent']`). */
@@ -8771,14 +8784,15 @@ interface Config$1<T extends TypesGeneric$1 = Types$1> extends Config$7<Mapping<
8771
8784
  setup?: boolean | SetupOptions$1<T>;
8772
8785
  /**
8773
8786
  * Ingest metadata extraction mapping.
8774
- * Extracts values from raw request objects (Express req, Lambda event, etc.)
8775
- * using walkerOS mapping syntax. Extracted data flows to transformers/destinations.
8787
+ * Extracts values from the normalized `Scope` the source built, using
8788
+ * walkerOS mapping syntax. Extracted data flows to transformers and
8789
+ * destinations. Paths resolve against the scope's fields, with no prefix.
8776
8790
  *
8777
8791
  * @example
8778
8792
  * ingest: {
8779
- * ip: 'req.ip',
8780
- * ua: 'req.headers.user-agent',
8781
- * origin: 'req.headers.origin'
8793
+ * ip: 'ip',
8794
+ * ua: 'headers.user-agent',
8795
+ * origin: 'headers.origin'
8782
8796
  * }
8783
8797
  */
8784
8798
  ingest?: Data$1;
@@ -8837,6 +8851,36 @@ type ScopeEnv<T extends TypesGeneric$1 = Types$1> = Env$1<T> & {
8837
8851
  /** Respond function bound to this scope (undefined for scopes without a response). */
8838
8852
  respond?: RespondFn;
8839
8853
  };
8854
+ /**
8855
+ * The normalized shape every source builds from its platform's request before
8856
+ * calling withScope. Downstream config.ingest mappings, transformers and
8857
+ * destinations read this shape and nothing else, so a mapping written once
8858
+ * resolves the same on every source.
8859
+ *
8860
+ * A value a platform does not supply is never guessed. `ip` is the one optional
8861
+ * field and is simply absent there; the required fields carry a documented
8862
+ * empty value instead (`url` is `''` when the platform cannot form one). `raw`
8863
+ * carries the untouched platform object for the cases normalization
8864
+ * deliberately does not cover.
8865
+ */
8866
+ interface Scope {
8867
+ /** Uppercase HTTP method. */
8868
+ method: string;
8869
+ /** Absolute request URL when the platform knows it, else ''. */
8870
+ url: string;
8871
+ /** Pathname only, no query string, leading slash. */
8872
+ path: string;
8873
+ /** Query parameters. Repeated keys joined with ','. */
8874
+ query: Record<string, string>;
8875
+ /** Header bag. Keys lowercased. Repeated values joined with ', '. */
8876
+ headers: Record<string, string>;
8877
+ /** Parsed body when it parses as JSON, the raw string when it does not, undefined when there is none. */
8878
+ body: unknown;
8879
+ /** Client IP as the platform reports it. Absent when the platform does not report one. */
8880
+ ip?: string;
8881
+ /** The untouched platform object. Escape hatch, never read by walkerOS itself. */
8882
+ raw: unknown;
8883
+ }
8840
8884
  /**
8841
8885
  * Context provided to source init function.
8842
8886
  * Extends base context with source-specific properties.
@@ -8846,29 +8890,32 @@ interface Context$1<T extends TypesGeneric$1 = Types$1> extends Base<Partial<Con
8846
8890
  /**
8847
8891
  * Bind ingest and respond to a single scope of work (e.g. one inbound
8848
8892
  * HTTP request, one queue message). Builds a fresh `Ingest` from the
8849
- * raw input via `config.ingest` mapping, wires the per-scope `respond`,
8850
- * and invokes `body(scopeEnv)` with a push function that captures both.
8893
+ * normalized scope via `config.ingest` mapping, wires the per-scope
8894
+ * `respond`, and invokes `body(scopeEnv)` with a push function that
8895
+ * captures both.
8851
8896
  *
8852
- * Server sources call this once per inbound request:
8897
+ * Server sources normalize their platform's request into a `Scope` first,
8898
+ * then call this once per inbound request:
8853
8899
  *
8854
8900
  * ```ts
8855
- * await context.withScope(req, createRespond(sender), async (env) => {
8856
- * await env.push(parsedData);
8901
+ * const scope = buildScope(req);
8902
+ *
8903
+ * await context.withScope(scope, createRespond(sender), async (env) => {
8904
+ * for (const event of toEventList(scope.body)) await env.push(event);
8857
8905
  * });
8858
8906
  * ```
8859
8907
  *
8860
8908
  * Browser sources with a single tab-lifetime scope may skip `withScope`
8861
8909
  * and use `env.push` directly.
8862
8910
  *
8863
- * @param rawScope - Raw input for `config.ingest` mapping (Express req,
8864
- * Lambda event, fetch Request, etc.). Pass `undefined` if no ingest
8865
- * mapping applies.
8911
+ * @param rawScope - The normalized scope the source built from its
8912
+ * platform's request. Pass `undefined` if no ingest mapping applies.
8866
8913
  * @param respond - Per-scope respond function, or `undefined` if the
8867
8914
  * scope produces no response.
8868
8915
  * @param body - Async callback receiving the per-scope env.
8869
8916
  * @returns The body's return value.
8870
8917
  */
8871
- withScope: <R>(rawScope: unknown, respond: RespondFn | undefined, body: (env: ScopeEnv<T>) => Promise<R>) => Promise<R>;
8918
+ withScope: <R>(rawScope: Scope | undefined, respond: RespondFn | undefined, body: (env: ScopeEnv<T>) => Promise<R>) => Promise<R>;
8872
8919
  }
8873
8920
  type Init$1<T extends TypesGeneric$1 = Types$1> = (context: Context$1<T>) => Instance$1<T> | Promise<Instance$1<T>>;
8874
8921
  type InitSource<T extends TypesGeneric$1 = Types$1> = {