@gjermundgaraba/clankerauth-sdk 0.12.0 → 0.14.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/session.js CHANGED
@@ -1,11 +1,20 @@
1
1
  /**
2
2
  * The identity a browser page needs, without any verification code. This entry point is
3
- * browser-safe: it declares the `session` contract and builds the issuer's sign-out URL,
4
- * so a page imports it directly and a server answers it with `resource.session`.
3
+ * browser-safe: it declares the identity a protected contract states, the `whoami` contract
4
+ * and the issuer's sign-out URL, so contracts, bindings and pages import it directly, and a
5
+ * server answers `Whoami` with its resource's `session`.
5
6
  */
6
- import { Schema } from "effect";
7
+ import { Context, Schema } from "effect";
7
8
  import * as Action from "@gjermundgaraba/effect-actions/Action";
8
- import * as ActionGroup from "@gjermundgaraba/effect-actions/ActionGroup";
9
+ /**
10
+ * The caller: what a protected contract states, `caller: CurrentPrincipal`, and what an
11
+ * authentication descriptor proves,
12
+ * `Authentication.make("notes.Login", CurrentPrincipal, { error: ProviderUnavailable })`.
13
+ * Remotely, the resource's provider gives it per request, from the verified credential; on
14
+ * a trusted local surface, the host gives it the process, `Notes.local(subject)`.
15
+ */
16
+ export class CurrentPrincipal extends Context.Service()("@clankerauth/CurrentPrincipal") {
17
+ }
9
18
  /** The verified credential behind a request, as the resource server read it. */
10
19
  export const Principal = Schema.Struct({
11
20
  subject: Schema.NonEmptyString,
@@ -13,15 +22,16 @@ export const Principal = Schema.Struct({
13
22
  scopes: Schema.Array(Schema.String),
14
23
  });
15
24
  /**
16
- * The credential is a transport concern: a process surface has none, so `whoami` is its
17
- * own group and never an MCP tool. Bind it with `Http.layer([app, resource.session])`.
25
+ * The credential is a transport concern: a process surface has none, so bind `Whoami` to
26
+ * HTTP alone, `ActionHttp.layer(Http, [app, Notes.session])`, and leave `Notes.session` out
27
+ * of MCP.
18
28
  */
19
- export const Session = ActionGroup.make({ name: "session" }, Action.make("whoami", {
29
+ export const Whoami = Action.make("whoami", {
20
30
  description: "The verified subject, issuer and scopes behind this request's credential.",
21
- access: "read",
31
+ readOnly: true,
32
+ caller: CurrentPrincipal,
22
33
  success: Principal,
23
- mcp: false,
24
- }));
34
+ });
25
35
  /**
26
36
  * Sign out at the issuer, which ends its session and every app's forward cookie, then
27
37
  * return to `returnTo`. It is a plain link: any page under the cookie domain may use it.
package/dist/testing.js CHANGED
@@ -29,7 +29,7 @@ export const startFakeIssuer = async (options) => {
29
29
  };
30
30
  if (request.url === "/api/auth/jwks")
31
31
  return send(failure ?? 200, JSON.stringify({ keys: [jwk] }));
32
- if (request.url !== "/api/issuer/keyList" || request.method !== "POST")
32
+ if (request.url !== "/api/keyList" || request.method !== "POST")
33
33
  return send(404, "{}");
34
34
  lists++;
35
35
  if (failure !== undefined)
package/dist/verify.d.ts CHANGED
@@ -2,6 +2,11 @@ import { Effect } from "effect";
2
2
  import { HttpClient } from "effect/http";
3
3
  import { ConfigurationError, InsufficientScope, ProviderUnavailable, Unauthorized } from "./errors.ts";
4
4
  import type { AuthenticationError } from "./errors.ts";
5
+ /**
6
+ * What a verified credential proves: an OAuth client's access token or an API key. A request's
7
+ * caller, `CurrentPrincipal`, is this or, on a trusted local surface, the process itself, which
8
+ * no verifier produces: `Caller` of the `/session` entry.
9
+ */
5
10
  export interface Principal {
6
11
  readonly subject: string;
7
12
  readonly scopes: readonly string[];
@@ -28,7 +33,6 @@ export interface Options {
28
33
  readonly apiKeys?: boolean;
29
34
  }
30
35
  export interface Verifier {
31
- readonly verify: (authorization: string | null | undefined) => Effect.Effect<Principal, AuthenticationError>;
32
36
  readonly verifyToken: (token: string) => Effect.Effect<Principal, AuthenticationError>;
33
37
  }
34
38
  /**
@@ -55,21 +59,4 @@ export declare const make: (options: Options) => Effect.Effect<{
55
59
  };
56
60
  expiresAt: number | undefined;
57
61
  }, InsufficientScope | ProviderUnavailable | Unauthorized, never>;
58
- verify: (authorization: string | null | undefined) => Effect.Effect<{
59
- subject: string;
60
- scopes: string[];
61
- actor: {
62
- kind: "client";
63
- clientId: string;
64
- };
65
- expiresAt: number;
66
- } | {
67
- subject: string;
68
- scopes: readonly string[];
69
- actor: {
70
- kind: "key";
71
- keyId: string;
72
- };
73
- expiresAt: number | undefined;
74
- }, InsufficientScope | ProviderUnavailable | Unauthorized, never>;
75
62
  }, ConfigurationError, HttpClient.HttpClient>;
package/dist/verify.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Cause, Clock, Duration, Effect, Exit, Ref, Result, Schema } from "effect";
1
+ import { Clock, Duration, Effect, Exit, Ref, Result, Schema } from "effect";
2
2
  import { createLocalJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
3
3
  import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
4
4
  import { ConfigurationError, InsufficientScope, ProviderUnavailable, Unauthorized, } from "./errors.js";
@@ -26,7 +26,7 @@ const Claims = Schema.Struct({
26
26
  /** The list document as the issuer's `keyList` action answers it. */
27
27
  const KeyListResponse = Schema.Struct({ list: Schema.String });
28
28
  const KeyListClaims = KeyList.Claims.pipe(Schema.fieldsAssign({ exp: Schema.Finite }));
29
- const unauthorized = (cause) => new Unauthorized({ message: "Authentication required", cause });
29
+ const unauthorized = (cause) => new Unauthorized({ cause });
30
30
  /** How long a JWKS document answers verification before it is read again. */
31
31
  const documentLifetime = "1 minute";
32
32
  /**
@@ -50,7 +50,7 @@ export const make = Effect.fn("Verifier.make")(function* (options) {
50
50
  const endpoints = yield* Effect.try({
51
51
  try: () => ({
52
52
  jwks: new URL(`${options.issuer}/jwks`).href,
53
- keyList: new URL("/api/issuer/keyList", options.issuer).href,
53
+ keyList: new URL("/api/keyList", options.issuer).href,
54
54
  }),
55
55
  catch: () => new ConfigurationError({ message: "Invalid issuer URL" }),
56
56
  });
@@ -66,15 +66,10 @@ export const make = Effect.fn("Verifier.make")(function* (options) {
66
66
  keys: jwks.keys.map(({ key_ops, ...key }) => key_ops ? { ...key, key_ops: [...key_ops] } : key),
67
67
  });
68
68
  }, Effect.scoped);
69
- // One read serves every key identifier until it expires, whatever its outcome, so an
70
- // unknown `kid` in an unauthenticated request never becomes traffic at the issuer.
71
- const read = yield* Effect.cachedWithTTL(document(), (exit) => Exit.isSuccess(exit) ? documentLifetime : cooldown);
72
- // Replaying a read must not hand a later request the interruption of an earlier
73
- // one's deadline; within the window the issuer simply was not reached.
74
- const replayed = (cached, operation) => Effect.catchCause(cached, (cause) => Cause.hasInterrupts(cause)
75
- ? Effect.fail(new ProviderUnavailable({ operation }))
76
- : Effect.failCause(cause));
77
- const keys = replayed(read, "jwks.refresh");
69
+ // One read serves every key identifier until it expires, or for the cooldown if it
70
+ // failed, so an unknown `kid` in an unauthenticated request never becomes traffic at
71
+ // the issuer. A read every caller gave up on is dropped, and the next caller reads again.
72
+ const keys = yield* Effect.cachedWithTTL(document(), (exit) => Exit.isSuccess(exit) ? documentLifetime : cooldown);
78
73
  const signingKey = Effect.fn("Verifier.signingKey")(function* (kid) {
79
74
  const resolve = yield* keys;
80
75
  return yield* Effect.tryPromise({
@@ -159,7 +154,7 @@ export const make = Effect.fn("Verifier.make")(function* (options) {
159
154
  const held = yield* Ref.make(undefined);
160
155
  /** The list read now, or within the cooldown, or else the held one, while inside its window. */
161
156
  const latest = Effect.gen(function* () {
162
- const fetched = yield* Effect.result(replayed(readList, "key-list.refresh"));
157
+ const fetched = yield* Effect.result(readList);
163
158
  if (Result.isSuccess(fetched))
164
159
  yield* Ref.set(held, fetched.success);
165
160
  const list = yield* Ref.get(held);
@@ -207,16 +202,8 @@ export const make = Effect.fn("Verifier.make")(function* (options) {
207
202
  : verifyJwt(token);
208
203
  const missing = options.requiredScopes?.find((scope) => !principal.scopes.includes(scope));
209
204
  if (missing !== undefined)
210
- return yield* new InsufficientScope({ scope: missing });
205
+ return yield* new InsufficientScope({ scope: missing, actor: principal.actor });
211
206
  return principal;
212
207
  });
213
- return {
214
- verifyToken,
215
- verify: Effect.fn("Verifier.verify")(function* (authorization) {
216
- const token = /^Bearer ([^\s,]+)$/iu.exec(authorization ?? "")?.[1];
217
- if (!token)
218
- return yield* unauthorized();
219
- return yield* verifyToken(token);
220
- }),
221
- };
208
+ return { verifyToken };
222
209
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba/clankerauth-sdk",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Effect-native clankerauth access-token and offline API-key verification, with optional effect-actions integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,17 +46,17 @@
46
46
  "jose": "6.2.12"
47
47
  },
48
48
  "devDependencies": {
49
- "@gjermundgaraba/effect-actions": "0.9.0",
50
- "@modelcontextprotocol/client": "2.0.0",
51
- "@types/node": "26.6.2",
52
- "effect": "4.0.0",
49
+ "@gjermundgaraba/effect-actions": "0.11.0",
50
+ "@modelcontextprotocol/client": "2.3.1",
51
+ "@types/node": "26.6.4",
52
+ "effect": "4.0.2",
53
53
  "typescript": "7.0.2",
54
- "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
55
- "vite-plus": "1.0.0"
54
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
55
+ "vite-plus": "1.1.0"
56
56
  },
57
57
  "peerDependencies": {
58
- "@gjermundgaraba/effect-actions": "^0.9.0",
59
- "effect": "^4.0.0"
58
+ "@gjermundgaraba/effect-actions": "^0.11.0",
59
+ "effect": "^4.0.2"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "@gjermundgaraba/effect-actions": {