@idpartners/client-attestation-sdk 0.2.0 → 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/dist/http.js CHANGED
@@ -12,11 +12,22 @@
12
12
  * into a {@link TransportError}.
13
13
  */
14
14
  import { TransportError } from "./errors.js";
15
+ import { utf8Decode, utf8Encode } from "./runtime.js";
16
+ /** @internal */
15
17
  export const DEFAULT_MAX_BYTES = 1024 * 1024;
18
+ /** @internal */
16
19
  export const DEFAULT_TIMEOUT_MS = 20_000;
17
- /** At most this many characters of a response body are carried in an error. */
20
+ /**
21
+ * At most this many characters of a response body are carried in an error.
22
+ *
23
+ * @internal
24
+ */
18
25
  export const ERROR_BODY_CHARS = 512;
19
- /** The first {@link ERROR_BODY_CHARS} characters of a body, for an error message. */
26
+ /**
27
+ * The first {@link ERROR_BODY_CHARS} characters of a body, for an error message.
28
+ *
29
+ * @internal
30
+ */
20
31
  export function snippet(body) {
21
32
  return body.length > ERROR_BODY_CHARS ? body.slice(0, ERROR_BODY_CHARS) : body;
22
33
  }
@@ -31,6 +42,8 @@ function isAbort(err) {
31
42
  * Make one HTTP request. Resolves with the status, headers and body text for any non-3xx response
32
43
  * (non-2xx included); rejects with a {@link TransportError} for a network failure, timeout, redirect or
33
44
  * oversized body.
45
+ *
46
+ * @internal
34
47
  */
35
48
  export async function request(url, opts = {}) {
36
49
  const method = opts.method ?? "GET";
@@ -98,7 +111,7 @@ async function readCapped(res, maxBytes, tooLarge) {
98
111
  if (!res.body) {
99
112
  // only reached by stand-in Response objects (tests); a real fetch always has a stream
100
113
  const text = await res.text();
101
- if (Buffer.byteLength(text, "utf8") > maxBytes)
114
+ if (utf8Encode(text).byteLength > maxBytes)
102
115
  throw tooLarge();
103
116
  return text;
104
117
  }
@@ -116,12 +129,20 @@ async function readCapped(res, maxBytes, tooLarge) {
116
129
  }
117
130
  chunks.push(value);
118
131
  }
119
- return Buffer.concat(chunks).toString("utf8");
132
+ const all = new Uint8Array(total);
133
+ let offset = 0;
134
+ for (const chunk of chunks) {
135
+ all.set(chunk, offset);
136
+ offset += chunk.byteLength;
137
+ }
138
+ return utf8Decode(all);
120
139
  }
121
140
  /**
122
141
  * Parse `res.body` as a JSON object. Throws a {@link TransportError} for a non-2xx status (carrying the
123
142
  * status and at most 512 characters of the body) and for a body that is not a JSON object. An empty body
124
143
  * reads as `{}` when `allowEmpty` is set.
144
+ *
145
+ * @internal
125
146
  */
126
147
  export function jsonOk(res, opts = {}) {
127
148
  if (!res.ok) {
@@ -133,7 +154,11 @@ export function jsonOk(res, opts = {}) {
133
154
  }
134
155
  return parseJsonObject(res, opts);
135
156
  }
136
- /** Parse `res.body` as a JSON object regardless of status; a {@link TransportError} if it is not one. */
157
+ /**
158
+ * Parse `res.body` as a JSON object regardless of status; a {@link TransportError} if it is not one.
159
+ *
160
+ * @internal
161
+ */
137
162
  export function parseJsonObject(res, opts = {}) {
138
163
  if (opts.allowEmpty && res.body === "")
139
164
  return {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Client-side builder SDK for OAuth Attestation-Based Client Authentication.
3
+ *
4
+ * Everything exported here is the package's API; docs/api.md says which parts are Stable and which are
5
+ * Experimental. Nothing else under dist/ can be imported: package.json "exports" allows only this entry
6
+ * point and "./validator".
3
7
  */
4
8
  export { SigningKeyPair } from "./signing-key-pair.js";
5
9
  export { ClientAttestationBuilder, DpopProofBuilder, PopBuilder, ATTESTATION_TYP, POP_TYP, DPOP_TYP, } from "./builders.js";
@@ -10,9 +14,13 @@ export { AttestationIssuanceClient, IssuedAttestation, IssuanceError, INSTANCE_P
10
14
  export { StaticSvidSource, HttpSvidSource, SpireWorkloadApiSvidSource, } from "./svid-source.js";
11
15
  export type { SvidSource } from "./svid-source.js";
12
16
  export { EvidenceNotFound, FileTokenSvidSource, GoogleMetadataSvidSource, autoDetectEvidence, ENV_SVID, ENV_TOKEN_FILE, ENV_SPIFFE_SOCKET, DEFAULT_TOKEN_FILE, DEFAULT_SPIFFE_SOCKET, } from "./evidence.js";
13
- export { ClientAttestation, ClientAttestationError, TransportError, normalizeBaseUrl, jwtSub, DEFAULT_EXPIRY_SKEW_SECONDS, WELL_KNOWN_PATH, } from "./token-source.js";
14
- export type { ClientAttestationOptions } from "./token-source.js";
17
+ export { ClientAttestation, ClientAttestationError, TransportError, DEFAULT_EXPIRY_SKEW_SECONDS, } from "./token-source.js";
18
+ export type { AttesterDiscovery, ClientAttestationOptions } from "./token-source.js";
19
+ export { MintRequest, PingFederateProfile } from "./attester.js";
20
+ export type { AttesterMetadata, AttesterProfile, AttesterTransport, MintResult } from "./attester.js";
15
21
  export type { TransportFailure } from "./errors.js";
16
- export declare const VERSION = "0.2.0";
22
+ export { NOOP_LOGGER } from "./logging.js";
23
+ export type { Logger, LogAttributes, LogLevel } from "./logging.js";
24
+ export declare const VERSION = "1.0.0";
17
25
  /** draft-ietf-oauth-attestation-based-client-auth revision this SDK implements. */
18
26
  export declare const DRAFT_REVISION = "10";
package/dist/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Client-side builder SDK for OAuth Attestation-Based Client Authentication.
3
+ *
4
+ * Everything exported here is the package's API; docs/api.md says which parts are Stable and which are
5
+ * Experimental. Nothing else under dist/ can be imported: package.json "exports" allows only this entry
6
+ * point and "./validator".
3
7
  */
4
8
  export { SigningKeyPair } from "./signing-key-pair.js";
5
9
  export { ClientAttestationBuilder, DpopProofBuilder, PopBuilder, ATTESTATION_TYP, POP_TYP, DPOP_TYP, } from "./builders.js";
@@ -8,7 +12,9 @@ export { OpenBaoTransitSigner } from "./signer.js";
8
12
  export { AttestationIssuanceClient, IssuedAttestation, IssuanceError, INSTANCE_PROOF_TYP, } from "./issuance.js";
9
13
  export { StaticSvidSource, HttpSvidSource, SpireWorkloadApiSvidSource, } from "./svid-source.js";
10
14
  export { EvidenceNotFound, FileTokenSvidSource, GoogleMetadataSvidSource, autoDetectEvidence, ENV_SVID, ENV_TOKEN_FILE, ENV_SPIFFE_SOCKET, DEFAULT_TOKEN_FILE, DEFAULT_SPIFFE_SOCKET, } from "./evidence.js";
11
- export { ClientAttestation, ClientAttestationError, TransportError, normalizeBaseUrl, jwtSub, DEFAULT_EXPIRY_SKEW_SECONDS, WELL_KNOWN_PATH, } from "./token-source.js";
12
- export const VERSION = "0.2.0";
15
+ export { ClientAttestation, ClientAttestationError, TransportError, DEFAULT_EXPIRY_SKEW_SECONDS, } from "./token-source.js";
16
+ export { MintRequest, PingFederateProfile } from "./attester.js";
17
+ export { NOOP_LOGGER } from "./logging.js";
18
+ export const VERSION = "1.0.0";
13
19
  /** draft-ietf-oauth-attestation-based-client-auth revision this SDK implements. */
14
20
  export const DRAFT_REVISION = "10";
@@ -1,7 +1,16 @@
1
+ /**
2
+ * Client for the Client Attestation **issuance** endpoint (`POST /federation/attestation`).
3
+ *
4
+ * The inverse of {@link ClientAttestationBuilder}: instead of building an attestation locally with an
5
+ * attester key, a workload proves its SPIFFE identity (a JWT-SVID) and the Authorization Server
6
+ * (PingFederate) validates it and **mints** a Client Attestation bound to the workload's own instance
7
+ * key — bootstrapping a client credential from infra identity, no pre-shared secret.
8
+ */
9
+ import { type AttesterProfile } from "./attester.js";
1
10
  import { SigningKeyPair } from "./signing-key-pair.js";
2
11
  import type { SvidSource } from "./svid-source.js";
3
- /** The instance-key proof typ presented at the issuance endpoint (server InstanceKeyProofValidator.TYP). */
4
- export declare const INSTANCE_PROOF_TYP = "oauth-attestation-instance-proof+jwt";
12
+ /** The instance-key proof typ the PingFederate profile presents (server InstanceKeyProofValidator.TYP). */
13
+ export { INSTANCE_PROOF_TYP } from "./attester.js";
5
14
  /**
6
15
  * Thrown when the issuance endpoint rejects the request (non-2xx). Transport failures — unreachable,
7
16
  * timeout, redirect, oversized or malformed response — are a {@link TransportError} instead.
@@ -25,17 +34,27 @@ export declare class IssuedAttestation {
25
34
  */
26
35
  dpop(method: string, uri: string, challenge?: string): Promise<string>;
27
36
  }
28
- /** Bootstraps a Client Attestation for a workload from its SPIFFE JWT-SVID. */
37
+ /**
38
+ * Bootstraps a Client Attestation for a workload from its SPIFFE JWT-SVID.
39
+ *
40
+ * `opts.profile` sets the mint protocol (default {@link PingFederateProfile}; see
41
+ * docs/attester-profiles.md). The endpoint is given directly, so the profile's discovery is not used.
42
+ *
43
+ * @experimental See docs/api.md: it calls a PingFederate demo issuance servlet (/federation/attestation)
44
+ * whose protocol is not standardised.
45
+ */
29
46
  export declare class AttestationIssuanceClient {
30
47
  readonly endpoint: string;
31
48
  readonly clientId: string;
32
49
  readonly attesterIssuer: string;
33
50
  private readonly source;
34
51
  readonly instanceKey: SigningKeyPair;
52
+ readonly profile: AttesterProfile;
35
53
  private constructor();
36
54
  /** Generate a fresh ES256 instance key and return a ready client. */
37
55
  static create(endpoint: string, clientId: string, attesterIssuer: string, source: SvidSource, opts?: {
38
56
  instanceKey?: SigningKeyPair;
57
+ profile?: AttesterProfile;
39
58
  }): Promise<AttestationIssuanceClient>;
40
59
  /** Fetch the SVID, prove possession of the instance key, and exchange them for a minted attestation. */
41
60
  bootstrap(authorizationDetails?: Array<Record<string, unknown>>, challenge?: string): Promise<IssuedAttestation>;
package/dist/issuance.js CHANGED
@@ -6,12 +6,12 @@
6
6
  * (PingFederate) validates it and **mints** a Client Attestation bound to the workload's own instance
7
7
  * key — bootstrapping a client credential from infra identity, no pre-shared secret.
8
8
  */
9
- import { randomUUID } from "node:crypto";
9
+ import { MintRequest, PingFederateProfile } from "./attester.js";
10
10
  import { DpopProofBuilder, PopBuilder } from "./builders.js";
11
11
  import { jsonOk, request } from "./http.js";
12
- import { SigningKeyPair, signCompact } from "./signing-key-pair.js";
13
- /** The instance-key proof typ presented at the issuance endpoint (server InstanceKeyProofValidator.TYP). */
14
- export const INSTANCE_PROOF_TYP = "oauth-attestation-instance-proof+jwt";
12
+ import { SigningKeyPair } from "./signing-key-pair.js";
13
+ /** The instance-key proof typ the PingFederate profile presents (server InstanceKeyProofValidator.TYP). */
14
+ export { INSTANCE_PROOF_TYP } from "./attester.js";
15
15
  /** Per-call timeout for the issuance endpoint. */
16
16
  const ISSUANCE_TIMEOUT_MS = 20_000;
17
17
  /**
@@ -58,53 +58,60 @@ export class IssuedAttestation {
58
58
  return builder.build();
59
59
  }
60
60
  }
61
- /** Bootstraps a Client Attestation for a workload from its SPIFFE JWT-SVID. */
61
+ /**
62
+ * Bootstraps a Client Attestation for a workload from its SPIFFE JWT-SVID.
63
+ *
64
+ * `opts.profile` sets the mint protocol (default {@link PingFederateProfile}; see
65
+ * docs/attester-profiles.md). The endpoint is given directly, so the profile's discovery is not used.
66
+ *
67
+ * @experimental See docs/api.md: it calls a PingFederate demo issuance servlet (/federation/attestation)
68
+ * whose protocol is not standardised.
69
+ */
62
70
  export class AttestationIssuanceClient {
63
71
  endpoint;
64
72
  clientId;
65
73
  attesterIssuer;
66
74
  source;
67
75
  instanceKey;
68
- constructor(endpoint, clientId, attesterIssuer, source, instanceKey) {
76
+ profile;
77
+ constructor(endpoint, clientId, attesterIssuer, source, instanceKey, profile) {
69
78
  this.endpoint = endpoint;
70
79
  this.clientId = clientId;
71
80
  this.attesterIssuer = attesterIssuer;
72
81
  this.source = source;
73
82
  this.instanceKey = instanceKey;
83
+ this.profile = profile;
74
84
  }
75
85
  /** Generate a fresh ES256 instance key and return a ready client. */
76
86
  static async create(endpoint, clientId, attesterIssuer, source, opts) {
77
87
  const key = opts?.instanceKey ?? (await SigningKeyPair.generate("ES256"));
78
- return new AttestationIssuanceClient(endpoint.replace(/\/$/, ""), clientId, attesterIssuer, source, key);
88
+ const profile = opts?.profile ?? new PingFederateProfile();
89
+ return new AttestationIssuanceClient(endpoint.replace(/\/$/, ""), clientId, attesterIssuer, source, key, profile);
79
90
  }
80
91
  /** Fetch the SVID, prove possession of the instance key, and exchange them for a minted attestation. */
81
92
  async bootstrap(authorizationDetails, challenge) {
82
93
  const svid = await this.source.fetch(this.attesterIssuer);
83
- const proofClaims = {
84
- aud: this.attesterIssuer,
85
- jti: randomUUID(),
86
- iat: Math.floor(Date.now() / 1000),
87
- };
88
- if (challenge)
89
- proofClaims.challenge = challenge;
90
- const proof = await signCompact(proofClaims, this.instanceKey, INSTANCE_PROOF_TYP, false);
91
- const body = {
92
- client_id: this.clientId,
93
- instance_key: this.instanceKey.publicJwk(),
94
- svid,
95
- proof,
96
- };
97
- if (authorizationDetails)
98
- body.authorization_details = authorizationDetails;
99
- const res = await request(this.endpoint, {
100
- method: "POST",
94
+ const minted = await this.profile.mint(issuanceTransport, { evidenceAudience: this.attesterIssuer, attestationEndpoint: this.endpoint }, new MintRequest({
95
+ instanceKey: this.instanceKey,
96
+ evidence: svid,
97
+ challenge: challenge || undefined,
98
+ clientId: this.clientId,
99
+ authorizationDetails,
100
+ }));
101
+ return new IssuedAttestation(minted.attestation, minted.expiresIn, this.instanceKey);
102
+ }
103
+ }
104
+ /** The issuance endpoint's transport: a non-2xx answer is an {@link IssuanceError}. */
105
+ const issuanceTransport = {
106
+ async requestJson(_step, method, url, body) {
107
+ const res = await request(url, {
108
+ method,
101
109
  headers: { "Content-Type": "application/json", Accept: "application/json" },
102
- body: JSON.stringify(body),
110
+ body: body === undefined ? undefined : JSON.stringify(body),
103
111
  timeoutMs: ISSUANCE_TIMEOUT_MS,
104
112
  });
105
113
  if (!res.ok)
106
114
  throw new IssuanceError(res.status, res.body);
107
- const json = jsonOk(res);
108
- return new IssuedAttestation(json.attestation, json.expires_in, this.instanceKey);
109
- }
110
- }
115
+ return jsonOk(res);
116
+ },
117
+ };
@@ -0,0 +1,12 @@
1
+ /** Attributes carried with an event. */
2
+ export type LogAttributes = Record<string, unknown>;
3
+ /** Anything with these four methods: `console` fits, as do pino, winston and bunyan wrappers. */
4
+ export interface Logger {
5
+ debug(msg: string, attrs?: LogAttributes): void;
6
+ info(msg: string, attrs?: LogAttributes): void;
7
+ warn(msg: string, attrs?: LogAttributes): void;
8
+ error(msg: string, attrs?: LogAttributes): void;
9
+ }
10
+ export type LogLevel = "debug" | "info" | "warn" | "error";
11
+ /** The default: drops everything. */
12
+ export declare const NOOP_LOGGER: Logger;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Optional logging. The SDK is silent unless a {@link Logger} is passed in (`logger` on
3
+ * `ClientAttestation` and on `AccessTokenValidator`). Event names and attribute keys are shared with the
4
+ * other ports; see docs/logging.md.
5
+ *
6
+ * Never logged: tokens, attestations, proofs, SVIDs, challenge values, key material, header values, or
7
+ * URLs with their query string. As a backstop, any string attribute containing `eyJ` (how a JWT's
8
+ * base64url-encoded header begins) is replaced with `<redacted:N bytes>` before it reaches the logger.
9
+ */
10
+ import { TransportError } from "./errors.js";
11
+ import { utf8Encode } from "./runtime.js";
12
+ const noop = () => undefined;
13
+ /** The default: drops everything. */
14
+ export const NOOP_LOGGER = { debug: noop, info: noop, warn: noop, error: noop };
15
+ function guard(value) {
16
+ if (typeof value !== "string")
17
+ return value;
18
+ // A JWT's header and claims (any base64url-encoded JSON object) begin with "eyJ".
19
+ if (value.includes("eyJ")) {
20
+ return `<redacted:${utf8Encode(value).length} bytes>`;
21
+ }
22
+ return value;
23
+ }
24
+ /**
25
+ * Emit one event: undefined attributes dropped, JWT-looking values redacted. Never throws.
26
+ *
27
+ * @internal
28
+ */
29
+ export function emit(logger, level, event, attrs = {}) {
30
+ if (logger === NOOP_LOGGER)
31
+ return;
32
+ const clean = {};
33
+ for (const [key, value] of Object.entries(attrs)) {
34
+ if (value !== undefined && value !== null)
35
+ clean[key] = guard(value);
36
+ }
37
+ try {
38
+ logger[level](event, clean);
39
+ }
40
+ catch {
41
+ // a broken logger must never break the flow
42
+ }
43
+ }
44
+ /**
45
+ * `host[:port]` and the path of a URL, never its query string or fragment.
46
+ *
47
+ * @internal
48
+ */
49
+ export function urlParts(url) {
50
+ try {
51
+ const parsed = new URL(url);
52
+ return { host: parsed.host, path: parsed.pathname };
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ /**
59
+ * `host[:port]` of a URL.
60
+ *
61
+ * @internal
62
+ */
63
+ export function hostOf(url) {
64
+ return urlParts(url).host;
65
+ }
66
+ /**
67
+ * The `reason` attribute for a failed outbound call.
68
+ *
69
+ * @internal
70
+ */
71
+ export function failureReason(err) {
72
+ if (err instanceof TransportError)
73
+ return err.kind === "json" ? "malformed" : err.kind;
74
+ if (err instanceof SyntaxError)
75
+ return "malformed";
76
+ return "network";
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Runtime-neutral helpers, so the package loads on edge runtimes (Cloudflare Workers, Vercel Edge,
3
+ * Deno Deploy) as well as Node. Nothing here, or anywhere else in `src`, imports a Node builtin
4
+ * statically: the few features that need one load it lazily through {@link importNodeBuiltin}.
5
+ * Not exported from the package index.
6
+ */
7
+ import { ClientAttestationError } from "./errors.js";
8
+ const textEncoder = new TextEncoder();
9
+ const textDecoder = new TextDecoder();
10
+ /**
11
+ * UTF-8 bytes of a string.
12
+ *
13
+ * @internal
14
+ */
15
+ export function utf8Encode(text) {
16
+ return textEncoder.encode(text);
17
+ }
18
+ /**
19
+ * A string from UTF-8 bytes; malformed sequences become U+FFFD, as `Buffer#toString` does.
20
+ *
21
+ * @internal
22
+ */
23
+ export function utf8Decode(bytes) {
24
+ return textDecoder.decode(bytes);
25
+ }
26
+ /**
27
+ * Standard (padded) base64 of some bytes.
28
+ *
29
+ * @internal
30
+ */
31
+ export function base64Encode(bytes) {
32
+ let binary = "";
33
+ for (let i = 0; i < bytes.length; i += 0x8000) {
34
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
35
+ }
36
+ return btoa(binary);
37
+ }
38
+ /**
39
+ * An environment variable, or `undefined` on a runtime with no `process.env` (most edge runtimes).
40
+ *
41
+ * @internal
42
+ */
43
+ export function envVar(name) {
44
+ const proc = globalThis.process;
45
+ return proc?.env?.[name];
46
+ }
47
+ /**
48
+ * Load a Node builtin (`node:<name>`) on first use. Throws a {@link ClientAttestationError} naming the
49
+ * feature when the runtime has no such module (an edge runtime without Node compatibility).
50
+ *
51
+ * The specifier is built at run time so bundlers for edge targets leave it alone instead of failing to
52
+ * resolve it.
53
+ *
54
+ * @internal
55
+ */
56
+ export async function importNodeBuiltin(name, feature) {
57
+ const specifier = "node:" + name;
58
+ try {
59
+ return (await import(/* @vite-ignore */ /* webpackIgnore: true */ specifier));
60
+ }
61
+ catch (err) {
62
+ throw new ClientAttestationError(`${feature} needs the Node.js module '${specifier}', which this runtime does not provide`, { cause: err });
63
+ }
64
+ }
package/dist/signer.d.ts CHANGED
@@ -13,14 +13,13 @@ export interface JwsSigner {
13
13
  publicJwk(): JWK;
14
14
  sign(signingInput: Uint8Array): Promise<Uint8Array>;
15
15
  }
16
- /** Assemble a compact JWS whose signature is produced by an external signer (header carries `kid`; external
17
- * signers hold issuing keys, referenced by id, never an embedded jwk). */
18
- export declare function signExternal(claims: Record<string, unknown>, signer: JwsSigner, typ: string): Promise<string>;
19
16
  /**
20
17
  * A {@link JwsSigner} backed by an OpenBao / HashiCorp Vault transit engine: the attestation is signed
21
18
  * inside the vault (`POST /v1/transit/sign/<key>` with `marshaling_algorithm=jws`, which returns the
22
19
  * fixed-width `r||s`) and the private key never leaves it. `create` reads the key metadata to pin the
23
20
  * latest version, derive the public JWK and compute its RFC 7638 `kid`. Fail-closed (vault errors throw).
21
+ *
22
+ * @experimental See docs/api.md: a client for a third-party API that authenticates with a static token only; its constructor will change when other auth methods are added.
24
23
  */
25
24
  export declare class OpenBaoTransitSigner implements JwsSigner {
26
25
  #private;
package/dist/signer.js CHANGED
@@ -4,13 +4,16 @@
4
4
  * {@link SigningKeyPair} covers the local-key case; {@link OpenBaoTransitSigner} signs inside an
5
5
  * OpenBao / HashiCorp Vault transit engine so the attester's private key never leaves it.
6
6
  */
7
- import { calculateJwkThumbprint, exportJWK, importSPKI } from "jose";
7
+ import { base64url, calculateJwkThumbprint, exportJWK, importSPKI } from "jose";
8
8
  import { TransportError } from "./errors.js";
9
+ import { base64Encode } from "./runtime.js";
9
10
  import { parseJsonObject, request as httpRequest, snippet } from "./http.js";
10
11
  /** Per-call timeout for the vault. */
11
12
  const OPENBAO_TIMEOUT_MS = 5_000;
12
- const b64url = (bytes) => Buffer.from(bytes).toString("base64url");
13
+ const b64url = (bytes) => base64url.encode(bytes);
13
14
  /** Assemble a compact JWS whose signature is produced by an external signer (header carries `kid`; external
15
+ *
16
+ * @internal
14
17
  * signers hold issuing keys, referenced by id, never an embedded jwk). */
15
18
  export async function signExternal(claims, signer, typ) {
16
19
  const enc = new TextEncoder();
@@ -26,6 +29,8 @@ const ALG_HASH = { ES256: "sha2-256", ES384: "sha2-384", ES512: "sha2-512" };
26
29
  * inside the vault (`POST /v1/transit/sign/<key>` with `marshaling_algorithm=jws`, which returns the
27
30
  * fixed-width `r||s`) and the private key never leaves it. `create` reads the key metadata to pin the
28
31
  * latest version, derive the public JWK and compute its RFC 7638 `kid`. Fail-closed (vault errors throw).
32
+ *
33
+ * @experimental See docs/api.md: a client for a third-party API that authenticates with a static token only; its constructor will change when other auth methods are added.
29
34
  */
30
35
  export class OpenBaoTransitSigner {
31
36
  algorithm;
@@ -67,7 +72,7 @@ export class OpenBaoTransitSigner {
67
72
  }
68
73
  async sign(signingInput) {
69
74
  const body = JSON.stringify({
70
- input: Buffer.from(signingInput).toString("base64"),
75
+ input: base64Encode(signingInput),
71
76
  marshaling_algorithm: "jws",
72
77
  hash_algorithm: this.#hash,
73
78
  key_version: this.#keyVersion,
@@ -78,7 +83,7 @@ export class OpenBaoTransitSigner {
78
83
  throw new Error("transit sign response carried no signature");
79
84
  }
80
85
  const raw = signature.slice(signature.lastIndexOf(":") + 1); // envelope: vault:v<n>:<base64url(r||s)>
81
- return new Uint8Array(Buffer.from(raw, "base64url"));
86
+ return base64url.decode(raw);
82
87
  }
83
88
  }
84
89
  async function request(method, base, token, path, body) {
@@ -19,11 +19,17 @@ export declare class SigningKeyPair {
19
19
  readonly keyId: string;
20
20
  private constructor();
21
21
  /**
22
- * Generate a fresh extractable key pair for the given JWS algorithm: `ES256` / `ES384` / `ES512`
22
+ * Generate a fresh key pair for the given JWS algorithm: `ES256` / `ES384` / `ES512`
23
23
  * (EC P-256 / P-384 / P-521), or `RS256` / `RS384` / `RS512` / `PS256` / `PS384` / `PS512`
24
24
  * (RSA 2048).
25
+ *
26
+ * The private key is not extractable unless `options.extractable` is true: nothing in the SDK needs
27
+ * to export it, so by default it can't leak through `exportJWK`. Pass `{ extractable: true }` only to
28
+ * persist the key yourself (then restore it with {@link SigningKeyPair.fromJwk}).
25
29
  */
26
- static generate(algorithm: string): Promise<SigningKeyPair>;
30
+ static generate(algorithm: string, options?: {
31
+ extractable?: boolean;
32
+ }): Promise<SigningKeyPair>;
27
33
  /**
28
34
  * Wrap an existing JWK (which must contain the private component `d`) for the given algorithm.
29
35
  */
@@ -36,15 +42,3 @@ export declare class SigningKeyPair {
36
42
  */
37
43
  publicJwk(): JWK;
38
44
  }
39
- /** The JWS algorithms {@link SigningKeyPair} can generate and sign with. */
40
- export declare const SUPPORTED_ALGORITHMS: readonly string[];
41
- /** RFC 7638 JWK thumbprint (SHA-256, base64url) over the required members only. */
42
- export declare function thumbprint(publicJwk: JWK): Promise<string>;
43
- /**
44
- * Sign claims into a compact JWS with an explicit `typ` header, keyed by `key`.
45
- *
46
- * When `embedJwk` is set the public key travels in a `jwk` header (as DPoP requires); otherwise
47
- * the key's thumbprint `kid` is set.
48
- */
49
- export declare function signCompact(claims: Record<string, unknown>, key: SigningKeyPair, typ: string, embedJwk: boolean): Promise<string>;
50
- export declare function requireText(value: string | null | undefined, field: string): string;
@@ -26,14 +26,18 @@ export class SigningKeyPair {
26
26
  this.keyId = keyId;
27
27
  }
28
28
  /**
29
- * Generate a fresh extractable key pair for the given JWS algorithm: `ES256` / `ES384` / `ES512`
29
+ * Generate a fresh key pair for the given JWS algorithm: `ES256` / `ES384` / `ES512`
30
30
  * (EC P-256 / P-384 / P-521), or `RS256` / `RS384` / `RS512` / `PS256` / `PS384` / `PS512`
31
31
  * (RSA 2048).
32
+ *
33
+ * The private key is not extractable unless `options.extractable` is true: nothing in the SDK needs
34
+ * to export it, so by default it can't leak through `exportJWK`. Pass `{ extractable: true }` only to
35
+ * persist the key yourself (then restore it with {@link SigningKeyPair.fromJwk}).
32
36
  */
33
- static async generate(algorithm) {
37
+ static async generate(algorithm, options = {}) {
34
38
  checkAlgorithm(algorithm);
35
39
  const { privateKey, publicKey } = await generateKeyPair(algorithm, {
36
- extractable: true,
40
+ extractable: options.extractable ?? false,
37
41
  ...(algorithm.startsWith("ES") ? {} : { modulusLength: RSA_MODULUS_BITS }),
38
42
  });
39
43
  const publicJwk = publicMembers(await exportJWK(publicKey));
@@ -66,7 +70,11 @@ export class SigningKeyPair {
66
70
  return { ...this.#publicJwk };
67
71
  }
68
72
  }
69
- /** The JWS algorithms {@link SigningKeyPair} can generate and sign with. */
73
+ /**
74
+ * The JWS algorithms {@link SigningKeyPair} can generate and sign with.
75
+ *
76
+ * @internal
77
+ */
70
78
  export const SUPPORTED_ALGORITHMS = [
71
79
  "ES256", "ES384", "ES512",
72
80
  "RS256", "RS384", "RS512",
@@ -85,7 +93,11 @@ function publicMembers(jwk) {
85
93
  ? { kty: jwk.kty, n: jwk.n, e: jwk.e }
86
94
  : { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
87
95
  }
88
- /** RFC 7638 JWK thumbprint (SHA-256, base64url) over the required members only. */
96
+ /**
97
+ * RFC 7638 JWK thumbprint (SHA-256, base64url) over the required members only.
98
+ *
99
+ * @internal
100
+ */
89
101
  export async function thumbprint(publicJwk) {
90
102
  return calculateJwkThumbprint(publicJwk, "sha256");
91
103
  }
@@ -94,6 +106,8 @@ export async function thumbprint(publicJwk) {
94
106
  *
95
107
  * When `embedJwk` is set the public key travels in a `jwk` header (as DPoP requires); otherwise
96
108
  * the key's thumbprint `kid` is set.
109
+ *
110
+ * @internal
97
111
  */
98
112
  export async function signCompact(claims, key, typ, embedJwk) {
99
113
  const header = { alg: key.algorithm, typ };
@@ -107,6 +121,7 @@ export async function signCompact(claims, key, typ, embedJwk) {
107
121
  }
108
122
  return new SignJWT(claims).setProtectedHeader(header).sign(key.privateKey);
109
123
  }
124
+ /** @internal */
110
125
  export function requireText(value, field) {
111
126
  if (value == null || String(value).trim() === "") {
112
127
  throw new Error(`${field} is required`);
@@ -8,7 +8,15 @@ export declare class StaticSvidSource implements SvidSource {
8
8
  constructor(svid: string);
9
9
  fetch(_audience: string): Promise<string>;
10
10
  }
11
- /** GET a URL returning `{"svid":"<jwt>"}` — e.g. the demo harness `/dev/svid` endpoint. */
11
+ /**
12
+ * GET a URL returning `{"svid":"<jwt>"}` — e.g. the demo harness `/dev/svid` endpoint.
13
+ *
14
+ * For development and testing only. It trusts whatever the URL returns and the endpoint has no caller
15
+ * authentication, so anyone who can reach it can obtain the workload's SVID. In production use a SPIRE,
16
+ * file or cloud source.
17
+ *
18
+ * @experimental See docs/api.md: a development and test source with no caller authentication.
19
+ */
12
20
  export declare class HttpSvidSource implements SvidSource {
13
21
  private readonly url;
14
22
  constructor(url: string);
@@ -18,6 +26,12 @@ export declare class HttpSvidSource implements SvidSource {
18
26
  * The default source: fetch a JWT-SVID from a real SPIRE agent's Workload API via the `spire-agent`
19
27
  * CLI. Swap for a native Workload API client by implementing {@link SvidSource} if you prefer no
20
28
  * subprocess.
29
+ *
30
+ * Needs Node.js (it spawns a process): `node:child_process` is loaded on the first `fetch`, which throws
31
+ * a `ClientAttestationError` on a runtime without it.
32
+ *
33
+ * @experimental See docs/api.md: it shells out to the spire-agent CLI, and its constructor will change
34
+ * when it moves to the Workload API socket.
21
35
  */
22
36
  export declare class SpireWorkloadApiSvidSource implements SvidSource {
23
37
  private readonly socketPath;