@uniformdev/mesh-sdk 20.72.2 → 20.72.3-alpha.14

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.
@@ -0,0 +1,386 @@
1
+ import { ApiClient } from '@uniformdev/context/api';
2
+
3
+ /** @deprecated This beta identity delegation API may change with breaking changes. */
4
+ interface DelegationTokenClientOptions {
5
+ /** Uniform API host (e.g. 'https://uniform.app'). */
6
+ apiHost: string;
7
+ /** UUID of the integration definition. */
8
+ integrationId: string;
9
+ /** Plaintext app secret for this integration. */
10
+ integrationSecret: string;
11
+ /** Optional fetch implementation. Defaults to global fetch. */
12
+ fetch?: typeof fetch;
13
+ }
14
+ /** @deprecated This beta identity delegation API may change with breaking changes. */
15
+ interface DelegationTokenResponse {
16
+ /** Bearer access token that can be used to call Uniform APIs on behalf of the user. */
17
+ accessToken: string;
18
+ /** Refresh token for obtaining a new access token when the current one expires. Absent when the session was minted with `allowRefresh: false`. */
19
+ refreshToken?: string;
20
+ /** Always 'Bearer'. */
21
+ tokenType: 'Bearer';
22
+ /** Token lifetime in seconds. */
23
+ expiresIn: number;
24
+ }
25
+ /**
26
+ * Stable, low-detail kinds of token-exchange failures. Callers branch on these
27
+ * instead of parsing arbitrary upstream message text, and integrators surface a
28
+ * sanitised public message rather than whatever the server happened to return.
29
+ */
30
+ type DelegationTokenErrorKind = 'bad_request' | 'unauthenticated' | 'forbidden' | 'not_found' | 'rate_limited' | 'server_error' | 'unknown';
31
+ /** @deprecated This beta identity delegation API may change with breaking changes. */
32
+ declare class DelegationTokenError extends Error {
33
+ readonly status: number;
34
+ readonly kind: DelegationTokenErrorKind;
35
+ constructor(status: number, kind: DelegationTokenErrorKind, publicMessage: string);
36
+ }
37
+ /**
38
+ * Maps an HTTP status code to a stable `DelegationTokenErrorKind`. Never
39
+ * propagate arbitrary upstream `errorMessage`/`error`/`message` body content.
40
+ * We are guarding against propagating attacker-influenced or sensitive upstream text
41
+ * out of /api/v1/token failures into integrator-side surfaces (logs, Sentry, integrator UI errors).
42
+ */
43
+ declare function classifyDelegationTokenStatus(status: number): DelegationTokenErrorKind;
44
+ /**
45
+ * Builds a sanitised {@link DelegationTokenError} for a non-OK token exchange response.
46
+ * The error body is intentionally ignored because it may contain attacker-controlled or
47
+ * upstream library text (token fragments, claim values, stack traces).
48
+ */
49
+ declare function buildDelegationTokenError(status: number): DelegationTokenError;
50
+ /**
51
+ * Server-side client for the Uniform token exchange endpoint.
52
+ * Use this in your integration's backend to exchange a session token (obtained from the
53
+ * Mesh SDK iframe context) for a delegation token, or to refresh an existing delegation token.
54
+ *
55
+ * Extends {@link ApiClient} to inherit concurrency limiting (limitPolicy) and 429 retry
56
+ * with exponential backoff (handleRateLimits). Auth credentials go in the POST body —
57
+ * the inherited header-based auth is intentionally unused.
58
+ *
59
+ * @deprecated This beta identity delegation API may change with breaking changes.
60
+ */
61
+ declare class DelegationTokenClient extends ApiClient {
62
+ #private;
63
+ constructor(options: DelegationTokenClientOptions);
64
+ /**
65
+ * Exchanges a short-lived session token for a delegation token and refresh token.
66
+ * The session token is obtained by the integration's frontend via `sdk.getSessionToken()`.
67
+ *
68
+ * @deprecated This beta identity delegation API may change with breaking changes.
69
+ */
70
+ exchangeSessionToken(sessionToken: string): Promise<DelegationTokenResponse>;
71
+ /**
72
+ * Exchanges a refresh token for a new delegation token and a new refresh token.
73
+ *
74
+ * Replay posture: refresh tokens are bearer credentials that are valid until
75
+ * their server-side expiry. They are NOT single-use — a captured refresh token can be
76
+ * replayed by an attacker that also has the integration secret until it expires.
77
+ * Single-use enforcement (refresh-token storage, family/jti tracking, replay revocation)
78
+ * is tracked in `UNI-9279`.
79
+ *
80
+ * @deprecated This beta identity delegation API may change with breaking changes.
81
+ */
82
+ refreshDelegationToken(refreshToken: string): Promise<DelegationTokenResponse>;
83
+ }
84
+
85
+ /**
86
+ * Default cookie name for the JWE-sealed Mesh delegation session.
87
+ *
88
+ * Exported as a constant so partners can keep one source of truth across the
89
+ * BFF (`Set-Cookie` writer + `Cookie` reader) without introducing typos.
90
+ * Partners running multiple integration BFFs on the same host SHOULD pick a
91
+ * unique cookie name per integration (e.g. `__Host-uniform_mesh_delegation_<integrationId>`)
92
+ * and pass that explicitly to `serializeSessionCookie`/`parseCookies` lookups.
93
+ * Names with the `__Host-` prefix require `Path=/` and must not set `Domain`.
94
+ */
95
+ declare const DELEGATION_COOKIE_NAME = "__Host-uniform_mesh_delegation";
96
+ /**
97
+ * Default session TTL in seconds (8 hours). Shared between
98
+ * `sealDelegationSession` (JWE `exp` claim) and `serializeSessionCookie`
99
+ * (`Max-Age` attribute) so they cannot drift independently.
100
+ */
101
+ declare const SESSION_TTL_SECONDS: number;
102
+
103
+ /**
104
+ * Parse a raw `Cookie` request-header value into a name → value map.
105
+ *
106
+ * Wraps `cookie.parse` so consumers get RFC 6265 §5.4 behaviour: quoted
107
+ * values are unwrapped, URL-encoded values are decoded, and when the same
108
+ * name appears twice the FIRST occurrence wins (matching the user-agent's
109
+ * stored set semantics). `undefined` input returns `{}` so callers can pass
110
+ * `req.headers.cookie` directly without a null-check at every call site.
111
+ *
112
+ * The return type is `Record<string, string>`; the underlying parser may
113
+ * return `undefined` for malformed values, but the wrapper filters those
114
+ * out so consumers can rely on `cookies[NAME]` being either a string or
115
+ * absent (`in` check) without a `typeof` guard.
116
+ */
117
+ declare function parseCookies(cookieHeader: string | undefined): Record<string, string>;
118
+
119
+ /**
120
+ * Caller-tunable options for `serializeSessionCookie`.
121
+ *
122
+ * Cookie-flag invariants intentionally absent from this surface:
123
+ * - `httpOnly`, `secure`, `sameSite` are always emitted with the values
124
+ * the iframe-embedded mesh-app context requires. Any other choice is
125
+ * silently broken in this deployment shape and is not configurable.
126
+ */
127
+ type SerializeSessionCookieOptions = {
128
+ /**
129
+ * `Path` attribute. Defaults to `/` so the cookie attaches on every BFF
130
+ * route. Override to scope the cookie to a sub-path when the BFF lives
131
+ * under a known prefix (e.g. `/api/mesh/`).
132
+ */
133
+ path?: string;
134
+ /**
135
+ * `Max-Age` in seconds. Defaults to 8 hours, matching the refresh-token
136
+ * lifetime issued by the Uniform delegation token endpoint at the time of
137
+ * writing. Partners on a different refresh-token TTL should pass their
138
+ * value explicitly.
139
+ */
140
+ maxAge?: number;
141
+ /**
142
+ * Emit the `Partitioned` (CHIPS) attribute. Defaults to `true` because
143
+ * the mesh-app is loaded as a cross-site iframe and CHIPS keys the cookie
144
+ * to (our origin × top-level site), which gives per-embedding isolation
145
+ * and survives Chrome's third-party cookie phase-out.
146
+ *
147
+ * Browser support for `Partitioned` is not yet universal — Safari is
148
+ * partial as of 2026 (same as Sec-Fetch-Site it seems :-/).
149
+ */
150
+ partitioned?: boolean;
151
+ };
152
+ /**
153
+ * Build a `Set-Cookie` header value for the delegation session cookie.
154
+ *
155
+ * The following flags are HARDCODED because the iframe-embedded mesh-app
156
+ * context is invariant — any other configuration is silently broken:
157
+ *
158
+ * - `HttpOnly`: JavaScript cannot read the cookie via `document.cookie`.
159
+ * - `Secure`: cookie only flows over HTTPS. Dev MUST also be HTTPS
160
+ * (`next dev --experimental-https`, self-signed cert for Express, etc.).
161
+ * - `SameSite=None`: required so the browser attaches the cookie on
162
+ * subresource requests from inside the cross-site iframe. With
163
+ * `Lax`/`Strict` the cookie would be stored but not returned.
164
+ *
165
+ * `Partitioned` (CHIPS) is emitted by default; pass `partitioned: false`
166
+ * to suppress it for partners deploying to a single embedding host.
167
+ *
168
+ * The cookie value is URL-encoded by the underlying `cookie` package so
169
+ * the resulting header is RFC 6265 §4.1.1 compliant. JWE strings produced
170
+ * by `sealDelegationSession` only contain unreserved characters, so the
171
+ * encoding is a no-op for them but is essential when partners reuse the
172
+ * helper for any other payload.
173
+ */
174
+ declare function serializeSessionCookie(name: string, value: string, opts?: SerializeSessionCookieOptions): string;
175
+
176
+ /**
177
+ * Headers `verifyCsrf` consumes. Callers extract these from their framework
178
+ * request object — see the README "Quick start" section for Next.js and
179
+ * Express snippets.
180
+ */
181
+ type CsrfInput = {
182
+ /** `Origin` request header. */
183
+ origin: string | undefined;
184
+ /** `Referer` request header. Used as fallback when `Origin` is absent. */
185
+ referer: string | undefined;
186
+ /** `Sec-Fetch-Site` request header. Chromium-only; absent on Safari/Firefox<90. */
187
+ secFetchSite: string | undefined;
188
+ /** Value of the custom CSRF header (default name `x-mesh-csrf`). */
189
+ customHeader: string | undefined;
190
+ };
191
+ /**
192
+ * Caller-supplied CSRF policy. The package never reads `process.env`; the
193
+ * allow-list lives in consumer code so a typo is loud at boot and so the
194
+ * package stays edge-runtime-safe.
195
+ */
196
+ type CsrfConfig = {
197
+ /**
198
+ * Origins allowed to call state-changing endpoints. Typical wiring:
199
+ * `[process.env.UNIFORM_DASHBOARD_ORIGIN, process.env.SELF_ORIGIN]`.
200
+ * MUST be origin strings (`https://host[:port]`). Trailing slashes are
201
+ * stripped before comparison so a misconfigured env var does not silently
202
+ * disable the CSRF check for that origin.
203
+ */
204
+ allowedOrigins: readonly string[];
205
+ /**
206
+ * Dev-only escape hatch. When `true`, skip Origin/Referer validation and
207
+ * rely on the custom CSRF header alone. Defaults to `false`.
208
+ *
209
+ * Wire from the consumer's dev-mode indicator — e.g.
210
+ * `process.env.NODE_ENV === 'development'` or an explicit local-only env
211
+ * var. Chromium often omits `Origin` on `https://localhost` iframe
212
+ * subresource requests (https://issues.chromium.org/issues/40499674); this
213
+ * flag exists for that local mesh dev case, not for loopback hosts in
214
+ * general.
215
+ */
216
+ skipOriginValidation?: boolean;
217
+ /** Expected custom-header value. Defaults to `'1'`. */
218
+ headerValue?: string;
219
+ /**
220
+ * When `true`, also require `Sec-Fetch-Site: same-origin`. Defaults to
221
+ * `false` because Safari/Firefox<90 omit the header. Partners on a
222
+ * Chromium-only embedding host can flip this on for defence-in-depth.
223
+ */
224
+ requireSecFetchSite?: boolean;
225
+ };
226
+ /**
227
+ * Discriminated result so consumers can log specific failure modes for
228
+ * metrics without echoing the reason verbatim to clients.
229
+ */
230
+ type CsrfResult = {
231
+ ok: true;
232
+ } | {
233
+ ok: false;
234
+ reason: 'origin_mismatch' | 'origin_missing' | 'header_missing' | 'sec_fetch_site_mismatch';
235
+ };
236
+
237
+ /**
238
+ * Verify a request against the delegation BFF CSRF policy.
239
+ *
240
+ * Order of checks (first failure wins):
241
+ *
242
+ * 1. Origin in `allowedOrigins`. If `Origin` header is absent, parse the
243
+ * origin from `Referer` and check that. If both are absent, accept
244
+ * `Sec-Fetch-Site: same-origin` as proof the request came from the
245
+ * server's own origin (this header is forbidden — browsers never let
246
+ * JS set it). This handles the spec-correct case where browsers omit
247
+ * `Origin` on same-origin GET/HEAD requests. Skipped entirely when
248
+ * `skipOriginValidation` is `true` (dev-only; see `CsrfConfig`). If
249
+ * none of the above apply and no origin signal is present, fail with
250
+ * `origin_missing`.
251
+ * 2. Custom header presence + value match. Closes the classic
252
+ * cross-origin-from-a-browser CSRF case via the no-CORS-preflight
253
+ * property of non-safelisted headers.
254
+ * 3. `Sec-Fetch-Site === 'same-origin'` when `requireSecFetchSite: true`.
255
+ * Defence-in-depth on Chromium-only deployments. NOT a substitute for
256
+ * either earlier check — Safari and Firefox<90 omit the header, so
257
+ * partners supporting those browsers MUST leave this off.
258
+ *
259
+ * The caller is responsible for responding `403` on `ok: false`. The
260
+ * `reason` is intended for server-side logging and metrics; do NOT echo it
261
+ * verbatim to the client if you want to avoid leaking which check failed
262
+ * to a probing attacker.
263
+ */
264
+ declare function verifyCsrf(input: CsrfInput, config: CsrfConfig): CsrfResult;
265
+
266
+ /**
267
+ * Token pair stored inside the encrypted delegation session cookie.
268
+ */
269
+ type DelegationSession = {
270
+ /** Short-lived Bearer token for calling Uniform APIs on behalf of the user. */
271
+ accessToken: string;
272
+ /**
273
+ * Long-lived refresh token. `undefined` means the user gave "this session
274
+ * only" consent during the delegation handshake — the BFF must re-handshake
275
+ * via cross-frame communication when the access token expires; it cannot
276
+ * silently rotate.
277
+ */
278
+ refreshToken: string | undefined;
279
+ /** Unix epoch milliseconds when `accessToken` expires. */
280
+ expiresAt: number;
281
+ };
282
+
283
+ /**
284
+ * Returns `true` when `session.accessToken` will expire within `bufferMs`
285
+ * (defaults to 60 seconds). 60s gives the BFF enough runway to absorb
286
+ * network latency to the Uniform token endpoint and minor clock skew.
287
+ *
288
+ * Consumers call this before every upstream Uniform API call. When it
289
+ * returns `true`, the consumer's BFF route refreshes the token pair via
290
+ * `DelegationTokenClient.refreshDelegationToken` (consumer code, not in
291
+ * this package), reseals via `sealDelegationSession`, and writes a new
292
+ * `Set-Cookie` header on the response BEFORE issuing the API call.
293
+ */
294
+ declare function needsRefresh(session: DelegationSession, bufferMs?: number): boolean;
295
+
296
+ /**
297
+ * Encrypt a `DelegationSession` into an AES-256-GCM JWE string suitable for
298
+ * storage inside an `HttpOnly` cookie.
299
+ *
300
+ * Algorithm: `dir` (direct key agreement) + `A256GCM` (encryption).
301
+ *
302
+ * The 32-byte key is derived from the secret via HKDF-SHA-256 (see `deriveKey`).
303
+ * The JWE carries `iat` and `exp` claims (8h TTL); `unsealDelegationSession`
304
+ * fails decryption once `exp` has elapsed, which protects against replay of
305
+ * a stale cookie value extracted from logs or a CDN cache after rotation.
306
+ */
307
+ declare function sealDelegationSession(session: DelegationSession, secret: string): Promise<string>;
308
+
309
+ /**
310
+ * Decrypt a JWE cookie string back into a `DelegationSession`.
311
+ *
312
+ * Returns `null` (instead of throwing) for any failure mode:
313
+ * - expired `exp` claim,
314
+ * - tampered ciphertext / authentication-tag mismatch,
315
+ * - wrong secret,
316
+ * - malformed payload (`accessToken` missing or wrong type, etc.).
317
+ *
318
+ * Callers treat `null` as "no session" and respond accordingly. Validation
319
+ * errors are surfaced via `console.error` with metadata about field presence
320
+ * and types only — raw token values are NEVER logged so a malformed payload
321
+ * cannot leak bearer credentials into log destinations.
322
+ */
323
+ declare function unsealDelegationSession(jwe: string, secret: string): Promise<DelegationSession | null>;
324
+
325
+ /** Issuer (`iss`) claim on Uniform delegation access tokens. */
326
+ declare const uniformDelegationIssuer = "uniform:delegation";
327
+ /** Audience (`aud`) claim on Uniform delegation access tokens accepted by Uniform HTTP APIs. */
328
+ declare const uniformApiAudience = "uniform:api";
329
+ /** Resolves the public JWKS URL for delegation access token verification. */
330
+ declare function getJwksUrl(origin?: string): string;
331
+
332
+ /**
333
+ * Configuration for {@link verifyDelegationAccessToken}.
334
+ */
335
+ interface VerifyDelegationTokenOptions {
336
+ /**
337
+ * Full URL to the delegation JWKS endpoint.
338
+ *
339
+ * Defaults to {@link getJwksUrl} (`https://uniform.app/delegation/.well-known/jwks.json`).
340
+ *
341
+ * @example getJwksUrl(process.env.UNIFORM_API_HOST)
342
+ */
343
+ jwksUrl?: string | URL;
344
+ /**
345
+ * Clock tolerance in seconds for `exp` / `nbf` validation.
346
+ * @default 2
347
+ */
348
+ clockToleranceSeconds?: number;
349
+ }
350
+ /** Verified claims from a Uniform delegation access token. */
351
+ interface DelegationAccessTokenClaims {
352
+ /** User identifier of the delegating user (`sub`). */
353
+ sub: string;
354
+ /** Team that owns the integration installation. */
355
+ teamId: string;
356
+ /** Actor claim: which integration is acting on behalf of the user. */
357
+ act: {
358
+ /** Integration definition ID. */
359
+ sub: string;
360
+ /** Integration type slug. */
361
+ integration_type: string;
362
+ };
363
+ /** Token issued-at (epoch seconds). */
364
+ iat: number;
365
+ /** Token expiry (epoch seconds). */
366
+ exp: number;
367
+ }
368
+
369
+ /**
370
+ * Verifies a Uniform delegation access token against the public JWKS.
371
+ *
372
+ * Use when your BFF makes authorization decisions outside Uniform APIs. If your BFF only
373
+ * proxies requests with the bearer token, skip verification — Uniform validates on its side.
374
+ *
375
+ * @throws When signature, issuer, audience, or expiry validation fails.
376
+ *
377
+ * @example
378
+ * ```ts
379
+ * const claims = await verifyDelegationAccessToken(token, {
380
+ * jwksUrl: getJwksUrl(process.env.UNIFORM_API_HOST),
381
+ * });
382
+ * ```
383
+ */
384
+ declare function verifyDelegationAccessToken(token: string, options?: VerifyDelegationTokenOptions): Promise<DelegationAccessTokenClaims>;
385
+
386
+ export { type CsrfConfig, type CsrfInput, type CsrfResult, DELEGATION_COOKIE_NAME, type DelegationAccessTokenClaims, type DelegationSession, DelegationTokenClient, type DelegationTokenClientOptions, DelegationTokenError, type DelegationTokenErrorKind, type DelegationTokenResponse, SESSION_TTL_SECONDS, type SerializeSessionCookieOptions, type VerifyDelegationTokenOptions, buildDelegationTokenError, classifyDelegationTokenStatus, getJwksUrl, needsRefresh, parseCookies, sealDelegationSession, serializeSessionCookie, uniformApiAudience, uniformDelegationIssuer, unsealDelegationSession, verifyCsrf, verifyDelegationAccessToken };