@devtune/ai-traffic 0.9.0 → 0.11.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 +33 -2
- package/dist/cjs/client.js +170 -5
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/vercel-oidc.js +159 -0
- package/dist/cjs/vercel-oidc.js.map +1 -0
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/version.js.map +1 -1
- package/dist/client.d.ts +55 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +170 -5
- package/dist/client.js.map +1 -1
- package/dist/vercel-oidc.d.ts +53 -0
- package/dist/vercel-oidc.d.ts.map +1 -0
- package/dist/vercel-oidc.js +152 -0
- package/dist/vercel-oidc.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABOUTME: Vercel OIDC credentials for ingest, used instead of an ingest key on Vercel.
|
|
3
|
+
* Filters the per-request token header and exchanges it for DevTune's audience with a cache.
|
|
4
|
+
*/
|
|
5
|
+
/** Vercel sets this on every request to a deployment that has OIDC enabled. */
|
|
6
|
+
export const VERCEL_OIDC_TOKEN_HEADER = "x-vercel-oidc-token";
|
|
7
|
+
/** Tells the ingest route which DevTune project's Vercel link to check the token against. */
|
|
8
|
+
export const DEVTUNE_PROJECT_HEADER = "x-devtune-project";
|
|
9
|
+
export const DEVTUNE_INGEST_AUDIENCE = "https://devtune.ai/ai-traffic-ingest";
|
|
10
|
+
export const VERCEL_TOKEN_EXCHANGE_URL = "https://oidc.vercel.com/~token";
|
|
11
|
+
const VERCEL_OIDC_ISSUER = "https://oidc.vercel.com";
|
|
12
|
+
const MAX_TOKEN_LENGTH = 8_192;
|
|
13
|
+
const TOKEN_EXCHANGE_TIMEOUT_MS = 5_000;
|
|
14
|
+
const BASE64URL_SEGMENT = /^[A-Za-z0-9_-]+$/u;
|
|
15
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
|
|
16
|
+
/** Tokens this close to expiry are neither captured, exchanged nor served from the cache. */
|
|
17
|
+
export const TOKEN_EXPIRY_MARGIN_MS = 60_000;
|
|
18
|
+
export function isDevTuneProjectId(value) {
|
|
19
|
+
return UUID_PATTERN.test(value);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns the token's expiry in milliseconds when the value looks like a live
|
|
23
|
+
* Vercel OIDC token, or null for anything else.
|
|
24
|
+
*
|
|
25
|
+
* The signature is not checked: the SDK has no way to, and the ingest route
|
|
26
|
+
* verifies it. This filter only keeps a client-supplied junk header, a token
|
|
27
|
+
* from another issuer or an expired token from replacing a good one.
|
|
28
|
+
*/
|
|
29
|
+
export function readVercelOidcTokenExpiry(value, nowMs) {
|
|
30
|
+
if (value.length > MAX_TOKEN_LENGTH) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const segments = value.split(".");
|
|
34
|
+
if (segments.length !== 3 || !segments.every((segment) => BASE64URL_SEGMENT.test(segment))) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const claims = decodeJsonSegment(segments[1] ?? "");
|
|
38
|
+
if (!claims ||
|
|
39
|
+
typeof claims.iss !== "string" ||
|
|
40
|
+
(claims.iss !== VERCEL_OIDC_ISSUER && !claims.iss.startsWith(`${VERCEL_OIDC_ISSUER}/`)) ||
|
|
41
|
+
typeof claims.owner_id !== "string" ||
|
|
42
|
+
typeof claims.project_id !== "string" ||
|
|
43
|
+
typeof claims.exp !== "number" ||
|
|
44
|
+
!Number.isFinite(claims.exp)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const expiresAtMs = claims.exp * 1000;
|
|
48
|
+
return expiresAtMs - nowMs > TOKEN_EXPIRY_MARGIN_MS ? expiresAtMs : null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when Vercel answers an exchange but not with a usable token. Carries
|
|
52
|
+
* a fixed `kind` and the status, so callers can report the failure without
|
|
53
|
+
* reading any message text.
|
|
54
|
+
*/
|
|
55
|
+
export class VercelOidcExchangeError extends Error {
|
|
56
|
+
kind;
|
|
57
|
+
status;
|
|
58
|
+
constructor(kind, status) {
|
|
59
|
+
super(kind === "exchange_status"
|
|
60
|
+
? `Vercel OIDC token exchange failed with status ${status}`
|
|
61
|
+
: "Vercel OIDC token exchange returned an unusable response");
|
|
62
|
+
this.kind = kind;
|
|
63
|
+
this.status = status;
|
|
64
|
+
this.name = "VercelOidcExchangeError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Exchanges a deployment's Vercel OIDC token for one scoped to DevTune's
|
|
69
|
+
* ingest audience.
|
|
70
|
+
*
|
|
71
|
+
* Results are cached in memory by source token until their expiry, less the
|
|
72
|
+
* margin, and expired entries are dropped on every call, so the steady state
|
|
73
|
+
* is one entry per live source token. Callers that need the same token at once
|
|
74
|
+
* share one request. Vercel's answers are reported as a
|
|
75
|
+
* `VercelOidcExchangeError`; anything the fetch itself throws is passed on.
|
|
76
|
+
*/
|
|
77
|
+
export class VercelOidcTokenExchanger {
|
|
78
|
+
fetchImpl;
|
|
79
|
+
nowMs;
|
|
80
|
+
cache = new Map();
|
|
81
|
+
inFlight = new Map();
|
|
82
|
+
constructor(fetchImpl, nowMs) {
|
|
83
|
+
this.fetchImpl = fetchImpl;
|
|
84
|
+
this.nowMs = nowMs;
|
|
85
|
+
}
|
|
86
|
+
async exchange(sourceToken) {
|
|
87
|
+
const nowMs = this.nowMs();
|
|
88
|
+
for (const [key, entry] of this.cache) {
|
|
89
|
+
if (entry.usableUntilMs <= nowMs) {
|
|
90
|
+
this.cache.delete(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const cached = this.cache.get(sourceToken);
|
|
94
|
+
if (cached) {
|
|
95
|
+
return cached.token;
|
|
96
|
+
}
|
|
97
|
+
const pending = this.inFlight.get(sourceToken);
|
|
98
|
+
if (pending) {
|
|
99
|
+
return pending;
|
|
100
|
+
}
|
|
101
|
+
const request = this.requestExchange(sourceToken).finally(() => {
|
|
102
|
+
this.inFlight.delete(sourceToken);
|
|
103
|
+
});
|
|
104
|
+
this.inFlight.set(sourceToken, request);
|
|
105
|
+
return request;
|
|
106
|
+
}
|
|
107
|
+
async requestExchange(sourceToken) {
|
|
108
|
+
const response = await this.fetchImpl(VERCEL_TOKEN_EXCHANGE_URL, {
|
|
109
|
+
body: JSON.stringify({ token: sourceToken, aud: DEVTUNE_INGEST_AUDIENCE }),
|
|
110
|
+
headers: {
|
|
111
|
+
accept: "application/json",
|
|
112
|
+
"content-type": "application/json",
|
|
113
|
+
},
|
|
114
|
+
method: "POST",
|
|
115
|
+
signal: AbortSignal.timeout(TOKEN_EXCHANGE_TIMEOUT_MS),
|
|
116
|
+
});
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new VercelOidcExchangeError("exchange_status", response.status);
|
|
119
|
+
}
|
|
120
|
+
const payload = await response.json().catch(() => null);
|
|
121
|
+
if (!isRecord(payload) ||
|
|
122
|
+
typeof payload.token !== "string" ||
|
|
123
|
+
payload.token.length === 0 ||
|
|
124
|
+
typeof payload.expiry !== "number" ||
|
|
125
|
+
!Number.isFinite(payload.expiry)) {
|
|
126
|
+
throw new VercelOidcExchangeError("exchange_response");
|
|
127
|
+
}
|
|
128
|
+
const usableUntilMs = payload.expiry * 1000 - TOKEN_EXPIRY_MARGIN_MS;
|
|
129
|
+
// A token that is already inside the margin would only be rejected at ingest.
|
|
130
|
+
if (usableUntilMs <= this.nowMs()) {
|
|
131
|
+
throw new VercelOidcExchangeError("exchange_response");
|
|
132
|
+
}
|
|
133
|
+
this.cache.set(sourceToken, { token: payload.token, usableUntilMs });
|
|
134
|
+
return payload.token;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function decodeJsonSegment(segment) {
|
|
138
|
+
try {
|
|
139
|
+
const base64 = segment.replace(/-/gu, "+").replace(/_/gu, "/");
|
|
140
|
+
const binary = atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "="));
|
|
141
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
142
|
+
const value = JSON.parse(new TextDecoder().decode(bytes));
|
|
143
|
+
return isRecord(value) ? value : null;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function isRecord(value) {
|
|
150
|
+
return typeof value === "object" && value !== null;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=vercel-oidc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vercel-oidc.js","sourceRoot":"","sources":["../src/vercel-oidc.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Lets DevTune tell a customer when their sensor has an upgrade available.
|
|
4
4
|
*/
|
|
5
5
|
/** Must equal `version` in package.json; a unit test and the release check enforce it. */
|
|
6
|
-
export declare const SDK_VERSION = "0.
|
|
6
|
+
export declare const SDK_VERSION = "0.11.0";
|
|
7
7
|
/** Sent on every ingest and registry request. Carries the version only, no visitor data. */
|
|
8
8
|
export declare const SDK_VERSION_HEADER = "x-devtune-sdk-version";
|
|
9
9
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0FAA0F;AAC1F,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0FAA0F;AAC1F,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,0BAA0B,CAAC"}
|
package/dist/version.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Lets DevTune tell a customer when their sensor has an upgrade available.
|
|
4
4
|
*/
|
|
5
5
|
/** Must equal `version` in package.json; a unit test and the release check enforce it. */
|
|
6
|
-
export const SDK_VERSION = "0.
|
|
6
|
+
export const SDK_VERSION = "0.11.0";
|
|
7
7
|
/** Sent on every ingest and registry request. Carries the version only, no visitor data. */
|
|
8
8
|
export const SDK_VERSION_HEADER = "x-devtune-sdk-version";
|
|
9
9
|
//# sourceMappingURL=version.js.map
|
package/dist/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,WAAW,GAAG,
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,WAAW,GAAG,QAAQ,CAAC;AAEpC,4FAA4F;AAC5F,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAC"}
|