@toa.io/core 1.0.0-alpha.259 → 1.0.0-alpha.262

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
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
7
+
8
+
9
+ ### Features
10
+
11
+ * **core:** identify the origin of a call with `request.source` ([9da6b66](https://github.com/toa-io/toa/commit/9da6b66df852690351a1e82c7b8cc176fd89774a))
12
+
13
+
14
+
15
+
16
+
6
17
  # [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
7
18
 
8
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/core",
3
- "version": "1.0.0-alpha.259",
3
+ "version": "1.0.0-alpha.262",
4
4
  "description": "Toa Core",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -27,5 +27,5 @@
27
27
  "openspan": "1.0.0-alpha.254",
28
28
  "uuid": "11.1.1"
29
29
  },
30
- "gitHead": "3f557c2f855f078163128c317e7cb2f3629968aa"
30
+ "gitHead": "e14059cf5cc364d933f2a4b3d7114f3b5ec0764f"
31
31
  }
package/src/call.js CHANGED
@@ -8,12 +8,14 @@ const { Err } = require('error-value')
8
8
  class Call extends Connector {
9
9
  #transmitter
10
10
  #contract
11
+ #source
11
12
 
12
- constructor (transmitter, contract) {
13
+ constructor (transmitter, contract, source) {
13
14
  super()
14
15
 
15
16
  this.#transmitter = transmitter
16
17
  this.#contract = contract
18
+ this.#source = source
17
19
 
18
20
  this.depends(transmitter)
19
21
  }
@@ -21,6 +23,10 @@ class Call extends Connector {
21
23
  async invoke (request = {}) {
22
24
  request.input ??= null
23
25
 
26
+ // the caller may have attributed the call itself, as the node bridge does
27
+ if (this.#source !== undefined)
28
+ request.source ??= this.#source
29
+
24
30
  this.#contract.fit(request)
25
31
 
26
32
  // avoid validation on the recipient's side
package/src/context.js CHANGED
@@ -9,6 +9,7 @@ class Context extends Connector {
9
9
  env
10
10
  name
11
11
  aspects
12
+ locator
12
13
 
13
14
  #local
14
15
  #discover
@@ -20,6 +21,7 @@ class Context extends Connector {
20
21
  this.env = process.env.TOA_ENV
21
22
  this.name = process.env.TOA_CONTEXT
22
23
  this.aspects = aspects
24
+ this.locator = local?.locator
23
25
 
24
26
  this.#local = local
25
27
  this.#discover = discover
@@ -24,7 +24,11 @@ class Request extends Contract {
24
24
  static schema (definition, entity) {
25
25
  const schema = {
26
26
  type: 'object',
27
- properties: { authentic: { type: 'boolean' }, task: { type: 'boolean' } },
27
+ properties: {
28
+ authentic: { type: 'boolean' },
29
+ task: { type: 'boolean' },
30
+ source: structuredClone(schemas.source)
31
+ },
28
32
  additionalProperties: true
29
33
  }
30
34
 
@@ -5,3 +5,4 @@ const { load } = require('@toa.io/yaml')
5
5
 
6
6
  exports.query = load.sync(resolve(__dirname, './query.yaml'))
7
7
  exports.error = load.sync(resolve(__dirname, './error.yaml'))
8
+ exports.source = load.sync(resolve(__dirname, './source.yaml'))
@@ -0,0 +1,23 @@
1
+ # Origin of a call, see toa.core.Source.
2
+ # Deliberately permissive on required properties: the contract throws on a mismatch,
3
+ # and a strict union would turn a partially stamped source into a failed business call.
4
+ # `additionalProperties: false` combined with `removeAdditional` is what matters here —
5
+ # it strips whatever a peer puts in, bounding the cardinality of the resulting map.
6
+ type: object
7
+ properties:
8
+ namespace:
9
+ type: string
10
+ maxLength: 64
11
+ component:
12
+ type: string
13
+ maxLength: 64
14
+ operation:
15
+ type: string
16
+ maxLength: 64
17
+ event:
18
+ type: string
19
+ maxLength: 64
20
+ service:
21
+ type: string
22
+ maxLength: 64
23
+ additionalProperties: false
package/src/receiver.js CHANGED
@@ -26,6 +26,9 @@ class Receiver extends Connector {
26
26
  /** @type {unknown[]} */
27
27
  #arguments
28
28
 
29
+ /** @type {toa.core.Source} */
30
+ #origin
31
+
29
32
  #local
30
33
 
31
34
  #bridge
@@ -41,6 +44,7 @@ class Receiver extends Connector {
41
44
  this.#label = definition.label ?? operation
42
45
  this.#destination = definition.destination ?? this.#label
43
46
  this.#arguments = definition.arguments
47
+ this.#origin = definition.origin
44
48
 
45
49
  this.#local = local
46
50
  this.#bridge = bridge
@@ -59,6 +63,9 @@ class Receiver extends Connector {
59
63
 
60
64
  add(request, extensions)
61
65
 
66
+ // set after `add`, so that a message field can not spoof the origin
67
+ if (this.#origin !== undefined) request.source = this.#origin
68
+
62
69
  // continue the trace from the producer span
63
70
  const remote = telemetry === undefined ? null : decode(telemetry)
64
71
  const task = () => this.#process(request)
@@ -110,3 +110,42 @@ describe('schema', () => {
110
110
  expect(objects.limit).toBeDefined()
111
111
  })
112
112
  })
113
+
114
+ describe('source', () => {
115
+ const schemas = require('@toa.io/schemas')
116
+
117
+ const compile = (definition, entity) =>
118
+ schemas.schema(Request.schema(definition, entity), { removeAdditional: true })
119
+
120
+ it('should declare source', () => {
121
+ const schema = Request.schema({}, dummy)
122
+
123
+ expect(schema.properties.source).toBeDefined()
124
+ expect(schema.properties.source.additionalProperties).toStrictEqual(false)
125
+ })
126
+
127
+ it('should pass known source variants', () => {
128
+ const schema = compile({}, undefined)
129
+
130
+ for (const source of [
131
+ { namespace: 'a', component: 'b', operation: 'c' },
132
+ { namespace: 'a', component: 'b', event: 'c' },
133
+ { service: 'exposition' }
134
+ ]) {
135
+ const request = { input: null, query: null, source }
136
+
137
+ expect(schema.fit(request)).toStrictEqual(null)
138
+ expect(request.source).toStrictEqual(source)
139
+ }
140
+ })
141
+
142
+ // `source` crosses the wire and keys the introspection map, so whatever
143
+ // a peer adds to it must not survive
144
+ it('should strip unknown source properties', () => {
145
+ const schema = compile({}, undefined)
146
+ const request = { input: null, query: null, source: { service: 'exposition', evil: 'x' } }
147
+
148
+ expect(schema.fit(request)).toStrictEqual(null)
149
+ expect(request.source).toStrictEqual({ service: 'exposition' })
150
+ })
151
+ })
@@ -1,4 +1,5 @@
1
1
  import * as _request from './request'
2
+ import { Locator } from './locator'
2
3
  import * as _reply from './reply'
3
4
  import * as _extensions from './extensions'
4
5
  import * as _connector from './connector'
@@ -6,6 +7,8 @@ import * as _connector from './connector'
6
7
  export interface Context extends _connector.Connector{
7
8
  aspects: _extensions.Aspect[]
8
9
 
10
+ locator: Locator
11
+
9
12
  /**
10
13
  * Calls local endpoint
11
14
  */
@@ -20,7 +20,7 @@ export interface Factory {
20
20
 
21
21
  storage? (storage: _storages.Storage): _storages.Storage
22
22
 
23
- emitter? (emitter: _bindings.Emitter, label: string): _bindings.Emitter
23
+ emitter? (emitter: _bindings.Emitter, label: string, locator: _core.Locator): _bindings.Emitter
24
24
 
25
25
  receiver? (receiver: _core.Receiver, locator: _core.Locator): _core.Receiver
26
26
  }
package/types/index.ts CHANGED
@@ -12,4 +12,4 @@ export type { Exception } from './exception'
12
12
  export { Locator } from './locator'
13
13
  export type { Receiver } from './receiver'
14
14
  export type { Message } from './message'
15
- export type { Request, Query, Reply } from './request'
15
+ export type { Request, Query, Reply, Source } from './request'
@@ -2,6 +2,7 @@ import { Exception } from './exception'
2
2
 
3
3
  export interface Query {
4
4
  id?: string
5
+ ids?: Array<string>
5
6
  criteria?: string
6
7
  search?: string
7
8
  sample?: number
@@ -13,12 +14,21 @@ export interface Query {
13
14
  deleted?: boolean
14
15
  }
15
16
 
17
+ /**
18
+ * Origin of a call. Stamped by the caller, sanitized by the request contract.
19
+ */
20
+ export type Source =
21
+ | { namespace: string, component: string, operation: string }
22
+ | { namespace: string, component: string, event: string }
23
+ | { service: string }
24
+
16
25
  export interface Request {
17
26
  input?: any
18
27
  query?: Query
19
28
  authentic?: boolean
20
29
  task?: boolean
21
30
  telemetry?: string // W3C traceparent
31
+ source?: Source
22
32
  }
23
33
 
24
34
  export interface Reply {