@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.
- package/README.md +121 -0
- package/dist/cjs/adapters/langgraph.d.ts.map +1 -1
- package/dist/cjs/adapters/langgraph.js +20 -0
- package/dist/cjs/adapters/langgraph.js.map +1 -1
- package/dist/cjs/authzen.d.ts +35 -10
- package/dist/cjs/authzen.d.ts.map +1 -1
- package/dist/cjs/authzen.js +45 -19
- package/dist/cjs/authzen.js.map +1 -1
- package/dist/cjs/client.d.ts +69 -10
- package/dist/cjs/client.d.ts.map +1 -1
- package/dist/cjs/client.js +338 -52
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/heartbeat.d.ts +55 -4
- package/dist/cjs/heartbeat.d.ts.map +1 -1
- package/dist/cjs/heartbeat.js +86 -7
- package/dist/cjs/heartbeat.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +12 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/read-identity.d.ts +267 -0
- package/dist/cjs/read-identity.d.ts.map +1 -0
- package/dist/cjs/read-identity.js +329 -0
- package/dist/cjs/read-identity.js.map +1 -0
- package/dist/cjs/telemetry.d.ts +108 -0
- package/dist/cjs/telemetry.d.ts.map +1 -1
- package/dist/cjs/telemetry.js +318 -35
- package/dist/cjs/telemetry.js.map +1 -1
- package/dist/cjs/types/authzen.gen.d.ts +4 -2
- package/dist/cjs/types/authzen.gen.d.ts.map +1 -1
- package/dist/cjs/types/authzen.gen.js +10 -4
- package/dist/cjs/types/authzen.gen.js.map +1 -1
- package/dist/cjs/types/config.d.ts +43 -1
- package/dist/cjs/types/config.d.ts.map +1 -1
- package/dist/cjs/types/hitl.js +1 -1
- package/dist/cjs/types/hitl.js.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/adapters/langgraph.d.ts.map +1 -1
- package/dist/esm/adapters/langgraph.js +20 -0
- package/dist/esm/adapters/langgraph.js.map +1 -1
- package/dist/esm/authzen.d.ts +35 -10
- package/dist/esm/authzen.d.ts.map +1 -1
- package/dist/esm/authzen.js +38 -14
- package/dist/esm/authzen.js.map +1 -1
- package/dist/esm/client.d.ts +69 -10
- package/dist/esm/client.d.ts.map +1 -1
- package/dist/esm/client.js +338 -52
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/heartbeat.d.ts +55 -4
- package/dist/esm/heartbeat.d.ts.map +1 -1
- package/dist/esm/heartbeat.js +85 -7
- package/dist/esm/heartbeat.js.map +1 -1
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/read-identity.d.ts +267 -0
- package/dist/esm/read-identity.d.ts.map +1 -0
- package/dist/esm/read-identity.js +320 -0
- package/dist/esm/read-identity.js.map +1 -0
- package/dist/esm/telemetry.d.ts +108 -0
- package/dist/esm/telemetry.d.ts.map +1 -1
- package/dist/esm/telemetry.js +313 -35
- package/dist/esm/telemetry.js.map +1 -1
- package/dist/esm/types/authzen.gen.d.ts +4 -2
- package/dist/esm/types/authzen.gen.d.ts.map +1 -1
- package/dist/esm/types/authzen.gen.js +8 -2
- package/dist/esm/types/authzen.gen.js.map +1 -1
- package/dist/esm/types/config.d.ts +43 -1
- package/dist/esm/types/config.d.ts.map +1 -1
- package/dist/esm/types/hitl.js +1 -1
- package/dist/esm/types/hitl.js.map +1 -1
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- 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":"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"}
|
package/dist/cjs/telemetry.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/telemetry.ts"],"names":[],"mappings":"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"}
|