@kanonak-protocol/sdk 5.38.0 → 6.0.1

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.
@@ -1,65 +1,85 @@
1
1
  /**
2
2
  * The credential seam between the open protocol layer and a platform's identity
3
- * mechanism (see ideas/protocol-vs-platform-auth-split.md).
3
+ * mechanism (see ideas/protocol-vs-platform-auth-split.md, and #126 for its
4
+ * resource-scoped shape).
4
5
  *
5
6
  * The protocol layer's entire knowledge of credentials is this interface: given
6
- * an authority (and any server challenge), a provider returns headers to attach
7
- * to a request, or declines. How the credential was minted, stored, or proven
8
- * never crosses this boundary — that is the platform's business.
9
- */
10
- /** A fetch that authenticates a request to a publisher/resource host. The
11
- * consumption transport produced by {@link createCredentialedFetch} has this
12
- * shape; repositories and the server accept it so a host can inject auth. */
13
- export type AuthenticatedFetchFn = (url: string, publisher: string, method?: string) => Promise<Response>;
14
- /**
15
- * What a provider contributes to an outgoing request — scheme-agnostic across
16
- * header-based credentials (Bearer, DPoP-bound, Basic-wrapped JWT). The protocol
17
- * layer merges `headers` verbatim and never inspects how they were formed.
7
+ * the resource being requested (and any server challenge), a provider returns
8
+ * headers and the scope they are good for, or declines. How the credential was
9
+ * minted, stored, or proven never crosses this boundary. The provider decides
10
+ * how wide the scope is; the transport never widens it, attaches the headers
11
+ * only to URLs inside it, and caches them by it until `expiresAt`.
18
12
  *
19
- * Transport-level mTLS is deliberately NOT expressible here: a client cert is
20
- * presented in the TLS handshake, not a header. mTLS is an acquisition mechanism
21
- * (cert → token); consumption uses the minted, header-based credential.
13
+ * The same shapes are the exec credential helper's wire protocol
14
+ * ({@link ExecCredentialProvider}), so a helper and an in-process provider are
15
+ * interchangeable.
22
16
  */
23
- export interface RequestCredential {
24
- /** Headers to merge onto the request (e.g. Authorization, plus any proof
25
- * header such as an RFC 9449 DPoP proof). */
26
- headers: Record<string, string>;
27
- /** Validity of the underlying session credential (NOT the per-request proof
28
- * header), so the consumer can pre-empt re-acquisition before a 401. */
29
- expiresAt?: string;
30
- }
17
+ /** A fetch that authenticates a request. The consumption transport produced by
18
+ * {@link createCredentialedFetch} has this shape; repositories and the server
19
+ * accept it so a host can inject auth. `publisher` names the package publisher
20
+ * the request is for; credentials are chosen by the request URL, not by it. */
21
+ export type AuthenticatedFetchFn = (url: string, publisher: string, method?: string) => Promise<Response>;
31
22
  /**
32
- * A server's authorization challenge, parsed from a 401/403 and forwarded to the
33
- * provider verbatim. The protocol layer stays dumb: it parses and relays; the
34
- * provider decides how to satisfy it (bind a nonce, step up, or re-mint).
23
+ * One challenge from a `WWW-Authenticate` response header (RFC 9110 §11.6.1),
24
+ * e.g. `Bearer resource_metadata="…", error="invalid_token"`.
35
25
  */
36
26
  export interface AuthChallenge {
37
- /** The raw WWW-Authenticate header value. */
38
- wwwAuthenticate: string;
39
- /** RFC 9449 server-supplied DPoP nonce, if present, to bind the next proof. */
40
- nonce?: string;
41
- /** RFC 9470 required assurance (acr_values), if the server demands step-up. */
42
- acrValues?: string;
27
+ /** The auth-scheme as the server spelled it; compare case-insensitively. */
28
+ scheme: string;
29
+ /** The auth-params, names lower-cased (RFC 9110: case-insensitive), values unquoted. */
30
+ params: Record<string, string>;
31
+ /** A token68 value, for a challenge that carries one instead of auth-params. */
32
+ token68?: string;
43
33
  }
44
- export interface ResolveOptions {
45
- /** The HTTP method of the request being authenticated (for per-request proofs). */
46
- method?: string;
47
- /** The URL being authenticated (for per-request proofs). */
48
- url?: string;
49
- /** Re-mint after a server-side revocation — a *stale* credential. */
50
- forceRefresh?: boolean;
51
- /** The server's challenge from a prior 401/403 on this request. */
52
- challenge?: AuthChallenge;
34
+ /** What the transport asks a provider for. */
35
+ export interface CredentialRequest {
36
+ /** The absolute https URL being requested. */
37
+ resource: string;
38
+ /** The request's HTTP method. */
39
+ method: string;
40
+ /** The server's challenges, in the order it sent them. Absent when the
41
+ * transport asks pre-emptively, for a resource it knows is gated. */
42
+ challenges?: AuthChallenge[];
53
43
  }
44
+ /** A request to forget the credential a server rejected. */
45
+ export interface CredentialEraseRequest extends CredentialRequest {
46
+ /** The scope of the rejected credential. */
47
+ scope: string;
48
+ }
49
+ /** A credential: headers, and where they may be attached. */
50
+ export interface Credential {
51
+ /** Headers merged verbatim onto requests inside `scope` (e.g. Authorization). */
52
+ headers: Record<string, string>;
53
+ /** An https origin plus a path prefix, matched on whole path segments. It must
54
+ * contain the requested resource. */
55
+ scope: string;
56
+ /** When the credential stops being valid, in Unix seconds. Absent: valid for
57
+ * as long as this process runs. */
58
+ expiresAt?: number;
59
+ }
60
+ /** A provider's answer. `declined` means only "no credential for this resource";
61
+ * a failure is an exception, never a decline. */
62
+ export type CredentialAnswer = ({
63
+ status: 'credential';
64
+ } & Credential) | {
65
+ status: 'declined';
66
+ reason?: string;
67
+ };
54
68
  /**
55
69
  * The credential seam. A platform implements this; the protocol layer only calls
56
- * it. Returns null to decline (anonymous / not this provider's authority).
57
- *
58
- * `forceRefresh` ("the credential is stale — mint the same kind") is distinct
59
- * from a `challenge` carrying `acrValues` ("the credential is insufficient —
60
- * acquire a stronger one"). Keeping them separate lets the provider tell those
61
- * apart; a binary drop-and-retry cannot.
70
+ * it. `get` answers for a resource; `erase` forgets a credential the server
71
+ * rejected (401), before the transport asks once more. A server that finds a
72
+ * credential valid but insufficient (403 with `error="insufficient_scope"`) gets
73
+ * `get` with its challenge and no `erase`: the credential still serves the rest
74
+ * of its scope.
62
75
  */
63
76
  export interface CredentialProvider {
64
- resolve(authority: string, opts?: ResolveOptions): Promise<RequestCredential | null>;
77
+ get(request: CredentialRequest): Promise<CredentialAnswer>;
78
+ erase(request: CredentialEraseRequest): Promise<void>;
79
+ }
80
+ /** A request that could not be authenticated: a provider failed, declined a
81
+ * resource that requires a credential, answered outside the protocol, or a
82
+ * server rejected the credential it answered with. Never silently anonymous. */
83
+ export declare class CredentialError extends Error {
84
+ constructor(message: string);
65
85
  }
@@ -0,0 +1,55 @@
1
+ import type { Writable } from 'node:stream';
2
+ import { type CredentialAnswer, type CredentialEraseRequest, type CredentialProvider, type CredentialRequest } from './CredentialProvider.js';
3
+ /** How long a helper may run before it is stopped. */
4
+ export declare const CREDENTIAL_HELPER_TIMEOUT_MS = 120000;
5
+ export interface ExecCredentialProviderOptions {
6
+ /** The helper's command line. It is split on whitespace and run without a
7
+ * shell (as Go's GOAUTH runs its commands); the operation, `get` or `erase`,
8
+ * is appended as the last argument. */
9
+ command: string;
10
+ /** Whether the helper may interact with the user; sent as `interactive`. */
11
+ interactive?: boolean;
12
+ /** Milliseconds a helper may run; {@link CREDENTIAL_HELPER_TIMEOUT_MS} by default. */
13
+ timeoutMs?: number;
14
+ /** Where the helper's stderr is shown as it runs; `process.stderr` by default. */
15
+ stderr?: Writable;
16
+ /** The clock, in Unix seconds. */
17
+ nowSeconds?: () => number;
18
+ }
19
+ /**
20
+ * A {@link CredentialProvider} that is a separate program: the exec credential
21
+ * helper protocol (#126), the shape git, docker, cargo (RFC 3139), kubectl and
22
+ * Bazel credential helpers share.
23
+ *
24
+ * The provider runs `<command> get` or `<command> erase`, writes one JSON
25
+ * request to its stdin, and reads one JSON answer from its stdout:
26
+ *
27
+ * request {"version": 1, "operation": "get" | "erase", "resource": URL,
28
+ * "method": "GET", "challenges"?: [{scheme, params, token68?}],
29
+ * "scope"?: (erase: the rejected credential's scope), "interactive": bool}
30
+ * get {"version": 1, "status": "credential", "headers": {...},
31
+ * "scope": "https://host/prefix", "expiresAt"?: unix-seconds}
32
+ * or {"version": 1, "status": "declined", "reason"?: "..."}
33
+ * erase {"version": 1, "status": "erased"}
34
+ *
35
+ * A non-zero exit, a timeout, or an answer outside the protocol is a
36
+ * {@link CredentialError} carrying the helper's stderr; nothing falls back to
37
+ * anonymous. The helper's stderr is also shown as it runs. Requests and answers
38
+ * are never logged: they carry credentials.
39
+ */
40
+ export declare class ExecCredentialProvider implements CredentialProvider {
41
+ private readonly argv;
42
+ private readonly interactive;
43
+ private readonly timeoutMs;
44
+ private readonly stderr;
45
+ private readonly now;
46
+ constructor(opts: ExecCredentialProviderOptions);
47
+ /** The command line as configured. */
48
+ get command(): string;
49
+ get(request: CredentialRequest): Promise<CredentialAnswer>;
50
+ erase(request: CredentialEraseRequest): Promise<void>;
51
+ private wireRequest;
52
+ private run;
53
+ }
54
+ /** A helper command line split the way it is run: on whitespace, no quoting. */
55
+ export declare function splitCommandLine(command: string): string[];
@@ -1,3 +1,14 @@
1
+ /**
2
+ * OAuth 2.0 Protected Resource Metadata (RFC 9728), as validated by
3
+ * {@link OAuthDiscovery}: only documents whose `resource` checks out are returned.
4
+ */
5
+ export interface ProtectedResourceMetadata {
6
+ /** The protected resource's identifier (`resource`). */
7
+ resource: string;
8
+ /** Issuer identifiers of the authorization servers that protect it
9
+ * (`authorization_servers`; empty when the document names none). */
10
+ authorizationServers: string[];
11
+ }
1
12
  /**
2
13
  * OAuth 2.0 Authorization Server Metadata (RFC 8414).
3
14
  */
@@ -44,18 +55,35 @@ export declare const DEFAULT_CONSENT_BINDING_PARAM = "key_thumbprint";
44
55
  */
45
56
  export declare class OAuthDiscovery {
46
57
  private readonly cache;
47
- private readonly prCache;
58
+ private readonly wellKnownCache;
59
+ private readonly metadataUrlCache;
60
+ /**
61
+ * RFC 9728 §3.1: the metadata of the resource `identifier` (an https URL, e.g.
62
+ * an origin), at its well-known URL: `/.well-known/oauth-protected-resource`
63
+ * inserted between the host and the identifier's path. Validated per §3.3:
64
+ * the document's `resource` must be identical to `identifier` (compared after
65
+ * URL normalization, so `https://h` and `https://h/` are one identifier).
66
+ *
67
+ * null when the resource does not advertise metadata there: a 404 or 410, a
68
+ * redirect (metadata for an origin is served by that origin), or a 200 that is
69
+ * not a JSON document (static hosts answer unknown paths with an HTML page).
70
+ * Throws a `KanonakHttpError` for any other status, including 401/403 (the
71
+ * host gates even its metadata), and an `Error` for a JSON document that
72
+ * fails validation, which RFC 9728 forbids using.
73
+ */
74
+ protectedResourceMetadata(identifier: string): Promise<ProtectedResourceMetadata | null>;
48
75
  /**
49
- * RFC 9728 (OAuth 2.0 Protected Resource Metadata): the authorization
50
- * server(s) that protect a resource host. The git credential helper uses this
51
- * to resolve a git/registry host → its Kanonak authority (issuer URLs in
52
- * `authorization_servers`); RFC 8414 on that authority then yields the session
53
- * endpoint. Returns null when the host advertises no protected-resource
54
- * metadata — i.e. it isn't a Kanonak-credentialed host, so the helper declines
55
- * and git falls through. Quiet on misses (the helper probes every host git
56
- * asks about, most of which won't be Kanonak resources).
76
+ * RFC 9728 §5.1: the metadata at the URL a challenge's `resource_metadata`
77
+ * parameter names, for the request to `requestedUrl` that drew the challenge.
78
+ * §3.3 requires its `resource` to match the requested URL. A resource
79
+ * identifier covers every URL beneath it, so this checks that `requestedUrl`
80
+ * lies inside `resource`: same origin, on whole path segments (the reading
81
+ * deployed clients use). Throws when the document is missing, is not JSON,
82
+ * or fails that check.
57
83
  */
58
- discoverProtectedResource(resourceHost: string): Promise<string[] | null>;
84
+ protectedResourceMetadataAt(metadataUrl: string, requestedUrl: string): Promise<ProtectedResourceMetadata>;
85
+ private fetchWellKnown;
86
+ private fetchMetadataAt;
59
87
  discover(host: string): Promise<OAuthServerMetadata | null>;
60
88
  supportsOAuth(host: string): Promise<boolean>;
61
89
  static supportsPkceS256(m: OAuthServerMetadata): boolean;
@@ -1,4 +1,5 @@
1
- import type { CredentialProvider, RequestCredential, ResolveOptions } from './CredentialProvider.js';
1
+ import type { CredentialAnswer, CredentialProvider, CredentialRequest } from './CredentialProvider.js';
2
+ import { OAuthDiscovery } from './OAuthDiscovery.js';
2
3
  /**
3
4
  * Reference / BYO-token credential provider — the open layer's minimal way to
4
5
  * consume a gated package without any platform tooling. It reads a token from
@@ -6,11 +7,18 @@ import type { CredentialProvider, RequestCredential, ResolveOptions } from './Cr
6
7
  *
7
8
  * `Bearer` here is only the *transport* of whatever the issuer minted — its
8
9
  * security properties are the issuer's choice, not the protocol's. A platform
9
- * ships a stronger, sender-constrained provider (e.g. a DPoP-bound JWT minted
10
- * from a device cert); this one keeps the open SDK/CLI usable standalone, e.g.
11
- * for an enterprise pointing at its own OAuth-issued token, or for CI.
10
+ * supplies a stronger provider, in process or as an exec credential helper;
11
+ * this one keeps the open SDK/CLI usable standalone, e.g. for an enterprise
12
+ * pointing at its own OAuth-issued token, or for CI.
12
13
  *
13
- * Token sources, in precedence order, per authority host:
14
+ * A token is presented only to a resource that names its authorization server
15
+ * in validated RFC 9728 metadata: from the challenge's `resource_metadata`, or
16
+ * else at the resource origin's well-known URL. The credential's scope is that
17
+ * metadata's `resource`, so the token is attached nowhere else. A resource with
18
+ * no such metadata is declined.
19
+ *
20
+ * Token sources, in precedence order, per authorization-server host (the first
21
+ * of the resource's `authorization_servers` that has one):
14
22
  * 1. an explicit `tokens` map
15
23
  * 2. env `KANONAK_TOKEN_<AUTHORITY>` (host upper-cased, non-alphanumeric → `_`)
16
24
  * 3. env `KANONAK_TOKEN` (applies to any authority)
@@ -20,10 +28,22 @@ export interface TokenCredentialProviderOptions {
20
28
  tokens?: Record<string, string>;
21
29
  /** Environment to read from; defaults to `process.env` when available. */
22
30
  env?: Record<string, string | undefined>;
31
+ /** RFC 9728 discovery; a fresh {@link OAuthDiscovery} by default. */
32
+ discovery?: Pick<OAuthDiscovery, 'protectedResourceMetadata' | 'protectedResourceMetadataAt'>;
23
33
  }
34
+ /** The environment variable a BYO token is read from, and the prefix of its
35
+ * per-authority form. */
36
+ export declare const TOKEN_ENV = "KANONAK_TOKEN";
37
+ /** `KANONAK_TOKEN_<AUTHORITY>` for an authorization-server host. */
38
+ export declare function tokenEnvKeyFor(authority: string): string;
24
39
  export declare class TokenCredentialProvider implements CredentialProvider {
25
40
  private readonly tokens;
26
41
  private readonly env;
42
+ private readonly discovery;
27
43
  constructor(opts?: TokenCredentialProviderOptions);
28
- resolve(authority: string, _opts?: ResolveOptions): Promise<RequestCredential | null>;
44
+ get(request: CredentialRequest): Promise<CredentialAnswer>;
45
+ /** A configured token cannot be erased; the transport asks once more, gets the
46
+ * same token, and reports the server's second rejection. */
47
+ erase(): Promise<void>;
48
+ private metadataFor;
29
49
  }
@@ -0,0 +1,17 @@
1
+ import type { AuthChallenge } from './CredentialProvider.js';
2
+ /**
3
+ * Parse a `WWW-Authenticate` field value into its challenges (RFC 9110 §11.6.1):
4
+ *
5
+ * WWW-Authenticate = #challenge
6
+ * challenge = auth-scheme [ 1*SP ( token68 / #auth-param ) ]
7
+ * auth-param = token BWS "=" BWS ( token / quoted-string )
8
+ * token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
9
+ *
10
+ * Several header lines arrive joined with ", " (the Fetch `Headers` contract),
11
+ * which is the same list syntax, so one value covers them all. Challenges keep
12
+ * the server's order. Parameter names are lower-cased, because RFC 9110 makes
13
+ * them case-insensitive; the scheme keeps the server's spelling (compare it
14
+ * case-insensitively). A malformed value, or a parameter repeated within one
15
+ * challenge, throws: a challenge we cannot read is not one we can act on.
16
+ */
17
+ export declare function parseWwwAuthenticate(value: string): AuthChallenge[];
@@ -0,0 +1,34 @@
1
+ import type { CredentialAnswer, CredentialRequest } from './CredentialProvider.js';
2
+ /** The exec credential helper protocol version this SDK speaks (#126). */
3
+ export declare const CREDENTIAL_HELPER_PROTOCOL_VERSION = 1;
4
+ /** Why an answer was refused. Stable: the conformance vectors pin them. */
5
+ export type AnswerProblem = 'malformed' | 'version' | 'status' | 'headers' | 'scope' | 'scope-excludes-resource' | 'expires-at' | 'expired' | 'reason';
6
+ export type AnswerCheck<T> = {
7
+ ok: true;
8
+ answer: T;
9
+ } | {
10
+ ok: false;
11
+ problem: AnswerProblem;
12
+ message: string;
13
+ };
14
+ /**
15
+ * Check a provider's answer to `request` against the protocol, for any provider,
16
+ * in-process or exec. A credential must:
17
+ * - carry at least one header, each a valid field name with a value free of
18
+ * CR, LF and NUL;
19
+ * - name a scope that is an https origin plus a path prefix (no userinfo, query
20
+ * or fragment) and that contains the requested resource;
21
+ * - when it has `expiresAt` (Unix seconds), not be expired at `nowSeconds`.
22
+ * A decline may carry a string `reason`. Fields the protocol does not define
23
+ * are ignored, so a newer provider can add some.
24
+ */
25
+ export declare function checkCredentialAnswer(request: CredentialRequest, value: unknown, nowSeconds: number): AnswerCheck<CredentialAnswer>;
26
+ /** {@link checkCredentialAnswer} for a helper's wire answer, which also carries
27
+ * `"version": 1`. */
28
+ export declare function checkHelperAnswer(request: CredentialRequest, value: unknown, nowSeconds: number): AnswerCheck<CredentialAnswer>;
29
+ /** A helper's answer to `erase`: exactly `{"version": 1, "status": "erased"}`
30
+ * (other fields ignored). Erasing a credential the helper no longer holds is
31
+ * still "erased". */
32
+ export declare function checkHelperEraseAnswer(value: unknown): AnswerCheck<{
33
+ status: 'erased';
34
+ }>;
@@ -0,0 +1,32 @@
1
+ import type { CredentialProvider } from './CredentialProvider.js';
2
+ import type { OAuthDiscovery } from './OAuthDiscovery.js';
3
+ /** The environment variable naming a credential helper's command line. */
4
+ export declare const CREDENTIAL_HELPER_ENV = "KANONAK_CREDENTIAL_HELPER";
5
+ /** The user-level config file: `~/.kanonak/config.json`. */
6
+ export declare function userConfigPath(): string;
7
+ export interface CredentialProviderConfigOptions {
8
+ /** Environment to read; `process.env` by default. */
9
+ env?: Record<string, string | undefined>;
10
+ /** The user config file; {@link userConfigPath} by default. */
11
+ configPath?: string;
12
+ /** Whether a helper may interact with the user. */
13
+ interactive?: boolean;
14
+ /** RFC 9728 discovery shared with the transport. */
15
+ discovery?: Pick<OAuthDiscovery, 'protectedResourceMetadata' | 'protectedResourceMetadataAt'>;
16
+ }
17
+ /**
18
+ * The credential provider the user configured, from user-level sources only.
19
+ * Nothing in a workspace or repository can name a helper: that would let a
20
+ * cloned repository choose a command the CLI runs.
21
+ *
22
+ * - A credential helper's command line, from `KANONAK_CREDENTIAL_HELPER`, or
23
+ * else `credentialHelper` in `~/.kanonak/config.json` (the file configures an
24
+ * environment without setting any variable in it). It becomes an
25
+ * {@link ExecCredentialProvider}.
26
+ * - Otherwise the {@link TokenCredentialProvider}, reading `KANONAK_TOKEN` and
27
+ * `KANONAK_TOKEN_<AUTHORITY>`.
28
+ *
29
+ * A helper together with any `KANONAK_TOKEN*` variable is an error that names
30
+ * both: two credential mechanisms with no rule between them is ambiguous.
31
+ */
32
+ export declare function credentialProviderFromConfig(opts?: CredentialProviderConfigOptions): CredentialProvider;
@@ -1,29 +1,40 @@
1
- import type { AuthenticatedFetchFn, AuthChallenge, CredentialProvider } from './CredentialProvider.js';
2
- /**
3
- * Parse a server's authorization challenge from a 401/403 response. Returns
4
- * undefined when the response carries no challenge (a plain unauthorized).
5
- */
6
- export declare function parseAuthChallenge(res: Response): AuthChallenge | undefined;
1
+ import { type AuthenticatedFetchFn, type CredentialProvider } from './CredentialProvider.js';
2
+ import { OAuthDiscovery } from './OAuthDiscovery.js';
3
+ /** A cached credential is not attached within this many seconds of its `expiresAt`. */
4
+ export declare const CREDENTIAL_EXPIRY_SKEW_SECONDS = 30;
7
5
  export interface CredentialedFetchDeps {
8
- discovery?: {
9
- discoverProtectedResource(host: string): Promise<string[] | null>;
10
- };
6
+ /** RFC 9728 discovery, used to learn that an origin is gated before asking it
7
+ * anything (see {@link createCredentialedFetch}). */
8
+ discovery?: Pick<OAuthDiscovery, 'protectedResourceMetadata'>;
9
+ /** The clock, in Unix seconds. */
10
+ nowSeconds?: () => number;
11
11
  }
12
12
  /**
13
- * The protocol's consumption transport. Given a {@link CredentialProvider}, it:
13
+ * The protocol's consumption transport (#126). Given a {@link CredentialProvider}:
14
14
  *
15
- * 1. Discovers whether a host is gated and, if so, its authority (RFC 9728).
16
- * 2. Asks the provider for a credential for that authority and attaches the
17
- * returned headers. A public host, or a provider that declines, → anonymous.
18
- * 3. Never blind-follows a gated redirect (`redirect: 'manual'`), which would
19
- * otherwise loop into a login.
20
- * 4. On a 401/403 to an authenticated request, parses the server challenge
21
- * and re-resolves: with a `challenge` the provider binds a nonce or steps
22
- * up; with no challenge the credential is treated as stale and re-minted
23
- * (`forceRefresh`). Retries once, then surfaces the failure.
15
+ * 1. A credential cached for a scope that contains the URL (the narrowest one,
16
+ * not within {@link CREDENTIAL_EXPIRY_SKEW_SECONDS} of expiring) is attached.
17
+ * Headers are attached only inside their scope, so a redirect to another
18
+ * origin, or a lock entry naming another host, never carries them.
19
+ * 2. With none cached, an origin known to be gated is asked pre-emptively, with
20
+ * no challenge. An origin is known to be gated once it has challenged in
21
+ * this process, or when it advertises RFC 9728 metadata (or gates even
22
+ * that). Discovery is what reaches a server that answers anonymous clients
23
+ * with 404 rather than a challenge.
24
+ * 3. A 401 is answered with the server's challenges: the rejected credential,
25
+ * if one was attached, is erased, and the provider is asked once more. A 403
26
+ * whose challenge says `insufficient_scope` is asked once more without an
27
+ * erase, since the credential still serves the rest of its scope. Any other
28
+ * 403 is a refusal, returned to the caller to report.
29
+ * 4. A second rejection, a decline when the server requires a credential, a
30
+ * provider failure, an answer outside the protocol, or a gated resource with
31
+ * no provider configured throws a {@link CredentialError}. Nothing falls back
32
+ * to anonymous.
24
33
  *
25
- * Without a provider it runs fully anonymous (public packages still resolve).
26
- * Discovery results and credentials are cached per host/authority so one
27
- * install/connect doesn't re-discover or re-mint for every package in a closure.
34
+ * Redirects are never followed blindly (`redirect: 'manual'`); the caller sees
35
+ * the 3xx and fetches the new location through this transport again. Provider
36
+ * calls are serialized per origin, and each re-checks the cache first, so a
37
+ * closure fetched concurrently asks once per scope rather than once per URL.
38
+ * Without a provider every request is anonymous, and a gated one fails loudly.
28
39
  */
29
40
  export declare function createCredentialedFetch(provider?: CredentialProvider, deps?: CredentialedFetchDeps): AuthenticatedFetchFn;
@@ -1,8 +1,14 @@
1
1
  export { normalizeHost } from './hosts.js';
2
2
  export { OAuthDiscovery, DEFAULT_CONSENT_BINDING_PARAM } from './OAuthDiscovery.js';
3
- export type { OAuthServerMetadata } from './OAuthDiscovery.js';
4
- export type { AuthenticatedFetchFn, CredentialProvider, RequestCredential, AuthChallenge, ResolveOptions, } from './CredentialProvider.js';
5
- export { createCredentialedFetch, parseAuthChallenge } from './credentialedFetch.js';
3
+ export type { OAuthServerMetadata, ProtectedResourceMetadata } from './OAuthDiscovery.js';
4
+ export { CredentialError } from './CredentialProvider.js';
5
+ export type { AuthenticatedFetchFn, AuthChallenge, Credential, CredentialAnswer, CredentialEraseRequest, CredentialProvider, CredentialRequest, } from './CredentialProvider.js';
6
+ export { createCredentialedFetch, CREDENTIAL_EXPIRY_SKEW_SECONDS } from './credentialedFetch.js';
6
7
  export type { CredentialedFetchDeps } from './credentialedFetch.js';
7
- export { TokenCredentialProvider } from './TokenCredentialProvider.js';
8
+ export { parseWwwAuthenticate } from './challenges.js';
9
+ export { parseScope, scopeContains, narrowestScope } from './scope.js';
10
+ export type { ParsedScope } from './scope.js';
11
+ export { CREDENTIAL_HELPER_PROTOCOL_VERSION, checkCredentialAnswer, checkHelperAnswer, checkHelperEraseAnswer, } from './credentialAnswer.js';
12
+ export type { AnswerCheck, AnswerProblem } from './credentialAnswer.js';
13
+ export { TokenCredentialProvider, TOKEN_ENV, tokenEnvKeyFor } from './TokenCredentialProvider.js';
8
14
  export type { TokenCredentialProviderOptions } from './TokenCredentialProvider.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Credential scopes (#126). A scope is an `https` origin plus a path prefix,
3
+ * e.g. `https://registry.example.net/example.com` or `https://example.com/`. A
4
+ * credential's headers are attached only to URLs inside its scope, so a
5
+ * redirect to another origin, or a lock entry naming another host, never
6
+ * carries them. The prefix matches whole path segments: `/example.com` covers
7
+ * `/example.com` and `/example.com/core/1.0.0.kan.yml`, never
8
+ * `/example.com.evil/…`.
9
+ */
10
+ export interface ParsedScope {
11
+ /** `https://host[:port]` */
12
+ origin: string;
13
+ /** The path prefix, normalized to end in `/`. */
14
+ prefix: string;
15
+ }
16
+ /**
17
+ * Parse and check a scope. Throws when it is not an absolute `https` URL, or
18
+ * carries credentials, a query or a fragment: none of those can bound a path
19
+ * prefix.
20
+ */
21
+ export declare function parseScope(scope: string): ParsedScope;
22
+ /**
23
+ * Whether `url` lies inside `scope`: same origin, and its path is the scope's
24
+ * path or continues it at a segment boundary. A non-https URL is never inside.
25
+ */
26
+ export declare function scopeContains(scope: string | ParsedScope, url: string): boolean;
27
+ /** Of the scopes that contain `url`, the most specific (longest prefix), or undefined. */
28
+ export declare function narrowestScope<T extends {
29
+ scope: string;
30
+ }>(entries: Iterable<T>, url: string): T | undefined;
package/dist/browser.d.ts CHANGED
@@ -34,9 +34,11 @@ export type { CanonicalInput, CanonicalInputSubject, CanonicalInputStatement, Ca
34
34
  export { buildOntologyModel } from './introspection/index.js';
35
35
  export type { OntologyModel, ClassDef, PropertyDef, ClassRef, TypeRef, BuildOntologyModelOptions, } from './introspection/index.js';
36
36
  export { normalizeHost } from './auth/hosts.js';
37
- export { OAuthDiscovery, createCredentialedFetch, parseAuthChallenge, TokenCredentialProvider, } from './auth/index.js';
38
- export type { AuthenticatedFetchFn, CredentialProvider, RequestCredential, AuthChallenge, ResolveOptions, } from './auth/index.js';
37
+ export { OAuthDiscovery, CredentialError, createCredentialedFetch, CREDENTIAL_EXPIRY_SKEW_SECONDS, parseWwwAuthenticate, parseScope, scopeContains, narrowestScope, CREDENTIAL_HELPER_PROTOCOL_VERSION, checkCredentialAnswer, checkHelperAnswer, checkHelperEraseAnswer, TokenCredentialProvider, TOKEN_ENV, tokenEnvKeyFor, } from './auth/index.js';
38
+ export type { AuthenticatedFetchFn, AuthChallenge, Credential, CredentialAnswer, CredentialEraseRequest, CredentialProvider, CredentialRequest, CredentialedFetchDeps, ParsedScope, AnswerCheck, AnswerProblem, ProtectedResourceMetadata, TokenCredentialProviderOptions, } from './auth/index.js';
39
39
  export { kanonakFetch, setKanonakUserAgent, getKanonakUserAgent, KANONAK_USER_AGENT, } from './http/kanonakFetch.js';
40
+ export { KanonakHttpError, httpError, describeResponse, retryAfterSecondsOf } from './http/HttpError.js';
41
+ export type { ProblemDetails } from './http/HttpError.js';
40
42
  export { parseLockFile, serializeLockFile, computeIntegrityAsync, INTEGRITY_PREFIX, } from './lock/LockFile.js';
41
43
  export type { LockFile, LockEntry } from './lock/LockFile.js';
42
44
  export { PackageBuilder, ImportBook, contentAddressedName, sanitizeName, ProducerRepository, EntitlementProducer, EntitlementDeniedError, allowAllPolicy, noopMeter, } from './producer/index.js';
package/dist/browser.js CHANGED
@@ -1 +1 @@
1
- import{a as $,b as ee,c as oe,d as Ue,f as Be,g as we,h as Ge,i as qe}from"./chunk-LBVHWFFL.js";import{a as ze}from"./chunk-ED35OFGD.js";import{a as ve,b as He,c as Te,d as Le,e as Me,f as _e,g as Fe,h as We,i as Ye,j as Xe,k as Je,l as Qe}from"./chunk-ADSDFJUO.js";import{a as c,b as m,c as u,e as d,h as f,j as y,k as R,l as k,m as x,n as P,o as g}from"./chunk-OOYGILH4.js";import{a as re,b as te,c as ne}from"./chunk-GKIX3OD6.js";import{a as ae}from"./chunk-D2X6J77P.js";import"./chunk-SC5M74NM.js";import{A as be,B as Ae,C as Oe,M as Ne,a as ie,b as se,c as pe,d as le,g as ce,h as me,i as ue,j as de,l as fe,m as ye,n as Re,o as ke,p as xe,q as Pe,r as ge,s as Se,t as Ce,u as Ee,v as Ie,w as Ke,x as he,y as je,z as De}from"./chunk-O6SOU5Y2.js";import"./chunk-VORVR5BZ.js";import{m as Ve}from"./chunk-TYVZXABI.js";import"./chunk-QHABFCRC.js";import{a as p}from"./chunk-6VIUXP4N.js";import{a as X,g as J,h as Q,k as Z}from"./chunk-22DSABP6.js";import{a as l}from"./chunk-7JBKOJB4.js";import"./chunk-PEJALHXK.js";import{a as T,b as L,h as F,i as U,j as B,k as w,ka as q,l as G,ma as z}from"./chunk-NUTHBBDZ.js";import{I as W,J as Y,a as S,b as C,c as E,d as I,e as h,g as j,h as D,i as b,j as A,k as O,l as V,m as N,n as v,o as H,u as M,v as _}from"./chunk-D4QGPMRW.js";import{a as K}from"./chunk-FUUTGGJS.js";import"./chunk-F67UH4FD.js";import{c as e,d as o,e as r,f as t,g as n,h as a,i,k as s}from"./chunk-QILEJIFU.js";export{Ie as AmbiguousReferenceRule,O as BooleanStatement,Te as CANONICAL_FORM_VERSION,J as Carrier,Ae as ClassDefinitionRule,ke as ClassHierarchyCycleRule,C as DefinedKanonak,ee as EdgeType,I as EmbeddedKanonak,fe as EmbeddedKanonakTypeRule,N as EmbeddedStatement,Je as EntitlementDeniedError,Qe as EntitlementProducer,oe as GraphBuilder,g as HttpKanonakDocumentRepository,y as INTEGRITY_PREFIX,_e as ImportBook,ye as ImportExistenceRule,p as InMemoryKanonakDocumentRepository,c as KANONAK_USER_AGENT,S as Kanonak,te as KanonakDocumentPositions,Z as KanonakObjectParser,Ne as KanonakObjectValidator,l as KanonakParser,K as KanonakUri,ae as KanonakUriBuilder,q as KanonakVocabulary,v as ListStatement,Oe as MarkdownLinkRule,H as MarkdownStatement,Ce as NamespaceImportCycleRule,ce as NamespacePrefixRule,$ as NodeType,A as NumberStatement,Be as OAuthDiscovery,he as ObjectPropertyValueValidationRule,pe as OntologyValidationError,ie as OntologyValidationResult,Le as PackageBuilder,We as ProducerRepository,je as PropertyDomainRule,xe as PropertyHierarchyCycleRule,De as PropertyKindRangeConsistencyRule,re as PropertyMetadata,Ke as PropertyRangeReferenceRule,Pe as PropertyRangeRequiredRule,ue as PropertyTypeSpecificityRule,f as PublisherConfigResolver,P as PublisherIndex,h as ReferenceKanonak,V as ReferenceStatement,be as ReservedNameShadowRule,me as ResourceNamingRule,T as ResourceResolver,z as ResourceTypeClassifier,U as SHACL_PACKAGE,F as SHACL_PUBLISHER,w as SH_NODE_SHAPE,G as SH_PROPERTY_SHAPE,B as SH_SHAPE,D as ScalarStatement,j as Statement,b as StringStatement,ge as SubClassOfReferenceRule,Se as SubPropertyOfReferenceRule,E as SubjectKanonak,de as SubjectKanonakTypeRequiredRule,qe as TokenCredentialProvider,L as TypeResolver,Ee as UnresolvedPredicateRule,Re as UnresolvedReferenceRule,le as ValidationContext,se as ValidationSeverity,Ye as allowAllPolicy,Ve as buildOntologyModel,ve as canonicalForm,He as canonicalHash,Q as carrierOf,e as compareVersions,x as computeIntegrityAsync,Me as contentAddressedName,Ge as createCredentialedFetch,t as createVersion,ze as findDerivation,r as formatVersion,u as getKanonakUserAgent,a as isCompatibleVersion,i as isMajorCompatible,d as kanonakFetch,Xe as noopMeter,Ue as normalizeHost,we as parseAuthChallenge,R as parseLockFile,n as parseVersionString,ne as parseWithPositions,s as pickHighestDocument,_ as referenceUris,Fe as sanitizeName,k as serializeLockFile,m as setKanonakUserAgent,Y as subjectAddress,M as typeReferenceUris,W as valuesOf,o as versionsEqual,X as walkDefinedKanonaks};
1
+ import{a as ne,b as ae,c as ie,d as We,e as ze,f as Ye,g as Xe,i as Je,j as Qe,k as Ze,l as $e,m as eo,n as oo,o as ro,p as to,q as no,r as ao,s as io}from"./chunk-PNBTIGX7.js";import{a as so}from"./chunk-ED35OFGD.js";import{a as we,b as Me,c as Fe,d as Ue,e as Be,f as Ge,g as qe,h as po,i as lo,j as co,k as mo,l as uo}from"./chunk-ADSDFJUO.js";import{a as c,b as m,c as d,d as u,e as R,f,g as y,h as k,j as x,m as P,o as E,p as C,q as S,r as g,s as I,t as K}from"./chunk-HKQTB65D.js";import{a as se,b as pe,c as le}from"./chunk-GKIX3OD6.js";import{a as ce}from"./chunk-D2X6J77P.js";import"./chunk-SC5M74NM.js";import{A as He,B as Te,C as ve,M as Le,a as me,b as de,c as ue,d as Re,g as fe,h as ye,i as ke,j as xe,l as Pe,m as Ee,n as Ce,o as Se,p as ge,q as Ie,r as Ke,s as he,t as Ae,u as Oe,v as De,w as be,x as je,y as Ne,z as Ve}from"./chunk-O6SOU5Y2.js";import"./chunk-VORVR5BZ.js";import{m as _e}from"./chunk-TYVZXABI.js";import"./chunk-QHABFCRC.js";import{a as p}from"./chunk-6VIUXP4N.js";import{a as ee,g as oe,h as re,k as te}from"./chunk-22DSABP6.js";import{a as l}from"./chunk-7JBKOJB4.js";import"./chunk-PEJALHXK.js";import{a as F,b as U,h as q,i as W,j as z,k as Y,ka as J,l as X,ma as Q}from"./chunk-NUTHBBDZ.js";import{I as Z,J as $,a as h,b as A,c as O,d as D,e as j,g as N,h as V,i as H,j as T,k as v,l as _,m as L,n as w,o as M,u as B,v as G}from"./chunk-D4QGPMRW.js";import{a as b}from"./chunk-FUUTGGJS.js";import"./chunk-F67UH4FD.js";import{c as e,d as o,e as r,f as t,g as n,h as a,i,k as s}from"./chunk-QILEJIFU.js";export{De as AmbiguousReferenceRule,v as BooleanStatement,Fe as CANONICAL_FORM_VERSION,ro as CREDENTIAL_EXPIRY_SKEW_SECONDS,Qe as CREDENTIAL_HELPER_PROTOCOL_VERSION,oe as Carrier,Te as ClassDefinitionRule,Se as ClassHierarchyCycleRule,R as CredentialError,A as DefinedKanonak,ae as EdgeType,D as EmbeddedKanonak,Pe as EmbeddedKanonakTypeRule,L as EmbeddedStatement,mo as EntitlementDeniedError,uo as EntitlementProducer,ie as GraphBuilder,K as HttpKanonakDocumentRepository,E as INTEGRITY_PREFIX,Ge as ImportBook,Ee as ImportExistenceRule,p as InMemoryKanonakDocumentRepository,f as KANONAK_USER_AGENT,h as Kanonak,pe as KanonakDocumentPositions,c as KanonakHttpError,te as KanonakObjectParser,Le as KanonakObjectValidator,l as KanonakParser,b as KanonakUri,ce as KanonakUriBuilder,J as KanonakVocabulary,w as ListStatement,ve as MarkdownLinkRule,M as MarkdownStatement,Ae as NamespaceImportCycleRule,fe as NamespacePrefixRule,ne as NodeType,T as NumberStatement,Je as OAuthDiscovery,je as ObjectPropertyValueValidationRule,ue as OntologyValidationError,me as OntologyValidationResult,Ue as PackageBuilder,po as ProducerRepository,Ne as PropertyDomainRule,ge as PropertyHierarchyCycleRule,Ve as PropertyKindRangeConsistencyRule,se as PropertyMetadata,be as PropertyRangeReferenceRule,Ie as PropertyRangeRequiredRule,ke as PropertyTypeSpecificityRule,P as PublisherConfigResolver,I as PublisherIndex,j as ReferenceKanonak,_ as ReferenceStatement,He as ReservedNameShadowRule,ye as ResourceNamingRule,F as ResourceResolver,Q as ResourceTypeClassifier,W as SHACL_PACKAGE,q as SHACL_PUBLISHER,Y as SH_NODE_SHAPE,X as SH_PROPERTY_SHAPE,z as SH_SHAPE,V as ScalarStatement,N as Statement,H as StringStatement,Ke as SubClassOfReferenceRule,he as SubPropertyOfReferenceRule,O as SubjectKanonak,xe as SubjectKanonakTypeRequiredRule,no as TOKEN_ENV,io as TokenCredentialProvider,U as TypeResolver,Oe as UnresolvedPredicateRule,Ce as UnresolvedReferenceRule,Re as ValidationContext,de as ValidationSeverity,lo as allowAllPolicy,_e as buildOntologyModel,we as canonicalForm,Me as canonicalHash,re as carrierOf,Ze as checkCredentialAnswer,$e as checkHelperAnswer,eo as checkHelperEraseAnswer,e as compareVersions,g as computeIntegrityAsync,Be as contentAddressedName,to as createCredentialedFetch,t as createVersion,d as describeResponse,so as findDerivation,r as formatVersion,k as getKanonakUserAgent,m as httpError,a as isCompatibleVersion,i as isMajorCompatible,x as kanonakFetch,Xe as narrowestScope,co as noopMeter,We as normalizeHost,C as parseLockFile,ze as parseScope,n as parseVersionString,le as parseWithPositions,oo as parseWwwAuthenticate,s as pickHighestDocument,G as referenceUris,u as retryAfterSecondsOf,qe as sanitizeName,Ye as scopeContains,S as serializeLockFile,y as setKanonakUserAgent,$ as subjectAddress,ao as tokenEnvKeyFor,B as typeReferenceUris,Z as valuesOf,o as versionsEqual,ee as walkDefinedKanonaks};