@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 +3 -3
- package/src/astro/lib/flow.ts +31 -1
- package/src/astro/lib/templates.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semics-tech/emdash-events-suite",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
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-
|
|
26
|
-
"@semics-tech/emdash-
|
|
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"
|
package/src/astro/lib/flow.ts
CHANGED
|
@@ -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
|
|
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
|
}
|