@semics-tech/emdash-events-suite 1.0.0-rc.2 → 1.0.0-rc.4
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.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
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.
|
|
26
|
-
"@semics-tech/emdash-events": "1.0.0-rc.
|
|
25
|
+
"@semics-tech/emdash-esign": "1.0.0-rc.4",
|
|
26
|
+
"@semics-tech/emdash-events": "1.0.0-rc.4"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"emdash": ">=1.2.0 <2"
|
|
@@ -10,7 +10,7 @@ import SigningCeremony from "@semics-tech/emdash-esign/astro/SigningCeremony.ast
|
|
|
10
10
|
import { ceremonyCsp } from "@semics-tech/emdash-esign/astro/lib/csp";
|
|
11
11
|
import { crossSiteResponse } from "@semics-tech/emdash-events/astro/lib/checkout-form";
|
|
12
12
|
|
|
13
|
-
import { handleWaiverRequest } from "./lib/flow";
|
|
13
|
+
import { handleWaiverRequest, leavesSite } from "./lib/flow";
|
|
14
14
|
import { WAIVER_MESSAGES, WAIVER_PATH } from "./lib/form";
|
|
15
15
|
import { waiverSlot } from "./lib/slot";
|
|
16
16
|
|
|
@@ -23,7 +23,9 @@ const outcome = await handleWaiverRequest(
|
|
|
23
23
|
if (outcome.type === "forbidden") return crossSiteResponse();
|
|
24
24
|
const cookies = outcome.type === "redirect" || outcome.type === "render" ? outcome.cookies : [];
|
|
25
25
|
for (const cookie of cookies) Astro.cookies.set(cookie.name, cookie.value, cookie.options);
|
|
26
|
-
if (outcome.type === "redirect") return Astro.redirect(outcome.location, 303);
|
|
26
|
+
if (outcome.type === "redirect" && !leavesSite(outcome.location, Astro.url)) return Astro.redirect(outcome.location, 303);
|
|
27
|
+
// The payment page is another site, which the CSP below stops a post redirecting to (T155): a page goes there instead.
|
|
28
|
+
const payment = outcome.type === "redirect" ? outcome.location : undefined;
|
|
27
29
|
|
|
28
30
|
Astro.response.headers.set("Referrer-Policy", "no-referrer");
|
|
29
31
|
Astro.response.headers.set("Cache-Control", "private, no-store");
|
|
@@ -54,9 +56,16 @@ const signer = render?.view.kind === "ready" ? render.view.session.signer : unde
|
|
|
54
56
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
55
57
|
<meta name="robots" content="noindex" />
|
|
56
58
|
<meta name="referrer" content="no-referrer" />
|
|
57
|
-
<title>Sign the waiver</title>
|
|
59
|
+
<title>{payment ? "Continue to payment" : "Sign the waiver"}</title>
|
|
60
|
+
{payment && <meta http-equiv="refresh" content={`0;url=${payment}`} data-testid="waiver-pay-refresh" />}
|
|
58
61
|
</head>
|
|
59
62
|
<body>
|
|
63
|
+
{payment ? (
|
|
64
|
+
<main class="semics-esign semics-sign" data-testid="waiver-page" data-step="pay">
|
|
65
|
+
<h1>Continue to payment</h1>
|
|
66
|
+
<p><a href={payment} data-testid="waiver-pay-link">Go to the payment page</a></p>
|
|
67
|
+
</main>
|
|
68
|
+
) : (
|
|
60
69
|
<main class="semics-esign semics-sign" data-testid="waiver-page" data-step={render ? "sign" : "error"}>
|
|
61
70
|
<h1>Sign the waiver</h1>
|
|
62
71
|
{error && <p role="alert" data-testid="waiver-error">{error}</p>}
|
|
@@ -89,5 +98,6 @@ const signer = render?.view.kind === "ready" ? render.view.session.signer : unde
|
|
|
89
98
|
)}
|
|
90
99
|
{ref && <p><a href={`/booking?ref=${ref}`}>Back to your booking</a></p>}
|
|
91
100
|
</main>
|
|
101
|
+
)}
|
|
92
102
|
</body>
|
|
93
103
|
</html>
|
package/src/astro/lib/flow.ts
CHANGED
|
@@ -100,6 +100,14 @@ export interface BookingAnswer {
|
|
|
100
100
|
waiver?: { refs: string[]; voids?: Record<string, string> };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Whether a redirect leaves this site (T155). A form on the signing page cannot be redirected there:
|
|
105
|
+
* its CSP's `form-action 'self'` also covers where the post redirects, so the browser stops on the page.
|
|
106
|
+
*/
|
|
107
|
+
export function leavesSite(location: string, site: URL): boolean {
|
|
108
|
+
return new URL(location, site).origin !== site.origin;
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
/** Booking statuses with nothing left to sign for: the places are gone (T151). */
|
|
104
112
|
const ENDED = ["expired", "cancelled", "declined", "refunded", "refunded_unavailable"];
|
|
105
113
|
|
|
@@ -354,6 +362,26 @@ async function retry(ctx: WaiverRequestContext, ids: SuiteIds): Promise<WaiverOu
|
|
|
354
362
|
return proceed(ctx, ids, ref, continued.data.next, [], accessToken);
|
|
355
363
|
}
|
|
356
364
|
|
|
365
|
+
/**
|
|
366
|
+
* T153: Pro's intents carry the hold as `signBy`, and a receipt extends the hold. Continue once more so
|
|
367
|
+
* the seats still to sign get intents with the new hold: e-sign moves each envelope's deadline and rotates
|
|
368
|
+
* its token. Refused (a lapsed licence, say), the next signer goes on with the token already held, whose
|
|
369
|
+
* envelope still runs to the hold it was issued for.
|
|
370
|
+
*/
|
|
371
|
+
async function nextSigner(ctx: WaiverRequestContext, ids: SuiteIds, ref: string, after: string[]) {
|
|
372
|
+
const keep = redirect(`${WAIVER_PATH}?ref=${ref}`, [flowCookie(ctx.url, ref, after)]);
|
|
373
|
+
const accessToken = ctx.cookie(retryCookieName(ref)) || ctx.cookie(bookingCookieName(ref));
|
|
374
|
+
if (!accessToken) return keep;
|
|
375
|
+
const call = { locals: ctx.locals, url: ctx.url, request: ctx.request };
|
|
376
|
+
const continued = await post<{ next?: { type?: unknown; url?: unknown; intents?: unknown } }>(
|
|
377
|
+
call,
|
|
378
|
+
ids.eventsPluginId,
|
|
379
|
+
"public/checkout/continue",
|
|
380
|
+
{ bookingRef: ref, accessToken },
|
|
381
|
+
);
|
|
382
|
+
return continued.ok ? proceed(ctx, ids, ref, continued.data.next, [], accessToken) : keep;
|
|
383
|
+
}
|
|
384
|
+
|
|
357
385
|
/** The receipt a finished ceremony hands back (the fragment of `ceremonyRedirect(view)`, §8.2). */
|
|
358
386
|
function receiptOf(view: SignView): string | undefined {
|
|
359
387
|
const back = ceremonyRedirect(view) ?? (view.kind === "closed" ? view.returnUrl : undefined);
|
|
@@ -382,6 +410,14 @@ async function payOrLand(ctx: WaiverRequestContext, ids: SuiteIds, ref: string,
|
|
|
382
410
|
return next?.type === "pay" && isPayUrl(next.url) ? next.url : landed;
|
|
383
411
|
}
|
|
384
412
|
|
|
413
|
+
/**
|
|
414
|
+
* T153: e-sign says the envelope's deadline passed while Events says the booking is still open (its hold was
|
|
415
|
+
* extended without the seat being re-issued). The retry mints fresh intents; a booking that has really
|
|
416
|
+
* ended never gets here, it stops on `ended` first.
|
|
417
|
+
*/
|
|
418
|
+
const lapsed = (view: SignView, booking: BookingAnswer | undefined) =>
|
|
419
|
+
booking !== undefined && view.kind === "error" && view.code === "SIGNING_TOKEN_EXPIRED";
|
|
420
|
+
|
|
385
421
|
async function sign(
|
|
386
422
|
ctx: WaiverRequestContext,
|
|
387
423
|
ids: SuiteIds,
|
|
@@ -424,6 +460,7 @@ async function sign(
|
|
|
424
460
|
if (form) {
|
|
425
461
|
const [token = "", ...after] = rest;
|
|
426
462
|
const view = await handleSign(signCtx, token, form);
|
|
463
|
+
if (lapsed(view, booking)) return retry(ctx, ids);
|
|
427
464
|
if (view.kind === "ready" || view.kind === "error") {
|
|
428
465
|
return { type: "render", ref, token, remaining: after.length, view, cookies: [], ...withBooking };
|
|
429
466
|
}
|
|
@@ -436,7 +473,7 @@ async function sign(
|
|
|
436
473
|
flowCookie(ctx.url, ref, []),
|
|
437
474
|
retryCookie(ctx.url, ref, undefined),
|
|
438
475
|
]);
|
|
439
|
-
return
|
|
476
|
+
return nextSigner(ctx, ids, ref, after);
|
|
440
477
|
}
|
|
441
478
|
|
|
442
479
|
// A GET only looks. An envelope closed meanwhile (signed in another tab) is forwarded and skipped.
|
|
@@ -444,6 +481,7 @@ async function sign(
|
|
|
444
481
|
while (rest.length > 0) {
|
|
445
482
|
const [token = "", ...after] = rest;
|
|
446
483
|
const view = await handleSign(signCtx, token, null);
|
|
484
|
+
if (lapsed(view, booking)) return retry(ctx, ids);
|
|
447
485
|
if (view.kind !== "closed") {
|
|
448
486
|
const cookies = rest.length === tokens.length ? [] : [flowCookie(ctx.url, ref, rest)];
|
|
449
487
|
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
|
}
|