@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.
@@ -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
@@ -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
@@ -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
@@ -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