@mi9-identity/token-client 1.0.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/CHANGELOG.md +52 -0
- package/LICENSE +201 -0
- package/README.md +402 -0
- package/dist/backoff.d.ts +35 -0
- package/dist/backoff.js +79 -0
- package/dist/claim.d.ts +21 -0
- package/dist/claim.js +169 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.js +485 -0
- package/dist/deadline.d.ts +78 -0
- package/dist/deadline.js +75 -0
- package/dist/drain-body.d.ts +10 -0
- package/dist/drain-body.js +18 -0
- package/dist/endpoints.d.ts +16 -0
- package/dist/endpoints.js +19 -0
- package/dist/errors.d.ts +63 -0
- package/dist/errors.js +63 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/oauth-error.d.ts +23 -0
- package/dist/oauth-error.js +42 -0
- package/dist/request-id.d.ts +9 -0
- package/dist/request-id.js +19 -0
- package/dist/schemas.d.ts +41 -0
- package/dist/schemas.js +44 -0
- package/dist/token-client.constants.d.ts +28 -0
- package/dist/token-client.constants.js +33 -0
- package/dist/type-guards.d.ts +93 -0
- package/dist/type-guards.js +102 -0
- package/dist/types.d.ts +241 -0
- package/dist/types.js +2 -0
- package/package.json +61 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the two Mi9 Identity endpoints this package talks to — `/oauth/token`
|
|
3
|
+
* for minting and `/credentials/me` for the GCP lazy claim and rotation pickup
|
|
4
|
+
* — from a single issuer URL. Deliberately local: the package has no dependency
|
|
5
|
+
* on `@mi9-identity/jwt-verifier`, so a consumer that only mints tokens never
|
|
6
|
+
* installs a JOSE stack to build two URLs. The JWKS URI is the verifier
|
|
7
|
+
* package's concern (`mi9IdentityJwksEndpoint`), not this one's.
|
|
8
|
+
*
|
|
9
|
+
* @param { string } issuerUrl Configured issuer URL (matches the JWT `iss` claim, e.g. `https://identity.mi9retail.com/api/v1`). Trailing slash tolerated.
|
|
10
|
+
* @returns { Readonly<{ tokenEndpoint: string; credentialEndpoint: string }> } Frozen record of `tokenEndpoint` and `credentialEndpoint`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const mi9IdentityEndpoints: (issuerUrl: string) => Readonly<{
|
|
13
|
+
readonly tokenEndpoint: `${string}/oauth/token`;
|
|
14
|
+
readonly credentialEndpoint: `${string}/credentials/me`;
|
|
15
|
+
}>;
|
|
16
|
+
//# sourceMappingURL=endpoints.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the two Mi9 Identity endpoints this package talks to — `/oauth/token`
|
|
3
|
+
* for minting and `/credentials/me` for the GCP lazy claim and rotation pickup
|
|
4
|
+
* — from a single issuer URL. Deliberately local: the package has no dependency
|
|
5
|
+
* on `@mi9-identity/jwt-verifier`, so a consumer that only mints tokens never
|
|
6
|
+
* installs a JOSE stack to build two URLs. The JWKS URI is the verifier
|
|
7
|
+
* package's concern (`mi9IdentityJwksEndpoint`), not this one's.
|
|
8
|
+
*
|
|
9
|
+
* @param { string } issuerUrl Configured issuer URL (matches the JWT `iss` claim, e.g. `https://identity.mi9retail.com/api/v1`). Trailing slash tolerated.
|
|
10
|
+
* @returns { Readonly<{ tokenEndpoint: string; credentialEndpoint: string }> } Frozen record of `tokenEndpoint` and `credentialEndpoint`.
|
|
11
|
+
*/
|
|
12
|
+
export const mi9IdentityEndpoints = (issuerUrl) => {
|
|
13
|
+
const base = issuerUrl.replace(/\/+$/, '');
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
tokenEndpoint: `${base}/oauth/token`,
|
|
16
|
+
credentialEndpoint: `${base}/credentials/me`,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=endpoints.js.map
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retryable failure — the request failed in a way that suggests success on
|
|
3
|
+
* a later attempt (429, 503, timeout, transient network glitch). The token
|
|
4
|
+
* client retries internally with backoff before exposing this to callers.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TransientError extends Error {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The credential secret was rejected (HTTP 401 from `/oauth/token` or
|
|
11
|
+
* `/credentials/me`). The client does NOT retry with the same secret. A GCP
|
|
12
|
+
* (Tier 1) client configured with an `idTokenProvider` re-claims once
|
|
13
|
+
* automatically before surfacing this; Tier 2/3 callers escalate to operators.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RevocationError extends Error {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Credential not provisioned or not usable for the grant it was presented
|
|
20
|
+
* with — HTTP 404 from `/oauth/token` or from the `gcp_identity` lazy-claim at
|
|
21
|
+
* `/credentials/me` (the admin has not created the credential yet, or it was
|
|
22
|
+
* fully revoked), and HTTP 503 `degraded` from that same lazy-claim (a
|
|
23
|
+
* credential exists for the service account but is tagged with a non-`gcp`
|
|
24
|
+
* consumer type). The client cannot recover by retrying — an administrator has
|
|
25
|
+
* to create or correct the row first — but a later attempt succeeds once they
|
|
26
|
+
* have.
|
|
27
|
+
*/
|
|
28
|
+
export declare class ProvisioningError extends Error {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The Tier-1 GCP lazy-claim did not yield a usable credential — HTTP 401/403
|
|
33
|
+
* from the `gcp_identity` grant at `/credentials/me`, the injected
|
|
34
|
+
* `idTokenProvider` threw, or the claimed credential's `authorizedAudiences`
|
|
35
|
+
* does not cover the configured `audience`. Distinct from `RevocationError`
|
|
36
|
+
* (a rejected `clientSecret`) and `ProvisioningError` (no credential bound
|
|
37
|
+
* yet): in every case here the fix is to the ID-token provider, its audience,
|
|
38
|
+
* or the credential's grants — never a bare retry.
|
|
39
|
+
*/
|
|
40
|
+
export declare class LazyClaimError extends Error {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Server response did not match the expected schema. Re-exported so callers
|
|
45
|
+
* can narrow on it in a custom error handler, but it sits outside the
|
|
46
|
+
* Transient / Revocation / Provisioning taxonomy: a shape mismatch indicates
|
|
47
|
+
* an issuer-side bug, not a recoverable condition the client can retry.
|
|
48
|
+
*/
|
|
49
|
+
export declare class ResponseShapeError extends Error {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The client cannot work with the configuration it was given — options that do
|
|
54
|
+
* not describe a usable credential mode, or an `audience` the issuer refuses
|
|
55
|
+
* to mint for (HTTP 403 `invalid_target` from `/oauth/token`). Terminal and
|
|
56
|
+
* outside the Transient / Revocation / Provisioning taxonomy: no retry and no
|
|
57
|
+
* re-claim can clear it, because the fix is a code or credential change, not a
|
|
58
|
+
* later attempt.
|
|
59
|
+
*/
|
|
60
|
+
export declare class ConfigurationError extends Error {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retryable failure — the request failed in a way that suggests success on
|
|
3
|
+
* a later attempt (429, 503, timeout, transient network glitch). The token
|
|
4
|
+
* client retries internally with backoff before exposing this to callers.
|
|
5
|
+
*/
|
|
6
|
+
export class TransientError extends Error {
|
|
7
|
+
name = 'TransientError';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The credential secret was rejected (HTTP 401 from `/oauth/token` or
|
|
11
|
+
* `/credentials/me`). The client does NOT retry with the same secret. A GCP
|
|
12
|
+
* (Tier 1) client configured with an `idTokenProvider` re-claims once
|
|
13
|
+
* automatically before surfacing this; Tier 2/3 callers escalate to operators.
|
|
14
|
+
*/
|
|
15
|
+
export class RevocationError extends Error {
|
|
16
|
+
name = 'RevocationError';
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Credential not provisioned or not usable for the grant it was presented
|
|
20
|
+
* with — HTTP 404 from `/oauth/token` or from the `gcp_identity` lazy-claim at
|
|
21
|
+
* `/credentials/me` (the admin has not created the credential yet, or it was
|
|
22
|
+
* fully revoked), and HTTP 503 `degraded` from that same lazy-claim (a
|
|
23
|
+
* credential exists for the service account but is tagged with a non-`gcp`
|
|
24
|
+
* consumer type). The client cannot recover by retrying — an administrator has
|
|
25
|
+
* to create or correct the row first — but a later attempt succeeds once they
|
|
26
|
+
* have.
|
|
27
|
+
*/
|
|
28
|
+
export class ProvisioningError extends Error {
|
|
29
|
+
name = 'ProvisioningError';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The Tier-1 GCP lazy-claim did not yield a usable credential — HTTP 401/403
|
|
33
|
+
* from the `gcp_identity` grant at `/credentials/me`, the injected
|
|
34
|
+
* `idTokenProvider` threw, or the claimed credential's `authorizedAudiences`
|
|
35
|
+
* does not cover the configured `audience`. Distinct from `RevocationError`
|
|
36
|
+
* (a rejected `clientSecret`) and `ProvisioningError` (no credential bound
|
|
37
|
+
* yet): in every case here the fix is to the ID-token provider, its audience,
|
|
38
|
+
* or the credential's grants — never a bare retry.
|
|
39
|
+
*/
|
|
40
|
+
export class LazyClaimError extends Error {
|
|
41
|
+
name = 'LazyClaimError';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Server response did not match the expected schema. Re-exported so callers
|
|
45
|
+
* can narrow on it in a custom error handler, but it sits outside the
|
|
46
|
+
* Transient / Revocation / Provisioning taxonomy: a shape mismatch indicates
|
|
47
|
+
* an issuer-side bug, not a recoverable condition the client can retry.
|
|
48
|
+
*/
|
|
49
|
+
export class ResponseShapeError extends Error {
|
|
50
|
+
name = 'ResponseShapeError';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The client cannot work with the configuration it was given — options that do
|
|
54
|
+
* not describe a usable credential mode, or an `audience` the issuer refuses
|
|
55
|
+
* to mint for (HTTP 403 `invalid_target` from `/oauth/token`). Terminal and
|
|
56
|
+
* outside the Transient / Revocation / Provisioning taxonomy: no retry and no
|
|
57
|
+
* re-claim can clear it, because the fix is a code or credential change, not a
|
|
58
|
+
* later attempt.
|
|
59
|
+
*/
|
|
60
|
+
export class ConfigurationError extends Error {
|
|
61
|
+
name = 'ConfigurationError';
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=errors.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createGcpTokenClient, createTokenClient } from './client.js';
|
|
2
|
+
export { claimGcpCredential } from './claim.js';
|
|
3
|
+
export { mi9IdentityEndpoints } from './endpoints.js';
|
|
4
|
+
export { ConfigurationError, LazyClaimError, ProvisioningError, ResponseShapeError, RevocationError, TransientError } from './errors.js';
|
|
5
|
+
export { DEFAULT_BACKOFF } from './backoff.js';
|
|
6
|
+
export type { AccessToken, BackoffPolicy, ClaimGcpCredentialOptions, CredentialClaim, GcpTokenClientOptions, Logger, TokenClient, TokenClientOptions } from './types.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createGcpTokenClient, createTokenClient } from './client.js';
|
|
2
|
+
export { claimGcpCredential } from './claim.js';
|
|
3
|
+
export { mi9IdentityEndpoints } from './endpoints.js';
|
|
4
|
+
export { ConfigurationError, LazyClaimError, ProvisioningError, ResponseShapeError, RevocationError, TransientError } from './errors.js';
|
|
5
|
+
export { DEFAULT_BACKOFF } from './backoff.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort extract of the `error` code from an issuer error envelope; falls
|
|
3
|
+
* back to `'unknown'` when the body is missing or malformed. Consumes the
|
|
4
|
+
* response body, so the caller must not read it again.
|
|
5
|
+
*
|
|
6
|
+
* @param { Response } response The non-2xx fetch response.
|
|
7
|
+
* @returns { Promise<string> } The `error` code value, or `'unknown'`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const readOAuthErrorCode: (response: Response) => Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Extract the `error` code plus the envelope's `errorDescription` when present.
|
|
12
|
+
* The issuer's machine-readable sub-codes are audit-only and never on the wire,
|
|
13
|
+
* so `errorDescription` is the only field that names the remedy — dropping it
|
|
14
|
+
* leaves a caller unable to tell `retailer_required` ("specify retailerCode")
|
|
15
|
+
* from an ordinary malformed body, since both are `invalid_request`.
|
|
16
|
+
*
|
|
17
|
+
* Consumes the response body, so the caller must not read it again.
|
|
18
|
+
*
|
|
19
|
+
* @param { Response } response The non-2xx fetch response.
|
|
20
|
+
* @returns { Promise<string> } `"<code>: <description>"`, or just the code when no description is present.
|
|
21
|
+
*/
|
|
22
|
+
export declare const readOAuthErrorDetail: (response: Response) => Promise<string>;
|
|
23
|
+
//# sourceMappingURL=oauth-error.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { oauthErrorSchema } from './schemas.js';
|
|
2
|
+
import { isEmpty, isUndefined } from './type-guards.js';
|
|
3
|
+
/**
|
|
4
|
+
* Best-effort parse of an issuer error envelope. Consumes the response body, so
|
|
5
|
+
* a caller must not read it again — and must not call two of the readers here
|
|
6
|
+
* on the same response.
|
|
7
|
+
*
|
|
8
|
+
* @param { Response } response The non-2xx fetch response.
|
|
9
|
+
* @returns { Promise<OauthError> } The parsed envelope, or an empty object when the body is missing or malformed.
|
|
10
|
+
*/
|
|
11
|
+
const readEnvelope = async (response) => {
|
|
12
|
+
const envelope = oauthErrorSchema.safeParse(await response.json().catch(() => ({})));
|
|
13
|
+
return envelope.success ? envelope.data : {};
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Best-effort extract of the `error` code from an issuer error envelope; falls
|
|
17
|
+
* back to `'unknown'` when the body is missing or malformed. Consumes the
|
|
18
|
+
* response body, so the caller must not read it again.
|
|
19
|
+
*
|
|
20
|
+
* @param { Response } response The non-2xx fetch response.
|
|
21
|
+
* @returns { Promise<string> } The `error` code value, or `'unknown'`.
|
|
22
|
+
*/
|
|
23
|
+
export const readOAuthErrorCode = async (response) => (await readEnvelope(response)).error ?? 'unknown';
|
|
24
|
+
/**
|
|
25
|
+
* Extract the `error` code plus the envelope's `errorDescription` when present.
|
|
26
|
+
* The issuer's machine-readable sub-codes are audit-only and never on the wire,
|
|
27
|
+
* so `errorDescription` is the only field that names the remedy — dropping it
|
|
28
|
+
* leaves a caller unable to tell `retailer_required` ("specify retailerCode")
|
|
29
|
+
* from an ordinary malformed body, since both are `invalid_request`.
|
|
30
|
+
*
|
|
31
|
+
* Consumes the response body, so the caller must not read it again.
|
|
32
|
+
*
|
|
33
|
+
* @param { Response } response The non-2xx fetch response.
|
|
34
|
+
* @returns { Promise<string> } `"<code>: <description>"`, or just the code when no description is present.
|
|
35
|
+
*/
|
|
36
|
+
export const readOAuthErrorDetail = async (response) => {
|
|
37
|
+
const envelope = await readEnvelope(response);
|
|
38
|
+
const code = envelope.error ?? 'unknown';
|
|
39
|
+
const description = envelope.errorDescription;
|
|
40
|
+
return isUndefined(description) || isEmpty(description) ? code : `${code}: ${description}`;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=oauth-error.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the correlation id to send on one outbound request: invoke the
|
|
3
|
+
* caller's getter, pin the caller's fixed string, or mint a fresh UUID.
|
|
4
|
+
*
|
|
5
|
+
* @param { string | (() => string) | undefined } requestId The configured correlation-id source.
|
|
6
|
+
* @returns { string } The correlation id for this request.
|
|
7
|
+
*/
|
|
8
|
+
export declare const resolveRequestId: (requestId: string | (() => string) | undefined) => string;
|
|
9
|
+
//# sourceMappingURL=request-id.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isEmpty, isFunction, isString } from './type-guards.js';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the correlation id to send on one outbound request: invoke the
|
|
5
|
+
* caller's getter, pin the caller's fixed string, or mint a fresh UUID.
|
|
6
|
+
*
|
|
7
|
+
* @param { string | (() => string) | undefined } requestId The configured correlation-id source.
|
|
8
|
+
* @returns { string } The correlation id for this request.
|
|
9
|
+
*/
|
|
10
|
+
export const resolveRequestId = (requestId) => {
|
|
11
|
+
if (isFunction(requestId)) {
|
|
12
|
+
return requestId();
|
|
13
|
+
}
|
|
14
|
+
if (isString(requestId) && !isEmpty(requestId)) {
|
|
15
|
+
return requestId;
|
|
16
|
+
}
|
|
17
|
+
return randomUUID();
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=request-id.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* `POST /oauth/token` success response. `credentialRotated` is the body
|
|
4
|
+
* flag described in design doc §3.3 / §5; the equivalent header is
|
|
5
|
+
* `X-Mi9-Credential-Rotated`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const tokenResponseSchema: z.ZodObject<{
|
|
8
|
+
accessToken: z.ZodString;
|
|
9
|
+
tokenType: z.ZodLiteral<"Bearer">;
|
|
10
|
+
expiresIn: z.ZodNumber;
|
|
11
|
+
credentialRotated: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
}, z.core.$loose>;
|
|
13
|
+
export type TokenResponse = z.infer<typeof tokenResponseSchema>;
|
|
14
|
+
/**
|
|
15
|
+
* `POST /credentials/me` success response. `clientSecret` is the new active
|
|
16
|
+
* secret to swap in via `onSecretRotated`. Closed schema (`.strict()`) so a
|
|
17
|
+
* future caller that logs `parsed.data` for debugging cannot leak unknown
|
|
18
|
+
* fields the issuer adds later — and the secret-bearing payload is never
|
|
19
|
+
* silently widened.
|
|
20
|
+
*/
|
|
21
|
+
export declare const credentialMeResponseSchema: z.ZodObject<{
|
|
22
|
+
clientId: z.ZodString;
|
|
23
|
+
clientSecret: z.ZodString;
|
|
24
|
+
audience: z.ZodArray<z.ZodString>;
|
|
25
|
+
tokenUrl: z.ZodURL;
|
|
26
|
+
}, z.core.$strict>;
|
|
27
|
+
export type CredentialMeResponse = z.infer<typeof credentialMeResponseSchema>;
|
|
28
|
+
/**
|
|
29
|
+
* `application/json` error envelope from `/oauth/token` and `/credentials/me`.
|
|
30
|
+
* Used to extract the `error` code for log lines without leaking the body to
|
|
31
|
+
* upstream callers. The error `code` stays snake_case (RFC 6749 §5.2 reserved),
|
|
32
|
+
* but the wrapping Mi9 fields are camelCase per ADR-12 — `errorDescription`,
|
|
33
|
+
* `requestId`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const oauthErrorSchema: z.ZodObject<{
|
|
36
|
+
error: z.ZodOptional<z.ZodString>;
|
|
37
|
+
errorDescription: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
38
|
+
requestId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
export type OauthError = z.infer<typeof oauthErrorSchema>;
|
|
41
|
+
//# sourceMappingURL=schemas.d.ts.map
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* `POST /oauth/token` success response. `credentialRotated` is the body
|
|
4
|
+
* flag described in design doc §3.3 / §5; the equivalent header is
|
|
5
|
+
* `X-Mi9-Credential-Rotated`.
|
|
6
|
+
*/
|
|
7
|
+
export const tokenResponseSchema = z
|
|
8
|
+
.object({
|
|
9
|
+
accessToken: z.string().min(1),
|
|
10
|
+
tokenType: z.literal('Bearer'),
|
|
11
|
+
expiresIn: z.number().int().positive(),
|
|
12
|
+
credentialRotated: z.boolean().optional(),
|
|
13
|
+
})
|
|
14
|
+
.passthrough();
|
|
15
|
+
/**
|
|
16
|
+
* `POST /credentials/me` success response. `clientSecret` is the new active
|
|
17
|
+
* secret to swap in via `onSecretRotated`. Closed schema (`.strict()`) so a
|
|
18
|
+
* future caller that logs `parsed.data` for debugging cannot leak unknown
|
|
19
|
+
* fields the issuer adds later — and the secret-bearing payload is never
|
|
20
|
+
* silently widened.
|
|
21
|
+
*/
|
|
22
|
+
export const credentialMeResponseSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
clientId: z.string().min(1),
|
|
25
|
+
clientSecret: z.string().min(1),
|
|
26
|
+
audience: z.array(z.string()),
|
|
27
|
+
tokenUrl: z.url(),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
/**
|
|
31
|
+
* `application/json` error envelope from `/oauth/token` and `/credentials/me`.
|
|
32
|
+
* Used to extract the `error` code for log lines without leaking the body to
|
|
33
|
+
* upstream callers. The error `code` stays snake_case (RFC 6749 §5.2 reserved),
|
|
34
|
+
* but the wrapping Mi9 fields are camelCase per ADR-12 — `errorDescription`,
|
|
35
|
+
* `requestId`.
|
|
36
|
+
*/
|
|
37
|
+
export const oauthErrorSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
error: z.string(),
|
|
40
|
+
errorDescription: z.string().optional(),
|
|
41
|
+
requestId: z.string().optional(),
|
|
42
|
+
})
|
|
43
|
+
.partial();
|
|
44
|
+
//# sourceMappingURL=schemas.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Logger } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Retry budget for transient (429 / 5xx / network) failures — the retry loop
|
|
4
|
+
* runs `MAX_RETRIES + 1` attempts in total before surfacing a `TransientError`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MAX_RETRIES = 5;
|
|
7
|
+
/**
|
|
8
|
+
* Default outbound correlation-id header name. Casing matches the design
|
|
9
|
+
* doc's canonical form; HTTP transports normalize it on the wire.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_REQUEST_ID_HEADER = "X-Request-ID";
|
|
12
|
+
/**
|
|
13
|
+
* Default proactive-refresh lead time — a cached token is re-minted once it is
|
|
14
|
+
* within five minutes of expiry (T-5 min for a 1 h TTL).
|
|
15
|
+
*/
|
|
16
|
+
export declare const DEFAULT_REFRESH_LEAD_TIME_MS: number;
|
|
17
|
+
/**
|
|
18
|
+
* Issuer error-envelope code accompanying a 503 when the credential bound to
|
|
19
|
+
* the caller's identity is not eligible for the grant it used. Operator
|
|
20
|
+
* misconfiguration, permanent until an administrator corrects the row — so it
|
|
21
|
+
* must never be treated as a retryable 5xx.
|
|
22
|
+
*/
|
|
23
|
+
export declare const OAUTH_ERROR_CODE_DEGRADED = "degraded";
|
|
24
|
+
/**
|
|
25
|
+
* Structural no-op logger installed when the caller injects none.
|
|
26
|
+
*/
|
|
27
|
+
export declare const NOOP_LOGGER: Logger;
|
|
28
|
+
//# sourceMappingURL=token-client.constants.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry budget for transient (429 / 5xx / network) failures — the retry loop
|
|
3
|
+
* runs `MAX_RETRIES + 1` attempts in total before surfacing a `TransientError`.
|
|
4
|
+
*/
|
|
5
|
+
export const MAX_RETRIES = 5;
|
|
6
|
+
/**
|
|
7
|
+
* Default outbound correlation-id header name. Casing matches the design
|
|
8
|
+
* doc's canonical form; HTTP transports normalize it on the wire.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_REQUEST_ID_HEADER = 'X-Request-ID';
|
|
11
|
+
/**
|
|
12
|
+
* Default proactive-refresh lead time — a cached token is re-minted once it is
|
|
13
|
+
* within five minutes of expiry (T-5 min for a 1 h TTL).
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_REFRESH_LEAD_TIME_MS = 5 * 60_000;
|
|
16
|
+
/**
|
|
17
|
+
* Issuer error-envelope code accompanying a 503 when the credential bound to
|
|
18
|
+
* the caller's identity is not eligible for the grant it used. Operator
|
|
19
|
+
* misconfiguration, permanent until an administrator corrects the row — so it
|
|
20
|
+
* must never be treated as a retryable 5xx.
|
|
21
|
+
*/
|
|
22
|
+
export const OAUTH_ERROR_CODE_DEGRADED = 'degraded';
|
|
23
|
+
/**
|
|
24
|
+
* Structural no-op logger installed when the caller injects none.
|
|
25
|
+
*/
|
|
26
|
+
export const NOOP_LOGGER = {
|
|
27
|
+
/* v8 ignore next -- debug is part of the structural Logger contract; the client never calls it but consumers may */
|
|
28
|
+
debug: () => undefined,
|
|
29
|
+
info: () => undefined,
|
|
30
|
+
warn: () => undefined,
|
|
31
|
+
error: () => undefined,
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=token-client.constants.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrowing type guards, duplicated per package by design (see CLAUDE.md) —
|
|
3
|
+
* mirrored from the identity service's canonical `utils/type-guards.ts`, plus
|
|
4
|
+
* a package-local `isFunction` (the canonical set has no function guard).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Strict null check.
|
|
8
|
+
*
|
|
9
|
+
* @param { unknown } value Value to test.
|
|
10
|
+
* @returns { value is null } True iff `value === null`.
|
|
11
|
+
* @example
|
|
12
|
+
* isNull(null); // true
|
|
13
|
+
* isNull(undefined); // false
|
|
14
|
+
* isNull(0); // false
|
|
15
|
+
* isNull(''); // false
|
|
16
|
+
*/
|
|
17
|
+
export declare const isNull: (value: unknown) => value is null;
|
|
18
|
+
/**
|
|
19
|
+
* Strict undefined check.
|
|
20
|
+
*
|
|
21
|
+
* @param { unknown } value Value to test.
|
|
22
|
+
* @returns { value is undefined } True iff `value === undefined`.
|
|
23
|
+
* @example
|
|
24
|
+
* isUndefined(undefined); // true
|
|
25
|
+
* isUndefined(null); // false
|
|
26
|
+
* isUndefined(0); // false
|
|
27
|
+
*/
|
|
28
|
+
export declare const isUndefined: (value: unknown) => value is undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Nullish check (null or undefined).
|
|
31
|
+
*
|
|
32
|
+
* @param { unknown } value Value to test.
|
|
33
|
+
* @returns { value is null | undefined } True iff `value` is null or undefined.
|
|
34
|
+
* @example
|
|
35
|
+
* isNil(null); // true
|
|
36
|
+
* isNil(undefined); // true
|
|
37
|
+
* isNil(0); // false
|
|
38
|
+
* isNil(''); // false
|
|
39
|
+
*/
|
|
40
|
+
export declare const isNil: (value: unknown) => value is null | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Primitive string check.
|
|
43
|
+
*
|
|
44
|
+
* @param { unknown } value Value to test.
|
|
45
|
+
* @returns { value is string } True iff `typeof value === 'string'`.
|
|
46
|
+
* @example
|
|
47
|
+
* isString('hello'); // true
|
|
48
|
+
* isString(''); // true
|
|
49
|
+
* isString(42); // false
|
|
50
|
+
* isString(new String('hello')); // false (boxed String, not primitive)
|
|
51
|
+
*/
|
|
52
|
+
export declare const isString: (value: unknown) => value is string;
|
|
53
|
+
/**
|
|
54
|
+
* Narrowing return type for `isEmpty`. Listed unions are the structural
|
|
55
|
+
* "empty" shapes; the negation `!isEmpty(x)` strips `null | undefined`
|
|
56
|
+
* (and the empty literal forms where TS retains discrimination), so a
|
|
57
|
+
* `string | undefined` source narrows to `string` after a non-empty check.
|
|
58
|
+
*/
|
|
59
|
+
type EmptyValue = null | undefined | '' | readonly never[] | Map<unknown, never> | Set<never> | Record<string, never>;
|
|
60
|
+
/**
|
|
61
|
+
* Empty-shape check (nullish, empty string, array, Map, Set, or plain object). Numbers and booleans are never empty.
|
|
62
|
+
*
|
|
63
|
+
* @param { unknown } value Value to test.
|
|
64
|
+
* @returns { value is EmptyValue } True iff `value` is an empty shape.
|
|
65
|
+
* @example
|
|
66
|
+
* isEmpty(null); // true
|
|
67
|
+
* isEmpty(undefined); // true
|
|
68
|
+
* isEmpty(''); // true
|
|
69
|
+
* isEmpty([]); // true
|
|
70
|
+
* isEmpty({}); // true
|
|
71
|
+
* isEmpty(new Map()); // true
|
|
72
|
+
* isEmpty(new Set()); // true
|
|
73
|
+
* isEmpty('hello'); // false
|
|
74
|
+
* isEmpty(0); // false
|
|
75
|
+
* isEmpty(false); // false
|
|
76
|
+
* isEmpty([0]); // false
|
|
77
|
+
* isEmpty({ a: undefined }); // false (key still counted)
|
|
78
|
+
*/
|
|
79
|
+
export declare const isEmpty: (value: unknown) => value is EmptyValue;
|
|
80
|
+
/**
|
|
81
|
+
* Callable check. Narrowing preserves the callable member of a union so the
|
|
82
|
+
* value stays invocable with its original signature.
|
|
83
|
+
*
|
|
84
|
+
* @param { unknown } value Value to test.
|
|
85
|
+
* @returns { value is (...args: never[]) => unknown } True iff `typeof value === 'function'`.
|
|
86
|
+
* @example
|
|
87
|
+
* isFunction(() => 1); // true
|
|
88
|
+
* isFunction(class {}); // true (classes are callable)
|
|
89
|
+
* isFunction(42); // false
|
|
90
|
+
*/
|
|
91
|
+
export declare const isFunction: (value: unknown) => value is (...args: never[]) => unknown;
|
|
92
|
+
export {};
|
|
93
|
+
//# sourceMappingURL=type-guards.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrowing type guards, duplicated per package by design (see CLAUDE.md) —
|
|
3
|
+
* mirrored from the identity service's canonical `utils/type-guards.ts`, plus
|
|
4
|
+
* a package-local `isFunction` (the canonical set has no function guard).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Strict null check.
|
|
8
|
+
*
|
|
9
|
+
* @param { unknown } value Value to test.
|
|
10
|
+
* @returns { value is null } True iff `value === null`.
|
|
11
|
+
* @example
|
|
12
|
+
* isNull(null); // true
|
|
13
|
+
* isNull(undefined); // false
|
|
14
|
+
* isNull(0); // false
|
|
15
|
+
* isNull(''); // false
|
|
16
|
+
*/
|
|
17
|
+
export const isNull = (value) => value === null;
|
|
18
|
+
/**
|
|
19
|
+
* Strict undefined check.
|
|
20
|
+
*
|
|
21
|
+
* @param { unknown } value Value to test.
|
|
22
|
+
* @returns { value is undefined } True iff `value === undefined`.
|
|
23
|
+
* @example
|
|
24
|
+
* isUndefined(undefined); // true
|
|
25
|
+
* isUndefined(null); // false
|
|
26
|
+
* isUndefined(0); // false
|
|
27
|
+
*/
|
|
28
|
+
export const isUndefined = (value) => value === undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Nullish check (null or undefined).
|
|
31
|
+
*
|
|
32
|
+
* @param { unknown } value Value to test.
|
|
33
|
+
* @returns { value is null | undefined } True iff `value` is null or undefined.
|
|
34
|
+
* @example
|
|
35
|
+
* isNil(null); // true
|
|
36
|
+
* isNil(undefined); // true
|
|
37
|
+
* isNil(0); // false
|
|
38
|
+
* isNil(''); // false
|
|
39
|
+
*/
|
|
40
|
+
export const isNil = (value) => isNull(value) || isUndefined(value);
|
|
41
|
+
/**
|
|
42
|
+
* Primitive string check.
|
|
43
|
+
*
|
|
44
|
+
* @param { unknown } value Value to test.
|
|
45
|
+
* @returns { value is string } True iff `typeof value === 'string'`.
|
|
46
|
+
* @example
|
|
47
|
+
* isString('hello'); // true
|
|
48
|
+
* isString(''); // true
|
|
49
|
+
* isString(42); // false
|
|
50
|
+
* isString(new String('hello')); // false (boxed String, not primitive)
|
|
51
|
+
*/
|
|
52
|
+
export const isString = (value) => typeof value === 'string';
|
|
53
|
+
/**
|
|
54
|
+
* Empty-shape check (nullish, empty string, array, Map, Set, or plain object). Numbers and booleans are never empty.
|
|
55
|
+
*
|
|
56
|
+
* @param { unknown } value Value to test.
|
|
57
|
+
* @returns { value is EmptyValue } True iff `value` is an empty shape.
|
|
58
|
+
* @example
|
|
59
|
+
* isEmpty(null); // true
|
|
60
|
+
* isEmpty(undefined); // true
|
|
61
|
+
* isEmpty(''); // true
|
|
62
|
+
* isEmpty([]); // true
|
|
63
|
+
* isEmpty({}); // true
|
|
64
|
+
* isEmpty(new Map()); // true
|
|
65
|
+
* isEmpty(new Set()); // true
|
|
66
|
+
* isEmpty('hello'); // false
|
|
67
|
+
* isEmpty(0); // false
|
|
68
|
+
* isEmpty(false); // false
|
|
69
|
+
* isEmpty([0]); // false
|
|
70
|
+
* isEmpty({ a: undefined }); // false (key still counted)
|
|
71
|
+
*/
|
|
72
|
+
export const isEmpty = (value) => {
|
|
73
|
+
if (isNil(value)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (isString(value)) {
|
|
77
|
+
return value.length === 0;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
return value.length === 0;
|
|
81
|
+
}
|
|
82
|
+
if (value instanceof Map || value instanceof Set) {
|
|
83
|
+
return value.size === 0;
|
|
84
|
+
}
|
|
85
|
+
if (typeof value === 'object') {
|
|
86
|
+
return Object.keys(value).length === 0;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Callable check. Narrowing preserves the callable member of a union so the
|
|
92
|
+
* value stays invocable with its original signature.
|
|
93
|
+
*
|
|
94
|
+
* @param { unknown } value Value to test.
|
|
95
|
+
* @returns { value is (...args: never[]) => unknown } True iff `typeof value === 'function'`.
|
|
96
|
+
* @example
|
|
97
|
+
* isFunction(() => 1); // true
|
|
98
|
+
* isFunction(class {}); // true (classes are callable)
|
|
99
|
+
* isFunction(42); // false
|
|
100
|
+
*/
|
|
101
|
+
export const isFunction = (value) => typeof value === 'function';
|
|
102
|
+
//# sourceMappingURL=type-guards.js.map
|