@semics-tech/emdash-events-suite 1.0.0-rc.2 → 1.0.0-rc.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semics-tech/emdash-events-suite",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.3",
4
4
  "type": "module",
5
5
  "description": "Links semics Events and E-sign on one EmDash site: one config call and a booking flow with inline waiver signing",
6
6
  "license": "MIT",
@@ -22,8 +22,8 @@
22
22
  "src/astro"
23
23
  ],
24
24
  "dependencies": {
25
- "@semics-tech/emdash-esign": "1.0.0-rc.2",
26
- "@semics-tech/emdash-events": "1.0.0-rc.2"
25
+ "@semics-tech/emdash-events": "1.0.0-rc.3",
26
+ "@semics-tech/emdash-esign": "1.0.0-rc.3"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "emdash": ">=1.2.0 <2"
@@ -354,6 +354,26 @@ async function retry(ctx: WaiverRequestContext, ids: SuiteIds): Promise<WaiverOu
354
354
  return proceed(ctx, ids, ref, continued.data.next, [], accessToken);
355
355
  }
356
356
 
357
+ /**
358
+ * T153: Pro's intents carry the hold as `signBy`, and a receipt extends the hold. Continue once more so
359
+ * the seats still to sign get intents with the new hold: e-sign moves each envelope's deadline and rotates
360
+ * its token. Refused (a lapsed licence, say), the next signer goes on with the token already held, whose
361
+ * envelope still runs to the hold it was issued for.
362
+ */
363
+ async function nextSigner(ctx: WaiverRequestContext, ids: SuiteIds, ref: string, after: string[]) {
364
+ const keep = redirect(`${WAIVER_PATH}?ref=${ref}`, [flowCookie(ctx.url, ref, after)]);
365
+ const accessToken = ctx.cookie(retryCookieName(ref)) || ctx.cookie(bookingCookieName(ref));
366
+ if (!accessToken) return keep;
367
+ const call = { locals: ctx.locals, url: ctx.url, request: ctx.request };
368
+ const continued = await post<{ next?: { type?: unknown; url?: unknown; intents?: unknown } }>(
369
+ call,
370
+ ids.eventsPluginId,
371
+ "public/checkout/continue",
372
+ { bookingRef: ref, accessToken },
373
+ );
374
+ return continued.ok ? proceed(ctx, ids, ref, continued.data.next, [], accessToken) : keep;
375
+ }
376
+
357
377
  /** The receipt a finished ceremony hands back (the fragment of `ceremonyRedirect(view)`, §8.2). */
358
378
  function receiptOf(view: SignView): string | undefined {
359
379
  const back = ceremonyRedirect(view) ?? (view.kind === "closed" ? view.returnUrl : undefined);
@@ -382,6 +402,14 @@ async function payOrLand(ctx: WaiverRequestContext, ids: SuiteIds, ref: string,
382
402
  return next?.type === "pay" && isPayUrl(next.url) ? next.url : landed;
383
403
  }
384
404
 
405
+ /**
406
+ * T153: e-sign says the envelope's deadline passed while Events says the booking is still open (its hold was
407
+ * extended without the seat being re-issued). The retry mints fresh intents; a booking that has really
408
+ * ended never gets here, it stops on `ended` first.
409
+ */
410
+ const lapsed = (view: SignView, booking: BookingAnswer | undefined) =>
411
+ booking !== undefined && view.kind === "error" && view.code === "SIGNING_TOKEN_EXPIRED";
412
+
385
413
  async function sign(
386
414
  ctx: WaiverRequestContext,
387
415
  ids: SuiteIds,
@@ -424,6 +452,7 @@ async function sign(
424
452
  if (form) {
425
453
  const [token = "", ...after] = rest;
426
454
  const view = await handleSign(signCtx, token, form);
455
+ if (lapsed(view, booking)) return retry(ctx, ids);
427
456
  if (view.kind === "ready" || view.kind === "error") {
428
457
  return { type: "render", ref, token, remaining: after.length, view, cookies: [], ...withBooking };
429
458
  }
@@ -436,7 +465,7 @@ async function sign(
436
465
  flowCookie(ctx.url, ref, []),
437
466
  retryCookie(ctx.url, ref, undefined),
438
467
  ]);
439
- return redirect(`${WAIVER_PATH}?ref=${ref}`, [flowCookie(ctx.url, ref, after)]);
468
+ return nextSigner(ctx, ids, ref, after);
440
469
  }
441
470
 
442
471
  // A GET only looks. An envelope closed meanwhile (signed in another tab) is forwarded and skipped.
@@ -444,6 +473,7 @@ async function sign(
444
473
  while (rest.length > 0) {
445
474
  const [token = "", ...after] = rest;
446
475
  const view = await handleSign(signCtx, token, null);
476
+ if (lapsed(view, booking)) return retry(ctx, ids);
447
477
  if (view.kind !== "closed") {
448
478
  const cookies = rest.length === tokens.length ? [] : [flowCookie(ctx.url, ref, rest)];
449
479
  return { type: "render", ref, token, remaining: after.length, view, cookies, ...withBooking };
@@ -76,6 +76,11 @@ export async function refreshCatalogue(
76
76
  }
77
77
 
78
78
  const REQUEST_ROUTE = "esign/catalog-request";
79
+ // Events plugins found without the route (Free): not asked again in this isolate, so the host logs one
80
+ // "Route not found" rather than one per Refresh (T154). Installing Pro restarts the site, which clears it.
81
+ const missingRoute = new Set<string>();
82
+ /** Tests only: forget which Events plugins lacked the route. */
83
+ export const forgetMissingRoutes = () => missingRoute.clear();
79
84
 
80
85
  type PrivateRoute = (
81
86
  pluginId: string,
@@ -95,6 +100,7 @@ export async function loadCatalogue(ctx: PublicRouteContext, ids: CatalogueIds):
95
100
  const l = (ctx.locals ?? {}) as { user?: unknown; emdash?: { handlePluginApiRoute?: unknown } };
96
101
  const call = l.emdash?.handlePluginApiRoute;
97
102
  if (typeof call !== "function" || !l.user) return { ok: false, code: "FORBIDDEN" };
103
+ if (missingRoute.has(ids.eventsPluginId)) return { ok: false, code: "NOT_FOUND" };
98
104
  const asked = new Request(new URL(`/_emdash/api/plugins/${ids.eventsPluginId}/${REQUEST_ROUTE}`, ctx.url), {
99
105
  method: "POST",
100
106
  headers: { "content-type": "application/json" },
@@ -109,6 +115,7 @@ export async function loadCatalogue(ctx: PublicRouteContext, ids: CatalogueIds):
109
115
  if (data?.ok === true) return refreshCatalogue(ctx, data.request, ids);
110
116
  // A sandboxed core without the route (Free): the host answers ROUTE_ERROR "… Route not found …".
111
117
  const missing = typeof res?.error?.message === "string" && /\bRoute not found\b/.test(res.error.message);
118
+ if (missing) missingRoute.add(ids.eventsPluginId);
112
119
  const code = missing ? "NOT_FOUND" : (data?.error?.code ?? res?.error?.code);
113
120
  return { ok: false, code: typeof code === "string" ? code : "INTERNAL" };
114
121
  }