@gjermundgaraba/clankerauth-sdk 0.13.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/README.md CHANGED
@@ -17,7 +17,7 @@ vp add effect
17
17
  The `/session` and `/effect-actions` entry points also need effect-actions, an optional peer. Install this range: the SDK's declarations are built against it, and under `skipLibCheck` they silently degrade to `any` with another.
18
18
 
19
19
  ```sh
20
- vp add @gjermundgaraba/effect-actions@^0.10.0
20
+ vp add @gjermundgaraba/effect-actions@^0.11.0
21
21
  ```
22
22
 
23
23
  ## Entry points
@@ -296,7 +296,7 @@ Tagged errors, all from `@gjermundgaraba/clankerauth-sdk/errors`: `Unauthorized`
296
296
 
297
297
  There is one 403. Only the verifier's own check fails with `InsufficientScope`: a credential that is valid but lacks a scope in `requiredScopes`, a resource's `required`. It names that scope and the credential's `actor`, which decides whether its `Forbidden` steps up. A scope asked for after verification, by `requires(scope)`, `admitted(scope)`, `admit(header, scope)` or `watch(header, scope)`, is refused with effect-actions' `Action.Forbidden` directly, decided by the same rule. A credential that is not this resource's at all — the wrong audience, or an API key with no entry in this resource's key list — is `Unauthorized`, because from the resource's side those are the same thing.
298
298
 
299
- JWTs are checked against issuer, audience, EdDSA signature, token type, required claims, expiry and scopes. Sender-constrained tokens are rejected. One JWKS read answers every verification for a minute, so a key the issuer starts or stops publishing takes effect within that minute; a `kid` the current document does not publish is `Unauthorized` and never triggers a read, so a token with an invented `kid` cannot become provider traffic. A failed read, or one whose caller gave up, answers `ProviderUnavailable` (503) for five seconds before the next attempt. API keys are verified offline, against the resource's key list: one document the issuer signs with its access-token key, holding a sealed entry per key granted on this resource, which only that key can find or open. The list is verified once, when it is read, and one read answers every key it names for a minute, without issuer traffic; so a key disabled, deleted or re-scoped keeps the access it had until the next read, within about a minute. A key the held list does not name may have been created, enabled or granted on this resource since, so the list is read again before the key is refused, unless it was read within the last five seconds: such a key works within seconds, and unknown keys reach the issuer at most once every five seconds. A key's own expiry is checked on every verification, so it applies at once. A failed read, one that takes longer than three seconds, or one whose caller gave up, is no error while the last list is inside its own lifetime, which the issuer sets to 24 hours: keys keep verifying through an outage, JWKS or not, and the issuer is tried again after five seconds. Past that lifetime, or before any list has been read, a key is `ProviderUnavailable`. Reads that start failing are logged once as a warning, and their recovery once at info level, so an outage the held list hides still shows in the application's logs without a line per retry. The last list read is the one held, whatever its `iat`: HTTPS to the issuer is what keeps an old list from being replayed, and one could only ever be replayed inside its own day. The issuer does not count key use, so rate limiting is the application's. A key is a bearer secret, and one granted on several resources can be replayed by any of them to the others; a resource you trust less gets its own key. A resource that accepts only OAuth access tokens sets `apiKeys: false`; key-shaped bearers then fail as `Unauthorized` without reading the key list. Admission through a `Resource` has a five-second deadline; the raw `Verifier` has none, so an in-process caller can await a provider call it cannot cancel. The exception is the key list read's own three seconds: shorter than that deadline, so an issuer that stops answering delays a key by at most that long, never fails it while a list is held.
299
+ JWTs are checked against issuer, audience, EdDSA signature, token type, required claims, expiry and scopes. Sender-constrained tokens are rejected. One JWKS read answers every verification for a minute, so a key the issuer starts or stops publishing takes effect within that minute; a `kid` the current document does not publish is `Unauthorized` and never triggers a read, so a token with an invented `kid` cannot become provider traffic. A failed read answers `ProviderUnavailable` (503) for five seconds before the next attempt; a read every waiting caller gave up on is dropped, and the next verification reads again. API keys are verified offline, against the resource's key list: one document the issuer signs with its access-token key, holding a sealed entry per key granted on this resource, which only that key can find or open. The list is verified once, when it is read, and one read answers every key it names for a minute, without issuer traffic; so a key disabled, deleted or re-scoped keeps the access it had until the next read, within about a minute. A key the held list does not name may have been created, enabled or granted on this resource since, so the list is read again before the key is refused, unless it was read within the last five seconds: such a key works within seconds, and unknown keys reach the issuer at most once every five seconds. A key's own expiry is checked on every verification, so it applies at once. A failed read, or one that takes longer than three seconds, is no error while the last list is inside its own lifetime, which the issuer sets to 24 hours: keys keep verifying through an outage, JWKS or not, and the issuer is tried again after five seconds. A read every waiting caller gave up on is dropped, and the next key reads again. Past that lifetime, or before any list has been read, a key is `ProviderUnavailable`. Reads that start failing are logged once as a warning, and their recovery once at info level, so an outage the held list hides still shows in the application's logs without a line per retry. The last list read is the one held, whatever its `iat`: HTTPS to the issuer is what keeps an old list from being replayed, and one could only ever be replayed inside its own day. The issuer does not count key use, so rate limiting is the application's. A key is a bearer secret, and one granted on several resources can be replayed by any of them to the others; a resource you trust less gets its own key. A resource that accepts only OAuth access tokens sets `apiKeys: false`; key-shaped bearers then fail as `Unauthorized` without reading the key list. Admission through a `Resource` has a five-second deadline; the raw `Verifier` has none, so an in-process caller can await a provider call it cannot cancel. The exception is the key list read's own three seconds: shorter than that deadline, so an issuer that stops answering delays a key by at most that long, never fails it while a list is held.
300
300
 
301
301
  The bundled issuer does not configure automatic signing-key rotation. Immediate-use rotation is supported, but tokens signed by a new key are rejected until the next JWKS read, up to a minute; a key list signed by it is not taken until then either, and the held list keeps deciding. Publish-before-use avoids that window; it is not mandatory. Key lists are re-signed on every read, so issued API keys are unaffected by rotation.
302
302
 
package/dist/resource.js CHANGED
@@ -63,9 +63,9 @@ const build = Effect.fn("Resource.layer")(function* (authentication, { scopes, r
63
63
  requiredScopes: [required],
64
64
  apiKeys: options.apiKeys,
65
65
  });
66
- // A request must not wait on an issuer that does not answer: the transport is
67
- // interrupted and the credential refused as unavailable. A key list read gives up
68
- // sooner, so a held list still decides a key.
66
+ // A request must not wait on an issuer that does not answer: the credential is refused
67
+ // as unavailable, and a read no other request waits on is interrupted. A key list read
68
+ // gives up sooner, so a held list still decides a key.
69
69
  const deadline = (effect) => Effect.timeoutOrElse(effect, {
70
70
  duration: "5 seconds",
71
71
  orElse: () => Effect.fail(new ProviderUnavailable({ operation: "verify.timeout" })),
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";
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba/clankerauth-sdk",
3
- "version": "0.13.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.10.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.10.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": {