@opendatalabs/service-auth 1.0.0-next.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.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # `@opendatalabs/service-auth`
2
+
3
+ OAuth 2.0 service-to-service authentication helper built around the
4
+ `private_key_jwt` client authentication method. A caller signs a JWT
5
+ assertion (RFC 7521 / 7523), exchanges it at the OAuth token endpoint
6
+ (Hydra in Vana's deployment) for an RFC 9068 JWT access token, and
7
+ forwards that token as `Authorization: Bearer <jwt>` to the resource
8
+ server. The resource server verifies the JWT locally against the
9
+ issuer's JWKS, with no back-channel introspect on the hot path.
10
+
11
+ The package wraps [`jose`](https://github.com/panva/jose) and ships three
12
+ subpaths consumers care about (`./client`, `./jwks`, `./resource-server`)
13
+ plus a `./testkit` for verifying integrations.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ npm install @opendatalabs/service-auth jose
19
+ ```
20
+
21
+ ## Caller (`./client`)
22
+
23
+ ```ts
24
+ import { createServiceAuthClient } from "@opendatalabs/service-auth/client";
25
+
26
+ const auth = createServiceAuthClient({
27
+ clientId: process.env.SERVICE_CLIENT_ID!,
28
+ privateKeyJwk: JSON.parse(process.env.SERVICE_PRIVATE_JWK!),
29
+ tokenEndpoint: process.env.OAUTH_TOKEN_ENDPOINT!,
30
+ });
31
+
32
+ const { token } = await auth.getServiceToken({
33
+ audience: "vana-account-introspect",
34
+ });
35
+
36
+ await fetch("https://account.vana.org/api/oauth/introspect", {
37
+ method: "POST",
38
+ headers: {
39
+ authorization: `Bearer ${token}`,
40
+ "content-type": "application/json",
41
+ },
42
+ body: JSON.stringify({ token: someUserToken }),
43
+ });
44
+ ```
45
+
46
+ Tokens are cached in-process by `(audience, scope)` until 30s before
47
+ expiry. No on-disk cache, no shared cache, no refresh-token flow. A cold
48
+ start re-fetches.
49
+
50
+ ## Publishing JWKS (`./jwks`)
51
+
52
+ ```ts
53
+ // app/.well-known/jwks.json/route.ts
54
+ import { createJwksHandler } from "@opendatalabs/service-auth/jwks";
55
+
56
+ const handler = createJwksHandler({
57
+ keys: [
58
+ JSON.parse(process.env.SERVICE_PUBLIC_JWK_CURRENT!),
59
+ // During rotation, include the previous public key too:
60
+ JSON.parse(process.env.SERVICE_PUBLIC_JWK_PREVIOUS ?? "null"),
61
+ ].filter(Boolean),
62
+ });
63
+
64
+ export function GET(request: Request) {
65
+ return handler(request);
66
+ }
67
+ ```
68
+
69
+ The helper strips all private fields. If you accidentally pass a private
70
+ JWK, the response still only exposes the public half.
71
+
72
+ ## Resource server (`./resource-server`)
73
+
74
+ ```ts
75
+ import { createServiceAuthValidator } from "@opendatalabs/service-auth/resource-server";
76
+
77
+ const validator = createServiceAuthValidator({
78
+ jwksUri: `${process.env.OAUTH_PUBLIC_URL}/.well-known/jwks.json`,
79
+ issuer: process.env.OAUTH_ISSUER!,
80
+ audience: "vana-account-introspect",
81
+ });
82
+
83
+ export async function POST(request: Request) {
84
+ const auth = await validator.requireJwt()(request);
85
+ if (!auth.ok) return auth.response;
86
+ // auth.result.clientId is the verified caller.
87
+ // ...do your work.
88
+ }
89
+ ```
90
+
91
+ Rejection shapes:
92
+
93
+ - **401** with `WWW-Authenticate: Bearer error="invalid_token"` for
94
+ expired / wrong-iss / wrong-aud / bad-signature / not-a-bearer.
95
+ - **403** with `WWW-Authenticate: Bearer error="insufficient_scope"` when
96
+ `requiredScopes` is not satisfied.
97
+
98
+ ## Testing (`./testkit`)
99
+
100
+ ```ts
101
+ import { createFixtures } from "@opendatalabs/service-auth/testkit";
102
+ import { createServiceAuthValidator } from "@opendatalabs/service-auth/resource-server";
103
+
104
+ const f = await createFixtures({ audience: "my-resource" });
105
+ const validator = createServiceAuthValidator({
106
+ issuer: f.issuer.issuer,
107
+ audience: "my-resource",
108
+ jwks: f.issuer.jwks,
109
+ });
110
+ ```
111
+
112
+ ## About this package
113
+
114
+ This package implements [RFC 7521](https://www.rfc-editor.org/rfc/rfc7521)
115
+ (assertion framework), [RFC 7523](https://www.rfc-editor.org/rfc/rfc7523)
116
+ (JWT bearer assertions for `private_key_jwt`), and
117
+ [RFC 9068](https://www.rfc-editor.org/rfc/rfc9068) (JWT access tokens).
118
+
119
+ It is the public companion to Vana's internal service-auth setup. The
120
+ `./registration` subpath (manifest schema + Hydra admin reconciliation)
121
+ is Vana-operator-only and not part of the public surface; it is exported
122
+ so the Vana account app can consume it as a workspace package.
123
+
124
+ ## Versioning
125
+
126
+ Released via [semantic-release](https://github.com/semantic-release/semantic-release)
127
+ from the `vana-com/unity-surfaces` repository. Pushes to `main` publish
128
+ the stable channel (dist-tag `latest`). Pushes to `dev` publish the
129
+ prerelease channel (dist-tag `next`, e.g. `1.0.0-next.1`). Use
130
+ [conventional commits](https://www.conventionalcommits.org/) for commit
131
+ messages.
132
+
133
+ ```sh
134
+ npm install @opendatalabs/service-auth # latest
135
+ npm install @opendatalabs/service-auth@next # prerelease
136
+ ```
137
+
138
+ ## License
139
+
140
+ MIT.
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Service-to-service caller. INTERNAL Vana package, not a public SDK.
3
+ *
4
+ * What this does
5
+ * --------------
6
+ *
7
+ * Given a Vana service that wants to call another Vana service:
8
+ *
9
+ * 1. Signs a `client_assertion` JWT per RFC 7523 with the caller's
10
+ * private key.
11
+ * 2. POSTs `grant_type=client_credentials` + the assertion to Hydra's
12
+ * token endpoint to obtain a JWT-shaped access token (RFC 9068).
13
+ * 3. Caches the access token by (audience, scope) until 30s before
14
+ * `exp` and returns it to the caller for forwarding as
15
+ * `Authorization: Bearer <jwt>` on the inter-service hop.
16
+ *
17
+ * What this does NOT do
18
+ * ---------------------
19
+ *
20
+ * - It does NOT manage your private key. Hand `privateKeyJwk` in from
21
+ * workload secrets (Doppler, Vercel env, GCP Secret Manager). The
22
+ * key never touches `process.env` of this package.
23
+ * - It does NOT host your JWKS. Use the `jwks` subpath for that.
24
+ * - It does NOT speak Hydra's admin API. Use the `registration`
25
+ * subpath for provisioning.
26
+ *
27
+ * RFC references
28
+ * --------------
29
+ *
30
+ * - RFC 7521: Assertion Framework for OAuth 2.0.
31
+ * - RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and
32
+ * Authorization Grants (the `private_key_jwt` pattern).
33
+ * - RFC 9068: JWT Profile for OAuth 2.0 Access Tokens (the shape of
34
+ * the token Hydra returns to us).
35
+ */
36
+ import { type JWK } from "jose";
37
+ /**
38
+ * Construction options. All fields are required and have no defaults
39
+ * because every value is environment- and service-specific.
40
+ */
41
+ export interface ServiceAuthClientOptions {
42
+ /**
43
+ * Hydra OAuth client id provisioned for this service. Matches the
44
+ * `hydra_client_id` in the service-auth.yaml manifest.
45
+ */
46
+ clientId: string;
47
+ /**
48
+ * Private JWK whose public counterpart is published at the service's
49
+ * JWKS URI and registered with Hydra under `jwks_uri`. Must have
50
+ * `kid` and `alg` set.
51
+ *
52
+ * The JWK is never serialized or logged by this package.
53
+ */
54
+ privateKeyJwk: JWK;
55
+ /**
56
+ * Hydra token endpoint URL, e.g.
57
+ * `https://oauth-dev.vana.org/oauth2/token`. Used both as the POST
58
+ * target and as the `aud` claim on the client assertion.
59
+ */
60
+ tokenEndpoint: string;
61
+ /**
62
+ * Hydra issuer URL, used as the `iss` claim on the client assertion.
63
+ * Distinct from `tokenEndpoint` so deployments with a separate
64
+ * issuer hostname (e.g. proxied via a public domain) still work.
65
+ *
66
+ * RFC 7523 §3 only requires `iss` to be a value the auth server can
67
+ * use to identify the client; for Hydra `private_key_jwt`, the
68
+ * caller's `client_id` is the canonical value, so we default the
69
+ * assertion `iss` to `clientId`. This field is kept for forensics
70
+ * and future flexibility.
71
+ */
72
+ issuer?: string;
73
+ /**
74
+ * Optional override of the wall clock (seconds since epoch). Tests
75
+ * pass a deterministic value; production omits.
76
+ */
77
+ now?: () => number;
78
+ /** Optional fetch override (tests). */
79
+ fetch?: (input: string, init?: RequestInit) => Promise<Response>;
80
+ /**
81
+ * Cache eviction watermark in seconds. Tokens are refreshed this
82
+ * many seconds BEFORE their `exp`. Default 30s.
83
+ */
84
+ refreshSkewSec?: number;
85
+ /**
86
+ * Assertion `exp` lifetime in seconds. Hydra rejects assertions
87
+ * with exp too far in the future; 60s is the well-trodden value.
88
+ */
89
+ assertionLifetimeSec?: number;
90
+ }
91
+ export interface GetServiceTokenInput {
92
+ /**
93
+ * The resource audience (e.g. `"vana-account-introspect"`). MUST be
94
+ * present in the manifest's `allowed_audiences`. Sent to Hydra as
95
+ * the `audience` form parameter; Hydra mints an access token whose
96
+ * `aud` includes this value.
97
+ */
98
+ audience: string;
99
+ /**
100
+ * Optional space-separated scopes. Empty/undefined means "no
101
+ * scopes" (service tokens are usually audience-scoped, not
102
+ * permission-scoped).
103
+ */
104
+ scope?: string;
105
+ }
106
+ export interface ServiceToken {
107
+ /** The raw JWT-shaped access token. */
108
+ token: string;
109
+ /** Absolute unix-seconds expiry from the Hydra response. */
110
+ expiresAt: number;
111
+ }
112
+ export interface SignRequestAssertionInput {
113
+ /**
114
+ * Optional override of the assertion `aud`. Defaults to the token
115
+ * endpoint, which is what RFC 7523 §3 prescribes for the
116
+ * `private_key_jwt` use case.
117
+ */
118
+ audience?: string;
119
+ }
120
+ export interface ServiceAuthClient {
121
+ /**
122
+ * Fetch (or return cached) a service-to-service access token for
123
+ * the given audience+scope. Refreshes when within `refreshSkewSec`
124
+ * of expiry. Throws on token-endpoint errors.
125
+ */
126
+ getServiceToken(input: GetServiceTokenInput): Promise<ServiceToken>;
127
+ /**
128
+ * Mint a single RFC 7523 client assertion. Advanced use only;
129
+ * `getServiceToken` already does this internally.
130
+ */
131
+ signRequestAssertion(input?: SignRequestAssertionInput): Promise<string>;
132
+ /** Test-only: drop the in-memory token cache. */
133
+ clearCache(): void;
134
+ }
135
+ export declare class ServiceAuthTokenError extends Error {
136
+ readonly status: number;
137
+ readonly body: unknown;
138
+ constructor(status: number, body: unknown);
139
+ }
140
+ export declare function createServiceAuthClient(options: ServiceAuthClientOptions): ServiceAuthClient;
141
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAEL,KAAK,GAAG,EAGT,MAAM,MAAM,CAAC;AAKd;;;GAGG;AACH,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,aAAa,EAAE,GAAG,CAAC;IACnB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,uCAAuC;IACvC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACpE;;;OAGG;IACH,oBAAoB,CAClB,KAAK,CAAC,EAAE,yBAAyB,GAChC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,iDAAiD;IACjD,UAAU,IAAI,IAAI,CAAC;CACpB;AAqDD,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;gBACX,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;CAgB1C;AAED,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,iBAAiB,CAuHnB"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Service-to-service caller. INTERNAL Vana package, not a public SDK.
3
+ *
4
+ * What this does
5
+ * --------------
6
+ *
7
+ * Given a Vana service that wants to call another Vana service:
8
+ *
9
+ * 1. Signs a `client_assertion` JWT per RFC 7523 with the caller's
10
+ * private key.
11
+ * 2. POSTs `grant_type=client_credentials` + the assertion to Hydra's
12
+ * token endpoint to obtain a JWT-shaped access token (RFC 9068).
13
+ * 3. Caches the access token by (audience, scope) until 30s before
14
+ * `exp` and returns it to the caller for forwarding as
15
+ * `Authorization: Bearer <jwt>` on the inter-service hop.
16
+ *
17
+ * What this does NOT do
18
+ * ---------------------
19
+ *
20
+ * - It does NOT manage your private key. Hand `privateKeyJwk` in from
21
+ * workload secrets (Doppler, Vercel env, GCP Secret Manager). The
22
+ * key never touches `process.env` of this package.
23
+ * - It does NOT host your JWKS. Use the `jwks` subpath for that.
24
+ * - It does NOT speak Hydra's admin API. Use the `registration`
25
+ * subpath for provisioning.
26
+ *
27
+ * RFC references
28
+ * --------------
29
+ *
30
+ * - RFC 7521: Assertion Framework for OAuth 2.0.
31
+ * - RFC 7523: JWT Profile for OAuth 2.0 Client Authentication and
32
+ * Authorization Grants (the `private_key_jwt` pattern).
33
+ * - RFC 9068: JWT Profile for OAuth 2.0 Access Tokens (the shape of
34
+ * the token Hydra returns to us).
35
+ */
36
+ import { importJWK, SignJWT, } from "jose";
37
+ const ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
38
+ function defaultNow() {
39
+ return Math.floor(Date.now() / 1000);
40
+ }
41
+ /**
42
+ * Build a stable cache key. Scope tokens are sorted so callers that
43
+ * pass `"a b"` and `"b a"` share a cache slot.
44
+ */
45
+ function cacheKey(audience, scope) {
46
+ const sorted = (scope ?? "")
47
+ .split(/\s+/)
48
+ .filter(Boolean)
49
+ .sort()
50
+ .join(" ");
51
+ return `${audience}::${sorted}`;
52
+ }
53
+ /**
54
+ * Generate a 128-bit random `jti`. Hydra requires uniqueness across
55
+ * the validity window of the assertion.
56
+ */
57
+ function randomJti() {
58
+ const bytes = new Uint8Array(16);
59
+ globalThis.crypto.getRandomValues(bytes);
60
+ let hex = "";
61
+ for (const b of bytes) {
62
+ hex += b.toString(16).padStart(2, "0");
63
+ }
64
+ return hex;
65
+ }
66
+ export class ServiceAuthTokenError extends Error {
67
+ status;
68
+ body;
69
+ constructor(status, body) {
70
+ let message = `service-auth: token endpoint returned ${status}`;
71
+ if (body &&
72
+ typeof body === "object" &&
73
+ "error" in body) {
74
+ const err = body.error;
75
+ const desc = body.error_description;
76
+ message += ` (${err}${desc ? `: ${desc}` : ""})`;
77
+ }
78
+ super(message);
79
+ this.name = "ServiceAuthTokenError";
80
+ this.status = status;
81
+ this.body = body;
82
+ }
83
+ }
84
+ export function createServiceAuthClient(options) {
85
+ assertNonEmpty(options.clientId, "clientId");
86
+ assertNonEmpty(options.tokenEndpoint, "tokenEndpoint");
87
+ if (!options.privateKeyJwk?.kid) {
88
+ throw new Error("service-auth: privateKeyJwk.kid is required (must match the public key registered in JWKS).");
89
+ }
90
+ if (!options.privateKeyJwk?.alg) {
91
+ throw new Error("service-auth: privateKeyJwk.alg is required (e.g. 'ES256', 'RS256').");
92
+ }
93
+ const now = options.now ?? defaultNow;
94
+ const fetchImpl = options.fetch ?? fetch;
95
+ const refreshSkewSec = options.refreshSkewSec ?? 30;
96
+ const assertionLifetimeSec = options.assertionLifetimeSec ?? 60;
97
+ const issuer = options.issuer ?? options.clientId;
98
+ const cache = new Map();
99
+ // jose.importJWK returns either a CryptoKey (web) or KeyObject
100
+ // (Node). Both satisfy the SignJWT.sign() signature.
101
+ let privateKeyPromise = null;
102
+ function getPrivateKey() {
103
+ if (!privateKeyPromise) {
104
+ privateKeyPromise = importJWK(options.privateKeyJwk, options.privateKeyJwk.alg);
105
+ }
106
+ return privateKeyPromise;
107
+ }
108
+ async function signRequestAssertion(input) {
109
+ const privateKey = await getPrivateKey();
110
+ const issuedAt = now();
111
+ return new SignJWT({})
112
+ .setProtectedHeader({
113
+ alg: options.privateKeyJwk.alg,
114
+ kid: options.privateKeyJwk.kid,
115
+ typ: "JWT",
116
+ })
117
+ .setIssuer(issuer)
118
+ .setSubject(options.clientId)
119
+ .setAudience(input?.audience ?? options.tokenEndpoint)
120
+ .setIssuedAt(issuedAt)
121
+ .setNotBefore(issuedAt)
122
+ .setExpirationTime(issuedAt + assertionLifetimeSec)
123
+ .setJti(randomJti())
124
+ // Use `any`-shaped key argument for jose 6 cross-runtime types.
125
+ .sign(privateKey);
126
+ }
127
+ async function fetchToken(audience, scope) {
128
+ const assertion = await signRequestAssertion();
129
+ const form = new URLSearchParams({
130
+ grant_type: "client_credentials",
131
+ client_assertion_type: ASSERTION_TYPE,
132
+ client_assertion: assertion,
133
+ audience,
134
+ });
135
+ if (scope && scope.trim()) {
136
+ form.set("scope", scope.trim());
137
+ }
138
+ const response = await fetchImpl(options.tokenEndpoint, {
139
+ method: "POST",
140
+ headers: {
141
+ "content-type": "application/x-www-form-urlencoded",
142
+ accept: "application/json",
143
+ },
144
+ body: form.toString(),
145
+ });
146
+ const text = await response.text();
147
+ let parsed = {};
148
+ if (text) {
149
+ try {
150
+ parsed = JSON.parse(text);
151
+ }
152
+ catch {
153
+ parsed = { raw: text };
154
+ }
155
+ }
156
+ if (!response.ok) {
157
+ throw new ServiceAuthTokenError(response.status, parsed);
158
+ }
159
+ const ok = parsed;
160
+ if (typeof ok.access_token !== "string" || typeof ok.expires_in !== "number") {
161
+ throw new ServiceAuthTokenError(response.status, parsed);
162
+ }
163
+ return {
164
+ token: ok.access_token,
165
+ expiresAt: now() + ok.expires_in,
166
+ };
167
+ }
168
+ return {
169
+ async getServiceToken(input) {
170
+ assertNonEmpty(input.audience, "audience");
171
+ const key = cacheKey(input.audience, input.scope);
172
+ const hit = cache.get(key);
173
+ const t = now();
174
+ if (hit && hit.expiresAt - refreshSkewSec > t) {
175
+ return { token: hit.token, expiresAt: hit.expiresAt };
176
+ }
177
+ const fresh = await fetchToken(input.audience, input.scope);
178
+ cache.set(key, fresh);
179
+ return fresh;
180
+ },
181
+ signRequestAssertion,
182
+ clearCache() {
183
+ cache.clear();
184
+ },
185
+ };
186
+ }
187
+ function assertNonEmpty(value, name) {
188
+ if (!value || typeof value !== "string") {
189
+ throw new Error(`service-auth: ${name} is required`);
190
+ }
191
+ }
192
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * JWKS publishing helper. INTERNAL Vana package.
3
+ *
4
+ * A service that authenticates to Hydra with `private_key_jwt` must
5
+ * publish the *public* counterpart of its signing key at a stable
6
+ * URI that Hydra fetches. This helper produces:
7
+ *
8
+ * - `buildJwksDocument({ keys })` -- returns the JSON shape suitable
9
+ * for `/.well-known/jwks.json`.
10
+ * - `createJwksHandler({ keys })` -- returns a function
11
+ * `(request: Request) => Response` that a Next.js / Hono / plain
12
+ * Node handler can call.
13
+ *
14
+ * Key rotation
15
+ * ------------
16
+ *
17
+ * `keys` is an array; publish the current and previous keys
18
+ * simultaneously during rotation so callers that cached an older
19
+ * `kid` still verify until they refresh. Hydra's
20
+ * `createRemoteJWKSet` and ours both pick the right key by `kid`.
21
+ *
22
+ * Safety
23
+ * ------
24
+ *
25
+ * Each entry is stripped of any private-key material via an
26
+ * allowlist of public-only members. This package will NEVER serialize
27
+ * `d`, `p`, `q`, `dp`, `dq`, or `qi`.
28
+ */
29
+ import type { JWK } from "jose";
30
+ export interface PublicJwk {
31
+ kty: string;
32
+ kid: string;
33
+ alg: string;
34
+ use?: string;
35
+ n?: string;
36
+ e?: string;
37
+ crv?: string;
38
+ x?: string;
39
+ y?: string;
40
+ }
41
+ export interface JwksDocument {
42
+ keys: PublicJwk[];
43
+ }
44
+ export interface CreateJwksOptions {
45
+ /**
46
+ * One or more JWKs. Each must have a stable `kid` and `alg`.
47
+ * Private-key fields are stripped before publication.
48
+ */
49
+ keys: JWK[];
50
+ }
51
+ export declare function buildJwksDocument(options: CreateJwksOptions): JwksDocument;
52
+ export type JwksHandler = (request: Request) => Response;
53
+ /**
54
+ * Build the JWKS Response with the standard headers. Pre-computed at
55
+ * handler-creation time because the keyset is immutable per process.
56
+ */
57
+ export declare function createJwksHandler(options: CreateJwksOptions): JwksHandler;
58
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jwks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEhC,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;IAEX,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;CAEZ;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,IAAI,EAAE,GAAG,EAAE,CAAC;CACb;AAwBD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,YAAY,CAiB1E;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,WAAW,CAYzE"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * JWKS publishing helper. INTERNAL Vana package.
3
+ *
4
+ * A service that authenticates to Hydra with `private_key_jwt` must
5
+ * publish the *public* counterpart of its signing key at a stable
6
+ * URI that Hydra fetches. This helper produces:
7
+ *
8
+ * - `buildJwksDocument({ keys })` -- returns the JSON shape suitable
9
+ * for `/.well-known/jwks.json`.
10
+ * - `createJwksHandler({ keys })` -- returns a function
11
+ * `(request: Request) => Response` that a Next.js / Hono / plain
12
+ * Node handler can call.
13
+ *
14
+ * Key rotation
15
+ * ------------
16
+ *
17
+ * `keys` is an array; publish the current and previous keys
18
+ * simultaneously during rotation so callers that cached an older
19
+ * `kid` still verify until they refresh. Hydra's
20
+ * `createRemoteJWKSet` and ours both pick the right key by `kid`.
21
+ *
22
+ * Safety
23
+ * ------
24
+ *
25
+ * Each entry is stripped of any private-key material via an
26
+ * allowlist of public-only members. This package will NEVER serialize
27
+ * `d`, `p`, `q`, `dp`, `dq`, or `qi`.
28
+ */
29
+ const PRIVATE_FIELDS = new Set(["d", "p", "q", "dp", "dq", "qi", "k"]);
30
+ function toPublicJwk(jwk) {
31
+ if (!jwk.kid) {
32
+ throw new Error("service-auth/jwks: every key must have a stable kid");
33
+ }
34
+ if (!jwk.alg) {
35
+ throw new Error("service-auth/jwks: every key must declare alg");
36
+ }
37
+ if (!jwk.kty) {
38
+ throw new Error("service-auth/jwks: every key must declare kty");
39
+ }
40
+ const out = {};
41
+ for (const [k, v] of Object.entries(jwk)) {
42
+ if (PRIVATE_FIELDS.has(k))
43
+ continue;
44
+ if (v === undefined)
45
+ continue;
46
+ out[k] = v;
47
+ }
48
+ if (!out.use)
49
+ out.use = "sig";
50
+ return out;
51
+ }
52
+ export function buildJwksDocument(options) {
53
+ if (!Array.isArray(options.keys) || options.keys.length === 0) {
54
+ throw new Error("service-auth/jwks: at least one key is required");
55
+ }
56
+ const seen = new Set();
57
+ const out = [];
58
+ for (const k of options.keys) {
59
+ const pub = toPublicJwk(k);
60
+ if (seen.has(pub.kid)) {
61
+ throw new Error(`service-auth/jwks: duplicate kid '${pub.kid}' in keyset`);
62
+ }
63
+ seen.add(pub.kid);
64
+ out.push(pub);
65
+ }
66
+ return { keys: out };
67
+ }
68
+ /**
69
+ * Build the JWKS Response with the standard headers. Pre-computed at
70
+ * handler-creation time because the keyset is immutable per process.
71
+ */
72
+ export function createJwksHandler(options) {
73
+ const doc = buildJwksDocument(options);
74
+ const body = JSON.stringify(doc);
75
+ return (_request) => new Response(body, {
76
+ status: 200,
77
+ headers: {
78
+ "content-type": "application/jwk-set+json",
79
+ // 5 minutes; mirrors Hydra's own JWKS cache cadence.
80
+ "cache-control": "public, max-age=300, must-revalidate",
81
+ },
82
+ });
83
+ }
84
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/jwks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AA+BH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;AAEvE,SAAS,WAAW,CAAC,GAAQ;IAC3B,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IACD,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAS;QACpC,IAAI,CAAC,KAAK,SAAS;YAAE,SAAS;QAC9B,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACb,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,GAAG;QAAE,GAAG,CAAC,GAAG,GAAG,KAAK,CAAC;IAC9B,OAAO,GAA2B,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,KAAK,MAAM,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,qCAAqC,GAAG,CAAC,GAAG,aAAa,CAC1D,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAClB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AACvB,CAAC;AAID;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA0B;IAC1D,MAAM,GAAG,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,CAAC,QAAiB,EAAE,EAAE,CAC3B,IAAI,QAAQ,CAAC,IAAI,EAAE;QACjB,MAAM,EAAE,GAAG;QACX,OAAO,EAAE;YACP,cAAc,EAAE,0BAA0B;YAC1C,qDAAqD;YACrD,eAAe,EAAE,sCAAsC;SACxD;KACF,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Registration: manifest schema + Hydra provisioning. INTERNAL Vana
3
+ * package; see ./manifest.ts and ./provision.ts.
4
+ */
5
+ export { diffHydraClient, type HydraClientBody, loadManifest, ManifestValidationError, manifestToHydraClient, parseManifestText, serviceAuthManifestSchema, type ServiceAuthManifest, } from "./manifest";
6
+ export { HydraAdminApiError, provisionServiceClient, type ProvisionAction, type ProvisionFetch, type ProvisionOptions, type ProvisionResult, } from "./provision";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registration/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,eAAe,EACf,KAAK,eAAe,EACpB,YAAY,EACZ,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Registration: manifest schema + Hydra provisioning. INTERNAL Vana
3
+ * package; see ./manifest.ts and ./provision.ts.
4
+ */
5
+ export { diffHydraClient, loadManifest, ManifestValidationError, manifestToHydraClient, parseManifestText, serviceAuthManifestSchema, } from "./manifest";
6
+ export { HydraAdminApiError, provisionServiceClient, } from "./provision";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/registration/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,eAAe,EAEf,YAAY,EACZ,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,yBAAyB,GAE1B,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,kBAAkB,EAClB,sBAAsB,GAKvB,MAAM,aAAa,CAAC"}