oidc-caep-client 0.2.7-next.0 → 0.2.7

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
@@ -255,7 +255,7 @@ In addition, a replayed `jti` is dropped.
255
255
  - A SET is acknowledged only after its handlers ran without throwing. If one throws, the SET is left for the transmitter to send again, up to `maxDeliveryAttempts` times (default 5). After that it is acknowledged and `onError` gets `handler_failed`. Make handlers idempotent: a reload can also bring a handled SET back before its acknowledgement was sent.
256
256
  - Only a definitely invalid SET is reported as an error (bad signature, issuer, audience, `typ`, claims). One that couldn't be checked for a reason that may pass is left for redelivery for as long as that lasts, and never reported: a network error fetching the JWKS, or a key the cached JWKS doesn't hold yet.
257
257
  - Polls never overlap: `poll()` during a poll shares it, and `pollNow()` during a poll asks for another right after it. A SET whose handlers are still running is never acknowledged by another poll.
258
- - `stop()` sends the acknowledgements still owed.
258
+ - `stop()` sends the acknowledgements still owed. A handler may await it on its own receiver: it then returns at once, and the poll in flight sends them when it ends.
259
259
  - The poll endpoint must use TLS and share an origin with the transmitter (`trustedPollOrigins` adds others), because every poll carries the person's access token.
260
260
  - Errors go to `onError`; without one, they are written to `console.error`, so a lost signal always leaves a trace.
261
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oidc-caep-client",
3
- "version": "0.2.7-next.0",
3
+ "version": "0.2.7",
4
4
  "description": "In-browser OpenID Connect public client (PKCE, PAR, DPoP, RFC 9207) with token caching/auto-refresh and a Shared Signals (SSF) / CAEP event receiver",
5
5
  "homepage": "https://rcbj.github.io/oidc-caep-client/",
6
6
  "repository": {
@@ -153,6 +153,8 @@ export class SSFReceiver {
153
153
  #polling = false;
154
154
  /** The poll request in flight, if any: polls never overlap (see poll()). */
155
155
  #pollInFlight;
156
+ /** How many polled SETs' handlers are running now: stop() must not wait for their poll (see stop()). */
157
+ #pollHandlers = 0;
156
158
  /** pollNow() was called while a poll was in flight: poll again as soon as it finishes. */
157
159
  #pollAgain = false;
158
160
  /** The jtis whose handlers are running now; a redelivery of one of them is left alone. */
@@ -391,18 +393,33 @@ export class SSFReceiver {
391
393
 
392
394
  /**
393
395
  * Stops polling and optionally deletes the stream at the transmitter.
396
+ *
397
+ * Resolves once the acknowledgements still owed are sent (and the stream deleted). Called while a polled
398
+ * SET's handlers are running — from a handler itself, typically on session-revoked — it stops polling and
399
+ * resolves at once instead: the poll in flight finishes its SETs, then sends the acknowledgements and
400
+ * deletes the stream, with failures going to `onError` as `phase: 'stop'`.
394
401
  * @param {{ deleteStream?: boolean }} [opts]
395
402
  */
396
403
  async stop({ deleteStream = false } = {}) {
397
404
  this.#unwatchPageHide();
398
405
  this.#polling = false;
399
406
  clearTimeout(this.#pollTimer);
400
- // A poll still running may finish handling SETs; their acknowledgements go in the flush below.
401
- await this.#pollInFlight?.catch(() => {});
402
- await this.#flushAcks();
403
- if (deleteStream && this.stream?.stream_id) {
404
- await this.deleteStream().catch((err) => this.#emitError(err, { phase: 'stop' }));
407
+ const finish = async () => {
408
+ // A poll still running may finish handling SETs; their acknowledgements go in the flush below.
409
+ await this.#pollInFlight?.catch(() => {});
410
+ await this.#flushAcks();
411
+ if (deleteStream && this.stream?.stream_id) {
412
+ await this.deleteStream().catch((err) => this.#emitError(err, { phase: 'stop' }));
413
+ }
414
+ };
415
+ // The poll in flight is waiting for the handler that is running, so a handler awaiting stop() would
416
+ // wait for itself: a deadlock. The browser has no way to ask whether THIS call comes from that
417
+ // handler, so any call made while one runs leaves the rest to the end of the poll.
418
+ if (this.#pollHandlers > 0) {
419
+ finish().catch((err) => this.#emitError(err, { phase: 'stop' }));
420
+ return;
405
421
  }
422
+ await finish();
406
423
  }
407
424
 
408
425
  // ---------------------------------------------------------------------------
@@ -689,7 +706,13 @@ export class SSFReceiver {
689
706
  this.#emitError(err, { phase: 'poll', jti });
690
707
  return;
691
708
  }
692
- const handled = await this.#dispatchAll(events);
709
+ let handled;
710
+ this.#pollHandlers += 1;
711
+ try {
712
+ handled = await this.#dispatchAll(events);
713
+ } finally {
714
+ this.#pollHandlers -= 1;
715
+ }
693
716
  if (!handled && this.#attempt(jti) < this.#cfg.maxDeliveryAttempts) {
694
717
  // Let the redelivery through the replay check: it is the same SET, not handled yet.
695
718
  for (const evt of events) this.#seenJtis.delete(evt.jti);
@@ -396,6 +396,11 @@ export declare class SSFReceiver {
396
396
  }): Promise<Record<string, any>>;
397
397
  /**
398
398
  * Stops polling and optionally deletes the stream at the transmitter.
399
+ *
400
+ * Resolves once the acknowledgements still owed are sent (and the stream deleted). Called while a polled
401
+ * SET's handlers are running — from a handler itself, typically on session-revoked — it stops polling and
402
+ * resolves at once instead: the poll in flight finishes its SETs, then sends the acknowledgements and
403
+ * deletes the stream, with failures going to `onError` as `phase: 'stop'`.
399
404
  * @param {{ deleteStream?: boolean }} [opts]
400
405
  */
401
406
  stop({ deleteStream }?: {