@axonflow/sdk 9.2.0 → 9.3.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.
Files changed (76) hide show
  1. package/README.md +121 -0
  2. package/dist/cjs/adapters/langgraph.d.ts.map +1 -1
  3. package/dist/cjs/adapters/langgraph.js +20 -0
  4. package/dist/cjs/adapters/langgraph.js.map +1 -1
  5. package/dist/cjs/authzen.d.ts +35 -10
  6. package/dist/cjs/authzen.d.ts.map +1 -1
  7. package/dist/cjs/authzen.js +45 -19
  8. package/dist/cjs/authzen.js.map +1 -1
  9. package/dist/cjs/client.d.ts +69 -10
  10. package/dist/cjs/client.d.ts.map +1 -1
  11. package/dist/cjs/client.js +338 -52
  12. package/dist/cjs/client.js.map +1 -1
  13. package/dist/cjs/heartbeat.d.ts +55 -4
  14. package/dist/cjs/heartbeat.d.ts.map +1 -1
  15. package/dist/cjs/heartbeat.js +86 -7
  16. package/dist/cjs/heartbeat.js.map +1 -1
  17. package/dist/cjs/index.d.ts +3 -0
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js +12 -2
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/read-identity.d.ts +267 -0
  22. package/dist/cjs/read-identity.d.ts.map +1 -0
  23. package/dist/cjs/read-identity.js +329 -0
  24. package/dist/cjs/read-identity.js.map +1 -0
  25. package/dist/cjs/telemetry.d.ts +108 -0
  26. package/dist/cjs/telemetry.d.ts.map +1 -1
  27. package/dist/cjs/telemetry.js +318 -35
  28. package/dist/cjs/telemetry.js.map +1 -1
  29. package/dist/cjs/types/authzen.gen.d.ts +4 -2
  30. package/dist/cjs/types/authzen.gen.d.ts.map +1 -1
  31. package/dist/cjs/types/authzen.gen.js +10 -4
  32. package/dist/cjs/types/authzen.gen.js.map +1 -1
  33. package/dist/cjs/types/config.d.ts +43 -1
  34. package/dist/cjs/types/config.d.ts.map +1 -1
  35. package/dist/cjs/types/hitl.js +1 -1
  36. package/dist/cjs/types/hitl.js.map +1 -1
  37. package/dist/cjs/version.d.ts +1 -1
  38. package/dist/cjs/version.js +1 -1
  39. package/dist/esm/adapters/langgraph.d.ts.map +1 -1
  40. package/dist/esm/adapters/langgraph.js +20 -0
  41. package/dist/esm/adapters/langgraph.js.map +1 -1
  42. package/dist/esm/authzen.d.ts +35 -10
  43. package/dist/esm/authzen.d.ts.map +1 -1
  44. package/dist/esm/authzen.js +38 -14
  45. package/dist/esm/authzen.js.map +1 -1
  46. package/dist/esm/client.d.ts +69 -10
  47. package/dist/esm/client.d.ts.map +1 -1
  48. package/dist/esm/client.js +338 -52
  49. package/dist/esm/client.js.map +1 -1
  50. package/dist/esm/heartbeat.d.ts +55 -4
  51. package/dist/esm/heartbeat.d.ts.map +1 -1
  52. package/dist/esm/heartbeat.js +85 -7
  53. package/dist/esm/heartbeat.js.map +1 -1
  54. package/dist/esm/index.d.ts +3 -0
  55. package/dist/esm/index.d.ts.map +1 -1
  56. package/dist/esm/index.js +5 -0
  57. package/dist/esm/index.js.map +1 -1
  58. package/dist/esm/read-identity.d.ts +267 -0
  59. package/dist/esm/read-identity.d.ts.map +1 -0
  60. package/dist/esm/read-identity.js +320 -0
  61. package/dist/esm/read-identity.js.map +1 -0
  62. package/dist/esm/telemetry.d.ts +108 -0
  63. package/dist/esm/telemetry.d.ts.map +1 -1
  64. package/dist/esm/telemetry.js +313 -35
  65. package/dist/esm/telemetry.js.map +1 -1
  66. package/dist/esm/types/authzen.gen.d.ts +4 -2
  67. package/dist/esm/types/authzen.gen.d.ts.map +1 -1
  68. package/dist/esm/types/authzen.gen.js +8 -2
  69. package/dist/esm/types/authzen.gen.js.map +1 -1
  70. package/dist/esm/types/config.d.ts +43 -1
  71. package/dist/esm/types/config.d.ts.map +1 -1
  72. package/dist/esm/types/hitl.js +1 -1
  73. package/dist/esm/types/hitl.js.map +1 -1
  74. package/dist/esm/version.d.ts +1 -1
  75. package/dist/esm/version.js +1 -1
  76. package/package.json +1 -1
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ /**
3
+ * Read-path per-user identity and the platform's read-scope contract.
4
+ *
5
+ * Since platform #2922 the role-scoped read routes (audit / decisions /
6
+ * overrides) answer from the identity the CALLER presents, not from the tenant
7
+ * credential alone. The tenant credential in `Authorization` says which
8
+ * organization is asking; it does not say WHO. A caller that presents no
9
+ * per-user identity to an enterprise stack is not "a caller who sees
10
+ * everything" and is not "a caller who sees nothing by coincidence" — it is a
11
+ * caller the platform cannot scope, and every scoped read it makes returns
12
+ * zero rows by construction.
13
+ *
14
+ * This module carries the whole surface:
15
+ *
16
+ * - the per-user identity itself (`userToken` on the config for a client-wide
17
+ * identity, the per-call `{ userToken }` option on a read, and
18
+ * `client.asUser(token)` for a process acting on behalf of several people),
19
+ * stamped as the `X-User-Token` header from exactly ONE site — the client's
20
+ * `_fetch` wrapper, which every request goes through. There is no per-method
21
+ * header plumbing, deliberately: the platform reads the header once in its
22
+ * own proxy middleware (`platform/agent/proxy.go` `proxyAuthMiddleware`),
23
+ * not per route, so a per-method sprinkle here would be a second, drifting
24
+ * copy of a decision the platform makes in one place.
25
+ *
26
+ * - the response side of the same contract: `X-Axonflow-Read-Scope`, which the
27
+ * platform stamps on every scoped read (`platform/orchestrator/read_scope.go`
28
+ * `applyReadScopeHeader`) to say which of the three scopes the answer was
29
+ * computed under. Without it, a 404 from explain and an empty list from
30
+ * `listDecisions` are indistinguishable from "the row is not there", which is
31
+ * how a governed read comes to report a confident, vacuous nothing.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.ReadScopeError = exports.ReadScope = exports.HEADER_READ_SCOPE = exports.HEADER_USER_TOKEN = void 0;
35
+ exports.readScopeOf = readScopeOf;
36
+ exports.readScopeErrorFor = readScopeErrorFor;
37
+ exports.refuseVacuousScopedPage = refuseVacuousScopedPage;
38
+ exports.stripCredentialsOffOrigin = stripCredentialsOffOrigin;
39
+ exports.applyReadIdentity = applyReadIdentity;
40
+ /**
41
+ * The request header carrying the per-user identity.
42
+ *
43
+ * This constant is the SDK's only spelling of it. The header is set in exactly
44
+ * one place (`applyReadIdentity`, called from the client's `_fetch`); if you
45
+ * find yourself setting it in a method, the method is the wrong altitude.
46
+ */
47
+ exports.HEADER_USER_TOKEN = 'X-User-Token';
48
+ /** The response header the platform stamps on scoped reads. */
49
+ exports.HEADER_READ_SCOPE = 'X-Axonflow-Read-Scope';
50
+ exports.ReadScope = {
51
+ /** No `X-Axonflow-Read-Scope` header at all. Distinct from `None`. */
52
+ Absent: '',
53
+ /**
54
+ * Tenant-wide: a tenant-wide role (admin / owner / policy_admin), or a
55
+ * Community / Community-SaaS deployment where the whole tenant is the one
56
+ * operator.
57
+ */
58
+ Tenant: 'tenant',
59
+ /**
60
+ * Narrowed to the rows attributed to the identity presented. A miss under
61
+ * this scope means "not among yours", which is NOT the same statement as
62
+ * "not there" — see `ReadScopeError`.
63
+ */
64
+ OwnRows: 'own-rows',
65
+ /**
66
+ * The platform RESOLVED no per-user identity and the caller holds no
67
+ * tenant-wide authority, so it returned zero rows by construction. Under this
68
+ * scope a read CANNOT have returned data, so its empty answer says nothing
69
+ * about what exists.
70
+ *
71
+ * "Resolved none" is wider than "presented none", and the difference is worth
72
+ * knowing before you go looking in the wrong place. A token that validates
73
+ * perfectly still resolves to no identity when its address is one the
74
+ * platform reserves for SHARED, non-personal identities — the whole of
75
+ * `@axonflow.local` and `@axonflow.internal`, plus the community and
76
+ * evaluator addresses. Those name a pool of callers rather than a person, and
77
+ * scoping a read to one would return the pool, so the platform deliberately
78
+ * censuses them to nothing. A per-user token minted with an address in one of
79
+ * those domains therefore reads exactly like no token at all. (Easy to hit:
80
+ * the platform's own `generate-jwt.sh` defaults to
81
+ * `demo-user@axonflow.local`.)
82
+ */
83
+ None: 'none',
84
+ };
85
+ /**
86
+ * The scope the platform reported on `response`.
87
+ *
88
+ * Trimmed and lower-cased, for the same reason the platform's own header
89
+ * helpers are: a proxy that normalises header casing or appends whitespace must
90
+ * not silently change the answer. The cost of getting that wrong is one-sided
91
+ * and quiet — a scope spelled `None` would fall to the unrecognised branch and
92
+ * the vacuous empty page it describes would come back as data again. An
93
+ * unrecognised value is otherwise unchanged, so it still round-trips.
94
+ */
95
+ function readScopeOf(response) {
96
+ // A response with no readable header bag states no scope. That is the same
97
+ // answer as a pre-#2922 platform's, and it is the SAFE one: the alternative
98
+ // is throwing from inside a read, or — worse — inventing `none` and refusing
99
+ // a page that was perfectly good.
100
+ const headers = response?.headers;
101
+ if (!headers || typeof headers.get !== 'function')
102
+ return exports.ReadScope.Absent;
103
+ return (headers.get(exports.HEADER_READ_SCOPE) ?? '').trim().toLowerCase();
104
+ }
105
+ /**
106
+ * A role-scoped read whose answer was decided by the caller's identity scope
107
+ * rather than by the data.
108
+ *
109
+ * It exists because "no rows" and "no identity" are the same bytes on the wire.
110
+ * The platform distinguishes them in the `X-Axonflow-Read-Scope` header; this
111
+ * error is that distinction made visible, so a read that could not have
112
+ * succeeded reports a cause instead of a confident nothing.
113
+ *
114
+ * Two shapes, told apart by `identityMissing`:
115
+ *
116
+ * - `ReadScope.None` — no identity was RESOLVED; the read returned zero rows by
117
+ * construction and says nothing about what exists. Remedy: present an
118
+ * identity whose address is a real person's — see `ReadScope.None` for why a
119
+ * valid token can still resolve to nothing.
120
+ * - `ReadScope.OwnRows` — an identity WAS resolved, and the row is not among
121
+ * the ones attributed to it. That does NOT mean the row exists and belongs to
122
+ * somebody else: the platform answers "not attributed to you" and "not there
123
+ * at all" with the identical 404, deliberately, so that a miss cannot be used
124
+ * to probe for another user's rows. This error therefore reports the scope,
125
+ * not a claim about what exists.
126
+ *
127
+ * The presented token is never included in the message: it is safe to log,
128
+ * which is the point of putting the diagnosis in a type rather than in a string
129
+ * the caller assembles from the credential.
130
+ */
131
+ class ReadScopeError extends Error {
132
+ constructor(args) {
133
+ const resource = args.resource ?? 'read';
134
+ const subject = args.identifier ? `${resource} "${args.identifier}"` : resource;
135
+ const identityMissing = args.scope === exports.ReadScope.None;
136
+ super(identityMissing
137
+ ? `HTTP ${args.statusCode}: ${subject}: the platform resolved no per-user identity for ` +
138
+ `this read (${exports.HEADER_READ_SCOPE}: ${args.scope}), so it returned zero rows by ` +
139
+ `construction and the empty answer says nothing about what exists. Either no ` +
140
+ `identity was presented — set userToken on the client, pass it to this call, or use ` +
141
+ `client.asUser(...) — or the one presented carries an address the platform reserves ` +
142
+ `for shared identities (@axonflow.local, @axonflow.internal), which resolves to ` +
143
+ `nobody. (platform #2922)`
144
+ : `HTTP ${args.statusCode}: ${subject} was not found among the rows this identity can ` +
145
+ `see: the platform reports ${exports.HEADER_READ_SCOPE}: ${args.scope}, so the read was ` +
146
+ `narrowed to the identity's own rows. It is either not attributed to this identity ` +
147
+ `or not there at all — the platform answers both the same way ON PURPOSE, so that a ` +
148
+ `miss cannot be used to probe for the existence of another user's rows, and this SDK ` +
149
+ `cannot tell them apart either. A tenant-wide role (admin, owner or policy_admin) ` +
150
+ `reads the whole tenant. (platform #2922)`);
151
+ this.name = 'ReadScopeError';
152
+ this.scope = args.scope;
153
+ this.statusCode = args.statusCode;
154
+ this.resource = resource;
155
+ this.identifier = args.identifier;
156
+ Object.setPrototypeOf(this, ReadScopeError.prototype);
157
+ }
158
+ /**
159
+ * Whether the read failed because no per-user identity was resolved, as
160
+ * opposed to one being resolved and not matching.
161
+ */
162
+ get identityMissing() {
163
+ return this.scope === exports.ReadScope.None;
164
+ }
165
+ }
166
+ exports.ReadScopeError = ReadScopeError;
167
+ /**
168
+ * The typed refusal for a scoped read that came back with nothing, or
169
+ * `undefined` when the scope does not explain the result.
170
+ *
171
+ * `undefined` for `ReadScope.Tenant` (the caller could see the whole tenant and
172
+ * it still was not there — a genuine miss), for `ReadScope.Absent` (the
173
+ * platform did not state a scope; see `ReadScope` for why absent is not none),
174
+ * and for any scope value this build does not recognise (a newer platform's;
175
+ * reporting a cause we cannot actually read would be a confident wrong
176
+ * diagnosis).
177
+ */
178
+ function readScopeErrorFor(args) {
179
+ if (args.scope === exports.ReadScope.None || args.scope === exports.ReadScope.OwnRows) {
180
+ return new ReadScopeError(args);
181
+ }
182
+ return undefined;
183
+ }
184
+ /**
185
+ * The typed refusal for a scoped read that came back EMPTY under a scope that
186
+ * could not have returned a row; `undefined` in every other case.
187
+ *
188
+ * One helper rather than a check at each read, because "the page is empty and
189
+ * the scope is none" is one rule and the reads that need it decode their body
190
+ * on more than one path each. A rule copied per return site is a rule that ends
191
+ * up applied on some of them.
192
+ *
193
+ * The emptiness guard is as load-bearing as the scope guard: a non-empty page
194
+ * is never turned into an error, whatever the header says. And only
195
+ * `ReadScope.None` refuses — an own-rows or tenant-wide read that legitimately
196
+ * found nothing is a real answer, and replacing it with an error would swap one
197
+ * wrong report for another.
198
+ */
199
+ function refuseVacuousScopedPage(response, resource, rows) {
200
+ if (rows > 0)
201
+ return undefined;
202
+ if (readScopeOf(response) !== exports.ReadScope.None)
203
+ return undefined;
204
+ return new ReadScopeError({
205
+ scope: exports.ReadScope.None,
206
+ statusCode: response?.status ?? 0,
207
+ resource,
208
+ });
209
+ }
210
+ /** Whether two URLs are the same origin: scheme, host AND port. */
211
+ function sameOrigin(a, b) {
212
+ return a.origin === b.origin;
213
+ }
214
+ /**
215
+ * Every credential this SDK sends, so an off-origin hop can drop ALL of them.
216
+ *
217
+ * Not just the new one. `fetch`'s own redirect follower strips `Authorization`
218
+ * on a cross-origin hop; the moment this SDK follows redirects by hand — which
219
+ * it does whenever an identity is attached — that stripping stops happening and
220
+ * becomes this code's job. Getting it wrong makes setting `userToken` LEAK
221
+ * `clientSecret` to a host the caller never named, on a client that did not
222
+ * leak it before: a fix for one credential that exports another.
223
+ *
224
+ * `X-Client-ID` and `X-Axonflow-Client` are not secrets, but they name the
225
+ * caller to whoever receives them and there is no reason for a host the caller
226
+ * never chose to learn it.
227
+ */
228
+ const CREDENTIAL_HEADERS = [
229
+ 'authorization',
230
+ exports.HEADER_USER_TOKEN.toLowerCase(),
231
+ 'x-client-id',
232
+ 'x-axonflow-client',
233
+ ];
234
+ /**
235
+ * Drop every credential from `headers` because the request is leaving the
236
+ * origin they were issued for.
237
+ *
238
+ * Case-insensitive on the way in: a caller may have spelled a header
239
+ * differently, and two spellings of one header is one credential that survives.
240
+ */
241
+ function stripCredentialsOffOrigin(headers) {
242
+ for (const key of Object.keys(headers)) {
243
+ if (CREDENTIAL_HEADERS.includes(key.toLowerCase())) {
244
+ delete headers[key];
245
+ }
246
+ }
247
+ }
248
+ /**
249
+ * Stamp the per-user identity on `headers`, if there is one, for a request to
250
+ * `target` from a client configured for `endpoint`.
251
+ *
252
+ * Called from the client's `_fetch` — the one site every request goes through —
253
+ * so the identity travels on every request without any method knowing about it.
254
+ * That is on purpose and mirrors the platform: the agent reads `X-User-Token`
255
+ * once, in the middleware in front of every proxied route, and the routes
256
+ * themselves never look at it.
257
+ *
258
+ * **The header is NOT inert on the routes that are not reads.** It is validated
259
+ * on every route the agent proxies: `proxyAuthMiddleware` resolves it before
260
+ * dispatch and answers `401 invalid user token` for a present-but-INVALID one —
261
+ * on `/api/v1/plans`, `/api/v1/policies`, `/api/v1/connectors`,
262
+ * `/api/v1/process`, `/api/v1/budgets`, `/api/v1/cost`, `/api/v1/executions`
263
+ * and the rest. So a stale or rotated token does not degrade to "unscoped
264
+ * reads"; it turns `listConnectors`, `installConnector` and policy CRUD into
265
+ * 401s. Fail-closed is the right direction, but it puts the value in the same
266
+ * rotation story as `clientSecret`.
267
+ *
268
+ * Genuinely inert only on the routes the agent SERVES ITSELF — only `proxy.go`
269
+ * and `mcp_identity.go` read the header at all: `/api/request`,
270
+ * `/api/v1/decide` (whose identity comes from the request BODY's `user_token`,
271
+ * which is the whole reason the read path needed a surface of its own),
272
+ * `/api/v1/access/evaluation`, `/api/v1/static-policies/*`,
273
+ * `/api/v1/circuit-breaker/*`, `/api/v1/hitl/*`, `/api/v1/mcp/check-input`,
274
+ * `/api/v1/mcp/check-output`, `/api/v1/register`, `/api/policy/pre-check`,
275
+ * `/api/audit/llm-call` and `/health`.
276
+ *
277
+ * **It is never sent anywhere but the configured endpoint.** `target` is
278
+ * compared against `endpoint` and the header is removed when they differ. That
279
+ * guard exists because of redirects: the fetch spec strips `Authorization` on a
280
+ * cross-origin redirect, but its list is fixed and `X-User-Token` is not on it.
281
+ * Measured on Node 25: the redirect target received `authorization: undefined`
282
+ * and `x-user-token: SENTINEL`. The client follows redirects manually while an
283
+ * identity is attached, re-entering this function on each hop, so the identity
284
+ * is dropped the moment the origin changes.
285
+ *
286
+ * The token is a CREDENTIAL. It is written to the header and nowhere else: it
287
+ * is never logged, never carried in an error message, and never reaches
288
+ * telemetry — the heartbeat uses raw `fetch`, deliberately not the wrapper this
289
+ * is called from.
290
+ */
291
+ /**
292
+ * @param headers the request's header bag, MUTATED in place.
293
+ *
294
+ * A plain record rather than a `Headers` instance, deliberately: this SDK
295
+ * builds its requests with plain objects, and converting them here would change
296
+ * the shape every existing caller and test observes on `fetch`. Widening the
297
+ * blast radius of an identity fix to "every request's header representation"
298
+ * is how a small change acquires a long tail of unrelated failures.
299
+ */
300
+ function applyReadIdentity(headers, target, endpoint, token) {
301
+ // Case-insensitively, because a caller may have spelled it differently and
302
+ // two spellings of one header is two identities.
303
+ for (const key of Object.keys(headers)) {
304
+ if (key.toLowerCase() === exports.HEADER_USER_TOKEN.toLowerCase())
305
+ delete headers[key];
306
+ }
307
+ const trimmed = (token ?? '').trim();
308
+ if (!trimmed) {
309
+ // Never send an empty header. To the platform a present-but-empty
310
+ // X-User-Token is still an absent one, but sending it advertises an
311
+ // identity mechanism the caller is not using, and it is one refactor away
312
+ // from a present-but-invalid token, which is a hard 401. The delete above
313
+ // also makes an explicit per-call clearing actually clear.
314
+ return;
315
+ }
316
+ if (endpoint) {
317
+ try {
318
+ if (!sameOrigin(target, new URL(endpoint)))
319
+ return;
320
+ }
321
+ catch {
322
+ // An unparseable configured endpoint is not a licence to send the
323
+ // credential anyway.
324
+ return;
325
+ }
326
+ }
327
+ headers[exports.HEADER_USER_TOKEN] = trimmed;
328
+ }
329
+ //# sourceMappingURL=read-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-identity.js","sourceRoot":"","sources":["../../src/read-identity.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;;;AAqFH,kCAQC;AAwFD,8CAUC;AAiBD,0DAYC;AAsDD,8DAMC;AAsDD,8CA+BC;AA3WD;;;;;;GAMG;AACU,QAAA,iBAAiB,GAAG,cAAc,CAAC;AAEhD,+DAA+D;AAClD,QAAA,iBAAiB,GAAG,uBAAuB,CAAC;AA2B5C,QAAA,SAAS,GAAG;IACvB,sEAAsE;IACtE,MAAM,EAAE,EAAe;IACvB;;;;OAIG;IACH,MAAM,EAAE,QAAqB;IAC7B;;;;OAIG;IACH,OAAO,EAAE,UAAuB;IAChC;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,EAAE,MAAmB;CACjB,CAAC;AAEX;;;;;;;;;GASG;AACH,SAAgB,WAAW,CAAC,QAAqC;IAC/D,2EAA2E;IAC3E,4EAA4E;IAC5E,6EAA6E;IAC7E,kCAAkC;IAClC,MAAM,OAAO,GAAG,QAAQ,EAAE,OAAO,CAAC;IAClC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,UAAU;QAAE,OAAO,iBAAS,CAAC,MAAM,CAAC;IAC3E,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,yBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACrE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAa,cAAe,SAAQ,KAAK;IAMvC,YAAY,IAKX;QACC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,MAAM,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,QAAQ,KAAK,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;QAChF,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,KAAK,iBAAS,CAAC,IAAI,CAAC;QACtD,KAAK,CACH,eAAe;YACb,CAAC,CAAC,QAAQ,IAAI,CAAC,UAAU,KAAK,OAAO,mDAAmD;gBACpF,cAAc,yBAAiB,KAAK,IAAI,CAAC,KAAK,iCAAiC;gBAC/E,8EAA8E;gBAC9E,qFAAqF;gBACrF,qFAAqF;gBACrF,iFAAiF;gBACjF,0BAA0B;YAC9B,CAAC,CAAC,QAAQ,IAAI,CAAC,UAAU,KAAK,OAAO,kDAAkD;gBACnF,6BAA6B,yBAAiB,KAAK,IAAI,CAAC,KAAK,oBAAoB;gBACjF,oFAAoF;gBACpF,qFAAqF;gBACrF,sFAAsF;gBACtF,mFAAmF;gBACnF,0CAA0C,CACjD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACxB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QAClC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IACxD,CAAC;IAED;;;OAGG;IACH,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,KAAK,KAAK,iBAAS,CAAC,IAAI,CAAC;IACvC,CAAC;CACF;AA/CD,wCA+CC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,iBAAiB,CAAC,IAKjC;IACC,IAAI,IAAI,CAAC,KAAK,KAAK,iBAAS,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,KAAK,iBAAS,CAAC,OAAO,EAAE,CAAC;QACtE,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAgB,uBAAuB,CACrC,QAAqC,EACrC,QAAgB,EAChB,IAAY;IAEZ,IAAI,IAAI,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAC/B,IAAI,WAAW,CAAC,QAAQ,CAAC,KAAK,iBAAS,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC/D,OAAO,IAAI,cAAc,CAAC;QACxB,KAAK,EAAE,iBAAS,CAAC,IAAI;QACrB,UAAU,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;QACjC,QAAQ;KACT,CAAC,CAAC;AACL,CAAC;AAqBD,mEAAmE;AACnE,SAAS,UAAU,CAAC,CAAM,EAAE,CAAM;IAChC,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC;AAC/B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,kBAAkB,GAAG;IACzB,eAAe;IACf,yBAAiB,CAAC,WAAW,EAAE;IAC/B,aAAa;IACb,mBAAmB;CACpB,CAAC;AAEF;;;;;;GAMG;AACH,SAAgB,yBAAyB,CAAC,OAA+B;IACvE,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,kBAAkB,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACnD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH;;;;;;;;GAQG;AACH,SAAgB,iBAAiB,CAC/B,OAA+B,EAC/B,MAAW,EACX,QAA4B,EAC5B,KAAyB;IAEzB,2EAA2E;IAC3E,iDAAiD;IACjD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,yBAAiB,CAAC,WAAW,EAAE;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC;IACjF,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACrC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,kEAAkE;QAClE,oEAAoE;QACpE,0EAA0E;QAC1E,0EAA0E;QAC1E,2DAA2D;QAC3D,OAAO;IACT,CAAC;IACD,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAAE,OAAO;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,kEAAkE;YAClE,qBAAqB;YACrB,OAAO;QACT,CAAC;IACH,CAAC;IACD,OAAO,CAAC,yBAAiB,CAAC,GAAG,OAAO,CAAC;AACvC,CAAC"}
@@ -1,3 +1,77 @@
1
+ /**
2
+ * Declare that a framework adapter is driving this SDK, so the next telemetry
3
+ * heartbeat carries `adapter:<name>` in its `features` array.
4
+ *
5
+ * A framework adapter (LangChain, LangGraph, LiteLLM, …) wrapping this SDK is
6
+ * indistinguishable from bare SDK use on every other telemetry dimension —
7
+ * same `sdk`, same `sdk_version`, same endpoint. This is the one call that
8
+ * makes the difference visible, and it is adoption signal only.
9
+ *
10
+ * IT ADDS NO REQUEST. The name rides the `features` array of the heartbeat
11
+ * that already fires; there is no second ping, no second endpoint and no new
12
+ * configuration surface. Calling it does not itself send anything.
13
+ *
14
+ * CALL IT BEFORE YOUR FIRST API CALL for day-one attribution. The heartbeat
15
+ * fires on the client's FIRST OUTBOUND REQUEST, not at construction, so
16
+ * anything registered before that request is on the very first ping and a name
17
+ * registered afterwards rides the next heartbeat.
18
+ *
19
+ * The SDK's own `AxonFlowLangGraphAdapter` registers from its constructor, so
20
+ * simply using it is enough — an adapter is necessarily built after the client
21
+ * and before any call through it.
22
+ *
23
+ * Idempotent. Repeat registrations of the same name collapse to one entry.
24
+ *
25
+ * THE NAME IS NOT VALIDATED AGAINST A LIST, DELIBERATELY. The canonical
26
+ * vocabulary lives on the receiver (checkpoint-service
27
+ * `NormalizeAdapterFeature`, which folds an unrecognised name into
28
+ * `adapter:unknown` at READ time while keeping the raw name on the row). An
29
+ * allowlist here would be a second vocabulary that drifts from the first: a
30
+ * name this SDK build predates would be dropped at the client instead of
31
+ * arriving and rendering as "someone is using an adapter we do not know
32
+ * about" — precisely the signal the unknown bucket exists to preserve.
33
+ *
34
+ * So the only transformations are the two the receiver also applies before
35
+ * matching: trim surrounding whitespace, and lowercase. What is refused is
36
+ * refused for a reason that is not about vocabulary: a name empty after
37
+ * trimming (there is nothing to declare, and `adapter:` alone is not an
38
+ * identifier), and a name longer than `MAX_RELAYED_VALUE_BYTES` (dropped
39
+ * WHOLE, never truncated). A non-string is refused the same way rather than
40
+ * coerced — `String(undefined)` would put the literal text `undefined` on the
41
+ * wire as an adapter name.
42
+ *
43
+ * Both refusals are silent: this is a telemetry declaration on a
44
+ * fire-and-forget path, and throwing would invite a caller to fail their own
45
+ * startup over an analytics detail.
46
+ */
47
+ export declare function registerAdapter(name: string): void;
48
+ /**
49
+ * Apply the receiver's array bounds: at most `MAX_FEATURES` entries, none over
50
+ * `MAX_FEATURE_BYTES` bytes.
51
+ *
52
+ * An over-long entry is DROPPED rather than truncated, which is where this
53
+ * deliberately differs from the receiver's own `BoundFeatures`. The receiver
54
+ * truncates because it is defending storage against arbitrary clients and a
55
+ * truncated entry harmlessly folds into its unknown bucket. Here the entry is
56
+ * something this process declared about itself, and a truncated adapter name
57
+ * is a name nothing is running.
58
+ */
59
+ export declare function boundFeatures(features: string[]): string[];
60
+ /**
61
+ * Render the registry as the `features` array for one ping.
62
+ *
63
+ * Sorted so the wire is deterministic — two processes that registered the same
64
+ * adapters in a different order produce the same array, which is what lets a
65
+ * test assert on the whole field, and what makes "which 32 survive" a defined
66
+ * answer rather than a Set-iteration accident.
67
+ */
68
+ export declare function registeredFeatures(): string[];
69
+ /** Test-only: empty the registry and return what was there, so the caller can
70
+ * restore it. The registry is module-global by design, so a test that
71
+ * registers an adapter would otherwise leak it into every later test's ping. */
72
+ export declare function _resetAdapterRegistryForTest(): string[];
73
+ /** Test-only: restore a registry saved by `_resetAdapterRegistryForTest`. */
74
+ export declare function _restoreAdapterRegistryForTest(previous: string[]): void;
1
75
  export interface TelemetryPayload {
2
76
  /**
3
77
  * v1 telemetry-schema discriminator (axonflow-enterprise#2008). Always
@@ -83,6 +157,36 @@ export interface TelemetryPayload {
83
157
  * reads as "unknown", not as any particular tier.
84
158
  */
85
159
  license_tier?: string;
160
+ /**
161
+ * The BUILD the connected platform reported on its own `/health`:
162
+ * `community` or `enterprise`. Relayed verbatim, and it rides the SAME
163
+ * `/health` response the version and the tier already come from — no new
164
+ * request. Issue axonflow-enterprise#3660.
165
+ *
166
+ * NOT an entitlement fact, on the same terms as `license_tier` above:
167
+ * whoever operates the configured endpoint controls the value completely and
168
+ * this SDK relays it unverified.
169
+ *
170
+ * NOT derivable from anything else here either — the Community-SaaS fleet
171
+ * runs the ENTERPRISE build against the community-saas schema, so neither
172
+ * `deployment_mode` nor `license_tier` implies it.
173
+ *
174
+ * ABSENT (property omitted) means NOT LEARNED.
175
+ */
176
+ edition?: string;
177
+ /**
178
+ * The connected platform's OWN deployment mode, as it reported it on
179
+ * `/health` under the member name `deployment_mode`.
180
+ *
181
+ * READ THE FIELD NAMES CAREFULLY — THIS IS THE TRAP THIS CONTRACT IS MOST
182
+ * LIKELY TO BE GOT WRONG ON. The `/health` member is called
183
+ * `deployment_mode` because there the platform is describing ITSELF. On this
184
+ * ping, `deployment_mode` already means something else entirely: the
185
+ * TOPOLOGY bucket this SDK derives from the endpoint URL it was configured
186
+ * with. They are different dimensions, and mapping `/health`'s member onto
187
+ * the topology field would overwrite a value every existing dashboard reads.
188
+ */
189
+ platform_deployment_mode?: string;
86
190
  }
87
191
  /**
88
192
  * Sentinel emitted on the telemetry wire when `ORG_ID` is unset — the
@@ -165,6 +269,10 @@ export declare function sendTelemetryPing(options: {
165
269
  export interface PlatformHealthProbe {
166
270
  platformVersion: string | null;
167
271
  licenseTier: string | null;
272
+ /** `/health` → `edition`. */
273
+ edition: string | null;
274
+ /** `/health` → `deployment_mode`, relayed as `platform_deployment_mode`. */
275
+ platformDeploymentMode: string | null;
168
276
  }
169
277
  /**
170
278
  * Probe the agent's `/health` endpoint ONCE and extract every telemetry
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/telemetry.ts"],"names":[],"mappings":"AAmGA,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,eAAe,EAAE,cAAc,CAAC;IAChC;;;;OAIG;IACH,aAAa,EAAE,YAAY,CAAC;IAC5B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,yBAAyB,kBAAkB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAGvC;AAED,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,iBAAiB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElF,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,gBAAgB,GAAG,SAAS,CAAC;AAE1E;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,cAAc,CAgBrF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAmF7E;AA0FD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,OAAO,CAAC,CAiEnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,IAAI,CA8FP;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,mBAAmB,CAAC,CAmD9B"}
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/telemetry.ts"],"names":[],"mappings":"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"}