@walkeros/core 4.4.0-next-1785285516008 → 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,6 +1,39 @@
1
1
  # @walkeros/core
2
2
 
3
- ## 4.4.0-next-1785285516008
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
+
36
+ ## 4.4.0
4
37
 
5
38
  ### Minor Changes
6
39
 
@@ -11,6 +44,19 @@
11
44
  `parentEventId`, instead of folding them into a single row. A new optional
12
45
  `unattributed` summary reports records the assembly could not attribute to any
13
46
  event instead of dropping them silently.
47
+ - 034b1de: Events pushed before the collector runs are now held and replayed at
48
+ run instead of being dropped, bounded by `queueMax`. `walker init <element>`
49
+ re-fires load triggers for already-tracked elements, restoring the SPA re-init
50
+ pattern. Queue sources start consuming at run and no longer acknowledge
51
+ messages the pipeline did not accept.
52
+ - d00e2bd: A source's `env` is now unambiguously the author's dependency bag:
53
+ the collector-provided `push`, `elb`, `command`, `logger` and `sources` always
54
+ win, so setting them in `env` has no effect. Sources that need the pipeline to
55
+ end elsewhere declare the new `terminus`, which receives the raw event and
56
+ skips the pipeline entirely; previously an `env.push` silently replaced the
57
+ pipeline's end and disabled some, but not all, of its stages, and flows that
58
+ set it should move to `terminus`. Stored flows now also reject unknown keys on
59
+ a source entry at validation instead of silently ignoring them.
14
60
 
15
61
  ### Patch Changes
16
62
 
package/dist/dev.d.mts CHANGED
@@ -3867,12 +3867,15 @@ declare namespace collector {
3867
3867
  * - sources: Other registered sources
3868
3868
  * - elb: Public API function (alias for collector.push)
3869
3869
  *
3870
- * Platform-specific sources extend this with their requirements
3871
- * (e.g., window, document, fetch, req, res)
3870
+ * The capability keys above (plus logger) are runtime-owned: the collector
3871
+ * applies them last, so author values for them are ignored. Platform-specific
3872
+ * sources extend the env with their own requirements (e.g., window, document,
3873
+ * fetch, req, res), and those author dependencies pass through untouched.
3872
3874
  *
3873
3875
  * This makes sources:
3874
3876
  * - Platform-agnostic (no direct dependencies)
3875
- * - Testable (mock env for tests)
3877
+ * - Testable (mock author dependencies via env; mock the collector boundary
3878
+ * via a hand-built Context or InitSource.terminus)
3876
3879
  * - Composable (share env between sources)
3877
3880
  */
3878
3881
  declare const BaseEnvSchema: z.ZodObject<{
@@ -3987,7 +3990,7 @@ declare const ConfigSchema$3: z.ZodObject<{
3987
3990
  }>]>>;
3988
3991
  handler: z.ZodOptional<z.ZodAny>;
3989
3992
  }, z.core.$strip>>;
3990
- async: z.ZodOptional<z.ZodBoolean>;
3993
+ async: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>;
3991
3994
  setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
3992
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>>>]>>;
3993
3996
  disabled: z.ZodOptional<z.ZodBoolean>;
@@ -4107,7 +4110,7 @@ declare const PartialConfigSchema$2: z.ZodObject<{
4107
4110
  }>]>>;
4108
4111
  handler: z.ZodOptional<z.ZodAny>;
4109
4112
  }, z.core.$strip>>>;
4110
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4113
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4111
4114
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4112
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>>>]>>>;
4113
4116
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -4243,7 +4246,7 @@ declare const InstanceSchema: z.ZodObject<{
4243
4246
  }>]>>;
4244
4247
  handler: z.ZodOptional<z.ZodAny>;
4245
4248
  }, z.core.$strip>>;
4246
- async: z.ZodOptional<z.ZodBoolean>;
4249
+ async: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>;
4247
4250
  setup: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>;
4248
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>>>]>>;
4249
4252
  disabled: z.ZodOptional<z.ZodBoolean>;
@@ -4388,7 +4391,7 @@ declare const InitSourceSchema: z.ZodObject<{
4388
4391
  }>]>>;
4389
4392
  handler: z.ZodOptional<z.ZodAny>;
4390
4393
  }, z.core.$strip>>>;
4391
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4394
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4392
4395
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4393
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>>>]>>>;
4394
4397
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -4531,7 +4534,7 @@ declare const InitSourcesSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
4531
4534
  }>]>>;
4532
4535
  handler: z.ZodOptional<z.ZodAny>;
4533
4536
  }, z.core.$strip>>>;
4534
- async: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
4537
+ async: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodBoolean>]>>>;
4535
4538
  setup: z.ZodOptional<z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodUnknown>]>>>;
4536
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>>>]>>>;
4537
4540
  disabled: z.ZodOptional<z.ZodOptional<z.ZodBoolean>>;
@@ -5112,7 +5115,7 @@ declare const SourceSchema: z.ZodObject<{
5112
5115
  update: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
5113
5116
  }, z.core.$strip>>;
5114
5117
  }, z.core.$strip>>;
5115
- }, z.core.$strip>;
5118
+ }, z.core.$strict>;
5116
5119
  /**
5117
5120
  * Transformer reference schema (Flow.Transformer).
5118
5121
  */
@@ -5419,7 +5422,7 @@ declare const FlowSchema: z.ZodObject<{
5419
5422
  update: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
5420
5423
  }, z.core.$strip>>;
5421
5424
  }, z.core.$strip>>;
5422
- }, z.core.$strip>>>;
5425
+ }, z.core.$strict>>>;
5423
5426
  destinations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
5424
5427
  package: z.ZodOptional<z.ZodString>;
5425
5428
  code: z.ZodOptional<z.ZodObject<{
@@ -5643,7 +5646,7 @@ declare const JsonSchema$1: z.ZodObject<{
5643
5646
  update: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
5644
5647
  }, z.core.$strip>>;
5645
5648
  }, z.core.$strip>>;
5646
- }, z.core.$strip>>>;
5649
+ }, z.core.$strict>>>;
5647
5650
  destinations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
5648
5651
  package: z.ZodOptional<z.ZodString>;
5649
5652
  code: z.ZodOptional<z.ZodObject<{
@@ -6278,7 +6281,7 @@ declare const configJsonSchema: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<
6278
6281
  update: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
6279
6282
  }, z.core.$strip>>;
6280
6283
  }, z.core.$strip>>;
6281
- }, z.core.$strip>>>;
6284
+ }, z.core.$strict>>>;
6282
6285
  destinations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
6283
6286
  package: z.ZodOptional<z.ZodString>;
6284
6287
  code: z.ZodOptional<z.ZodObject<{
@@ -7678,6 +7681,14 @@ interface SourceStatus {
7678
7681
  count: number;
7679
7682
  lastAt?: number;
7680
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;
7681
7692
  }
7682
7693
  interface DestinationStatus {
7683
7694
  count: number;
@@ -7796,6 +7807,15 @@ interface Instance$5 {
7796
7807
  logger: Instance$3;
7797
7808
  on: OnConfig;
7798
7809
  queue: Events;
7810
+ /**
7811
+ * Events pushed while `allowed === false`, held raw (pre-pipeline) and
7812
+ * replayed FIFO by the run command. Bounded by `config.queueMax`
7813
+ * (drop-oldest). RECORD-immediate/DELIVER-gated, extended to events.
7814
+ */
7815
+ preRunQueue: Array<{
7816
+ event: DeepPartialEvent;
7817
+ options: PushOptions;
7818
+ }>;
7799
7819
  round: number;
7800
7820
  /** Run-scoped W3C trace id, minted on each run and stamped onto events. */
7801
7821
  trace?: string;
@@ -8288,6 +8308,10 @@ interface PushResult {
8288
8308
  failed?: Record<string, Ref>;
8289
8309
  /** Event was intentionally not forwarded: a transformer chain stopped it. */
8290
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;
8291
8315
  }
8292
8316
 
8293
8317
  type AnyFunction<P extends unknown[] = never[], R = unknown> = (...args: P) => R;
@@ -8665,8 +8689,12 @@ interface InitTransformers {
8665
8689
  /**
8666
8690
  * Base Env interface for dependency injection into sources.
8667
8691
  *
8668
- * Sources receive all their dependencies through this environment object,
8669
- * making them platform-agnostic and easily testable.
8692
+ * `env` is the author's dependency bag: platform and vendor dependencies are
8693
+ * injected here, making sources platform-agnostic. The five capabilities the
8694
+ * collector provides (`push`, `command`, `sources`, `elb`, `logger`) are
8695
+ * runtime-owned and applied last, so author values for them are ignored. To
8696
+ * mock the collector boundary in a test, call the factory directly with a
8697
+ * hand-built `Context`, or declare `InitSource.terminus`.
8670
8698
  */
8671
8699
  interface BaseEnv$1 {
8672
8700
  [key: string]: unknown;
@@ -8733,17 +8761,18 @@ interface Config$1<T extends TypesGeneric$1 = Types$1> extends Config$7<Mapping<
8733
8761
  /**
8734
8762
  * Respond-first acknowledgement for response-producing server sources.
8735
8763
  *
8736
- * When a source produces an HTTP response (express today; future fetch /
8737
- * lambda), `async: true` (the default for such sources) responds 2xx
8738
- * ("accepted") before the event is delivered to the collector, so the
8739
- * client is not blocked on backend delivery. `async: false` waits for
8740
- * delivery to settle before responding. A 2xx means "accepted", not
8741
- * "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".
8742
8771
  *
8743
8772
  * Browser and dataLayer sources have no HTTP response to defer and ignore
8744
- * this flag. The default is per source type.
8773
+ * this flag. Defaults are per source type.
8745
8774
  */
8746
- async?: boolean;
8775
+ async?: boolean | Record<string, boolean>;
8747
8776
  /** Mark as primary source; its push function becomes the exported `elb` from startFlow. */
8748
8777
  primary?: boolean;
8749
8778
  /** Defer source initialization until these collector events fire (e.g., `['consent']`). */
@@ -8755,14 +8784,15 @@ interface Config$1<T extends TypesGeneric$1 = Types$1> extends Config$7<Mapping<
8755
8784
  setup?: boolean | SetupOptions$1<T>;
8756
8785
  /**
8757
8786
  * Ingest metadata extraction mapping.
8758
- * Extracts values from raw request objects (Express req, Lambda event, etc.)
8759
- * 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.
8760
8790
  *
8761
8791
  * @example
8762
8792
  * ingest: {
8763
- * ip: 'req.ip',
8764
- * ua: 'req.headers.user-agent',
8765
- * origin: 'req.headers.origin'
8793
+ * ip: 'ip',
8794
+ * ua: 'headers.user-agent',
8795
+ * origin: 'headers.origin'
8766
8796
  * }
8767
8797
  */
8768
8798
  ingest?: Data$1;
@@ -8821,6 +8851,36 @@ type ScopeEnv<T extends TypesGeneric$1 = Types$1> = Env$1<T> & {
8821
8851
  /** Respond function bound to this scope (undefined for scopes without a response). */
8822
8852
  respond?: RespondFn;
8823
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
+ }
8824
8884
  /**
8825
8885
  * Context provided to source init function.
8826
8886
  * Extends base context with source-specific properties.
@@ -8830,29 +8890,32 @@ interface Context$1<T extends TypesGeneric$1 = Types$1> extends Base<Partial<Con
8830
8890
  /**
8831
8891
  * Bind ingest and respond to a single scope of work (e.g. one inbound
8832
8892
  * HTTP request, one queue message). Builds a fresh `Ingest` from the
8833
- * raw input via `config.ingest` mapping, wires the per-scope `respond`,
8834
- * 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.
8835
8896
  *
8836
- * 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:
8837
8899
  *
8838
8900
  * ```ts
8839
- * await context.withScope(req, createRespond(sender), async (env) => {
8840
- * 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);
8841
8905
  * });
8842
8906
  * ```
8843
8907
  *
8844
8908
  * Browser sources with a single tab-lifetime scope may skip `withScope`
8845
8909
  * and use `env.push` directly.
8846
8910
  *
8847
- * @param rawScope - Raw input for `config.ingest` mapping (Express req,
8848
- * Lambda event, fetch Request, etc.). Pass `undefined` if no ingest
8849
- * mapping applies.
8911
+ * @param rawScope - The normalized scope the source built from its
8912
+ * platform's request. Pass `undefined` if no ingest mapping applies.
8850
8913
  * @param respond - Per-scope respond function, or `undefined` if the
8851
8914
  * scope produces no response.
8852
8915
  * @param body - Async callback receiving the per-scope env.
8853
8916
  * @returns The body's return value.
8854
8917
  */
8855
- 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>;
8856
8919
  }
8857
8920
  type Init$1<T extends TypesGeneric$1 = Types$1> = (context: Context$1<T>) => Instance$1<T> | Promise<Instance$1<T>>;
8858
8921
  type InitSource<T extends TypesGeneric$1 = Types$1> = {
@@ -8862,6 +8925,21 @@ type InitSource<T extends TypesGeneric$1 = Types$1> = {
8862
8925
  primary?: boolean;
8863
8926
  next?: Route;
8864
8927
  before?: Route;
8928
+ /**
8929
+ * Replace the collector at the END of this source's pipeline.
8930
+ *
8931
+ * A terminus receives the event exactly as the source emitted it and the
8932
+ * entire pipeline is skipped: no `before`/`next` chains, no source `cache`,
8933
+ * no `state`, no `mapping`, no minted span id, no per-source ingest, no
8934
+ * `respond`, no `status.sources` counting and no observability records.
8935
+ *
8936
+ * This is a TOTAL bypass, deliberately, so there is one thing to know rather
8937
+ * than a list of exceptions. It exists for deterministic capture at the
8938
+ * source→collector boundary (step examples, dev tooling). Production flows
8939
+ * leave it unset. Runtime-only: stored-flow validation rejects unknown
8940
+ * source keys, so a persisted flow cannot carry it.
8941
+ */
8942
+ terminus?: PushFn$1;
8865
8943
  cache?: Cache;
8866
8944
  state?: State | State[];
8867
8945
  };