@odla-ai/chapter 0.5.0 → 0.8.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.
@@ -94,6 +94,47 @@ function createWorkerContext(options) {
94
94
  // src/worker-routes.ts
95
95
  import { createCrmRoutes } from "@odla-ai/crm";
96
96
 
97
+ // src/clerk.ts
98
+ function clerkInviteRequest(input) {
99
+ return {
100
+ path: "/v1/invitations",
101
+ body: {
102
+ email_address: input.email,
103
+ notify: true,
104
+ ...input.redirectUrl ? { redirect_url: input.redirectUrl } : {}
105
+ }
106
+ };
107
+ }
108
+ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
109
+ const { path, body } = clerkInviteRequest(input);
110
+ const res = await fetchImpl(`https://api.clerk.com${path}`, {
111
+ method: "POST",
112
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
113
+ body: JSON.stringify(body)
114
+ });
115
+ return { ok: res.ok, status: res.status };
116
+ }
117
+ function clerkUserRequest(input) {
118
+ return {
119
+ path: "/v1/users",
120
+ body: {
121
+ email_address: [input.email],
122
+ skip_password_requirement: true,
123
+ ...input.firstName ? { first_name: input.firstName } : {},
124
+ ...input.lastName ? { last_name: input.lastName } : {}
125
+ }
126
+ };
127
+ }
128
+ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
129
+ const { path, body } = clerkUserRequest(input);
130
+ const res = await fetchImpl(`https://api.clerk.com${path}`, {
131
+ method: "POST",
132
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
133
+ body: JSON.stringify(body)
134
+ });
135
+ return { ok: res.ok, status: res.status };
136
+ }
137
+
97
138
  // src/member.ts
98
139
  async function submitApplication(db, chapter, fields, opts) {
99
140
  const app = chapter.application;
@@ -147,21 +188,32 @@ function sharedPersonInput(person) {
147
188
  if (person.linkedin) input.linkedin = person.linkedin;
148
189
  return input;
149
190
  }
150
- async function projectSharedRecord(deps, person) {
151
- const email = person.email.toLowerCase();
152
- const input = sharedPersonInput(person);
191
+ async function upsertPerson(deps, opts) {
192
+ const email = opts.email.toLowerCase();
153
193
  const crmDeps = { crm: deps.crm, db: deps.db, now: deps.now, newId: deps.newId };
154
- const { crm_record } = await deps.db.query({
155
- crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } }
156
- });
194
+ const { crm_record } = await deps.db.query({ crm_record: { $: { where: { type: "person", primaryEmail: email }, limit: 1 } } });
157
195
  const existing = crm_record?.[0];
158
196
  if (existing && typeof existing.id === "string") {
159
- await updateRecord(crmDeps, { id: existing.id, input });
197
+ await updateRecord(crmDeps, { id: existing.id, input: opts.input });
160
198
  return { recordId: existing.id };
161
199
  }
162
- const created = await createRecord(crmDeps, { type: "person", input, mutationId: `share:${person.hubRecordId}` });
200
+ const created = await createRecord(crmDeps, { type: "person", input: opts.input, mutationId: opts.mutationId });
163
201
  return { recordId: created.id };
164
202
  }
203
+ async function projectSharedRecord(deps, person) {
204
+ return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
205
+ }
206
+ async function projectApplicant(deps, applicant) {
207
+ const input = sharedPersonInput({
208
+ email: applicant.email,
209
+ firstName: applicant.firstName,
210
+ lastName: applicant.lastName,
211
+ phone: applicant.phone,
212
+ linkedin: applicant.linkedin,
213
+ hubRecordId: applicant.applicationId
214
+ });
215
+ return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });
216
+ }
165
217
 
166
218
  // src/scheduling.ts
167
219
  var SCHEDULING_DEFAULTS = {
@@ -209,10 +261,6 @@ function resolveScheduling(config) {
209
261
  if (typeof c.summaryTemplate !== "string") fail2("summaryTemplate must be a string");
210
262
  return { ...c, days };
211
263
  }
212
- var BOOKABLE_STATUSES = ["submitted", "paid_pending_vetting", "call_scheduled"];
213
- function canBookFrom(status) {
214
- return BOOKABLE_STATUSES.includes(status);
215
- }
216
264
  function slotWindow(now, windowDays) {
217
265
  return { from: now, to: now + windowDays * 864e5 };
218
266
  }
@@ -291,7 +339,146 @@ function memberApplication(app, meeting, defaultTimezone) {
291
339
  return { ...summary, meetingAt, meetUrl, timezone };
292
340
  }
293
341
 
342
+ // src/email.ts
343
+ function render(template, vars) {
344
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
345
+ }
346
+ function groupVars(group, vars) {
347
+ return {
348
+ ...vars,
349
+ refundPolicyText: group.refundPolicyText ?? "",
350
+ commitmentText: group.commitmentText ?? "",
351
+ normsText: group.normsText ?? ""
352
+ };
353
+ }
354
+ function isAlreadySent(priorRows) {
355
+ return priorRows.some((row) => !row.error);
356
+ }
357
+ function planDelivery(input) {
358
+ const tpl = input.group.emailTemplates?.[input.template];
359
+ if (!tpl) return { deliver: false, reason: "template-missing" };
360
+ if (tpl.enabled === false && !input.force) return { deliver: false, reason: "disabled" };
361
+ const vars = groupVars(input.group, input.vars);
362
+ const isProd = input.envName === "prod";
363
+ const redirect = !isProd && !!input.group.debugEmail;
364
+ const transport = !isProd && !redirect ? "log-only" : input.cloudflareReady ? "cloudflare" : "log-only";
365
+ const to = redirect ? input.group.debugEmail : input.to;
366
+ const subject = (redirect ? "[dev] " : "") + render(tpl.subject, vars);
367
+ const text = redirect ? `(dev redirect; original recipient: ${input.to})
368
+
369
+ ` + render(tpl.text, vars) : render(tpl.text, vars);
370
+ return { deliver: true, transport, to, subject, text, redirected: redirect };
371
+ }
372
+
373
+ // src/notify.ts
374
+ async function sendTemplated(deps, input) {
375
+ const { emailLog } = await deps.db.query({ emailLog: { $: { where: { dedupeKey: input.dedupeKey } } } });
376
+ const prior = Array.isArray(emailLog) ? emailLog : [];
377
+ if (isAlreadySent(prior)) return { sent: true, reason: "already-sent" };
378
+ const cloudflareReady = Boolean(deps.sender && deps.from);
379
+ const decision = planDelivery({
380
+ envName: deps.envName,
381
+ group: input.group,
382
+ template: input.template,
383
+ to: input.to,
384
+ vars: input.vars,
385
+ cloudflareReady,
386
+ force: input.force
387
+ });
388
+ if (!decision.deliver) return { sent: false, reason: decision.reason };
389
+ let error;
390
+ let messageId;
391
+ if (decision.transport === "cloudflare" && deps.sender && deps.from) {
392
+ try {
393
+ const res = await deps.sender.send({
394
+ from: deps.from,
395
+ to: [decision.to],
396
+ subject: decision.subject,
397
+ text: decision.text,
398
+ replyTo: input.group.replyTo
399
+ });
400
+ messageId = res.messageId;
401
+ } catch (e) {
402
+ error = e instanceof Error ? e.message : String(e);
403
+ }
404
+ }
405
+ const id = deps.newId();
406
+ const row = {
407
+ id,
408
+ groupId: input.group.id,
409
+ to: decision.to,
410
+ template: input.template,
411
+ subject: decision.subject,
412
+ body: decision.text,
413
+ transport: decision.transport,
414
+ redirected: decision.redirected,
415
+ dedupeKey: input.dedupeKey,
416
+ sentAt: deps.now(),
417
+ ...input.applicationId ? { applicationId: input.applicationId } : {},
418
+ ...messageId ? { messageId } : {},
419
+ ...error ? { error } : {}
420
+ };
421
+ await deps.db.transact([{ t: "update", ns: "emailLog", id, attrs: row }], error ? void 0 : { mutationId: `email:${input.dedupeKey}` });
422
+ return error ? { sent: false, reason: error } : { sent: true };
423
+ }
424
+ function emailGroupFrom(row) {
425
+ const str = (v) => typeof v === "string" ? v : void 0;
426
+ const templates = row.emailTemplates && typeof row.emailTemplates === "object" ? row.emailTemplates : {};
427
+ return {
428
+ id: String(row.id),
429
+ name: String(row.name ?? ""),
430
+ replyTo: str(row.replyTo) ?? "",
431
+ debugEmail: str(row.debugEmail),
432
+ refundPolicyText: str(row.refundPolicyText),
433
+ commitmentText: str(row.commitmentText),
434
+ normsText: str(row.normsText),
435
+ emailTemplates: templates
436
+ };
437
+ }
438
+
294
439
  // src/worker-routes.ts
440
+ async function provisionApplicant(db, chapter, applicationId, fields) {
441
+ const email = typeof fields.email === "string" ? fields.email : "";
442
+ if (!email) return;
443
+ const s = (v) => typeof v === "string" ? v : void 0;
444
+ try {
445
+ await projectApplicant(
446
+ { crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
447
+ { applicationId, email, firstName: s(fields.firstName), lastName: s(fields.lastName), phone: s(fields.phone), linkedin: s(fields.linkedin) }
448
+ );
449
+ } catch {
450
+ }
451
+ if (chapter.account !== "none") {
452
+ try {
453
+ const secret = await getVaultSecret(db, "clerk_secret_key");
454
+ if (secret) {
455
+ if (chapter.account === "create") await createClerkUser(secret, { email, firstName: s(fields.firstName), lastName: s(fields.lastName) });
456
+ else await createClerkInvitation(secret, { email });
457
+ }
458
+ } catch {
459
+ }
460
+ }
461
+ }
462
+ async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
463
+ try {
464
+ const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
465
+ const group = Array.isArray(groups) ? groups[0] : void 0;
466
+ if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
467
+ const s = (v) => typeof v === "string" ? v : "";
468
+ await sendTemplated(
469
+ { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
470
+ {
471
+ group: emailGroupFrom(group),
472
+ template: "adminNotification",
473
+ to: group.notificationEmail,
474
+ vars: { firstName: s(fields.firstName), lastName: s(fields.lastName), email: s(fields.email), phone: s(fields.phone), state: s(fields.state) },
475
+ dedupeKey: `apply:${applicationId}:admin`,
476
+ applicationId
477
+ }
478
+ );
479
+ } catch {
480
+ }
481
+ }
295
482
  async function memberSessionApplication(db, chapterId, email) {
296
483
  const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: "desc" }, limit: 1 } } })).applications;
297
484
  const app = Array.isArray(apps) ? apps[0] : void 0;
@@ -303,6 +490,7 @@ async function memberSessionApplication(db, chapterId, email) {
303
490
  const timezone = resolveScheduling(group?.schedulingJson).timezone;
304
491
  return memberApplication(app, meeting, timezone);
305
492
  }
493
+ var handleHealth = async (_req, url) => url.pathname === "/api/health" ? json({ ok: true }) : null;
306
494
  var handleConfig = async (_req, url, env, ctx) => {
307
495
  if (url.pathname !== "/api/config") return null;
308
496
  try {
@@ -392,13 +580,18 @@ var handleMember = async (req, url, env, ctx) => {
392
580
  return json({ error: "invalid JSON body" }, 400);
393
581
  }
394
582
  const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
395
- const result = await submitApplication(ctx.makeDb(env), chapter, parsed, {
583
+ const db = ctx.makeDb(env);
584
+ const result = await submitApplication(db, chapter, parsed, {
396
585
  submissionId,
397
586
  groupId: chapter.id,
398
587
  now: Date.now(),
399
588
  newId: () => crypto.randomUUID()
400
589
  });
401
590
  if (!result.ok) return json({ error: result.error }, 400);
591
+ if (!result.duplicate) {
592
+ await notifyAdminOfApplication(db, env, chapter.id, result.id, parsed);
593
+ await provisionApplicant(db, chapter, result.id, parsed);
594
+ }
402
595
  return json({ id: result.id, duplicate: result.duplicate, status: result.status });
403
596
  }
404
597
  return null;
@@ -406,6 +599,13 @@ var handleMember = async (req, url, env, ctx) => {
406
599
 
407
600
  // src/worker-routes-schedule.ts
408
601
  import { computeBookableSlots, initCalendar } from "@odla-ai/calendar";
602
+
603
+ // src/pipeline.ts
604
+ function canBook(status, p) {
605
+ return p.bookableFrom.includes(status);
606
+ }
607
+
608
+ // src/worker-routes-schedule.ts
409
609
  function errCode(err) {
410
610
  if (err && typeof err === "object") {
411
611
  const code = err.code;
@@ -447,7 +647,7 @@ async function bookSlot(req, env, ctx) {
447
647
  const app = await firstRow(db, "applications", { where: { id: applicationId }, limit: 1 });
448
648
  if (!app) return json({ error: "not found" }, 404);
449
649
  const status = String(app.status ?? "");
450
- if (!canBookFrom(status)) return json({ error: `cannot book from status "${status}"` }, 409);
650
+ if (!canBook(status, ctx.chapter.pipeline)) return json({ error: `cannot book from status "${status}"` }, 409);
451
651
  const group = await firstRow(db, "groups", { where: { id: String(app.groupId ?? ctx.chapter.id) }, limit: 1 });
452
652
  if (!group) return json({ error: "group not found" }, 500);
453
653
  const cfg = resolveScheduling(group.schedulingJson);
@@ -509,6 +709,12 @@ async function bookSlot(req, env, ctx) {
509
709
  }
510
710
  const appOp = { t: "update", ns: "applications", id: applicationId, attrs: applicationBookingUpdate(status, startAt, htmlLink) };
511
711
  await db.transact([meetingOp, appOp]);
712
+ if (typeof app.email === "string" && app.email) {
713
+ await sendTemplated(
714
+ { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
715
+ { group: emailGroupFrom(group), template: "prepEmail", to: app.email, vars: { firstName: String(app.firstName ?? "") }, dedupeKey: `prep:${applicationId}`, applicationId }
716
+ ).catch(() => void 0);
717
+ }
512
718
  return json({ ok: true, startAt, endAt, meetUrl, rescheduled: decision.reschedule });
513
719
  }
514
720
  var handleSchedule = async (req, url, env, ctx) => {
@@ -744,6 +950,25 @@ async function findApplication(db, event) {
744
950
  }
745
951
  return void 0;
746
952
  }
953
+ async function notifyPaymentConfirmed(db, env, eventId, app) {
954
+ try {
955
+ if (typeof app.email !== "string" || !app.email) return;
956
+ const group = await firstRow2(db, "groups", { where: { id: String(app.groupId ?? "") }, limit: 1 });
957
+ if (!group) return;
958
+ await sendTemplated(
959
+ { db, envName: env.ODLA_ENV, sender: env.SEND_EMAIL, from: env.EMAIL_FROM, now: () => Date.now(), newId: () => crypto.randomUUID() },
960
+ {
961
+ group: emailGroupFrom(group),
962
+ template: "paymentConfirmation",
963
+ to: app.email,
964
+ vars: { firstName: typeof app.firstName === "string" ? app.firstName : "" },
965
+ dedupeKey: `${eventId}:confirm`,
966
+ applicationId: String(app.id)
967
+ }
968
+ );
969
+ } catch {
970
+ }
971
+ }
747
972
  function webhookPatch(event, status) {
748
973
  switch (event.kind) {
749
974
  case "first_payment":
@@ -815,6 +1040,7 @@ async function ingestWebhook(req, env, ctx) {
815
1040
  if (Object.keys(patch).length) {
816
1041
  await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: patch }], { mutationId: webhookMutationId(eventId) });
817
1042
  }
1043
+ if (event.kind === "first_payment") await notifyPaymentConfirmed(db, env, eventId, app);
818
1044
  return json({ ok: true });
819
1045
  }
820
1046
  async function refundApplication(req, url, env, ctx) {
@@ -851,14 +1077,139 @@ var handlePayments = async (req, url, env, ctx) => {
851
1077
  return null;
852
1078
  };
853
1079
 
1080
+ // src/worker-routes-admin.ts
1081
+ import { initCalendar as initCalendar2 } from "@odla-ai/calendar";
1082
+
1083
+ // src/reconcile.ts
1084
+ function isReconcilable(meeting, now) {
1085
+ return meeting.status === "scheduled" && Boolean(meeting.googleEventId) && (meeting.startAt ?? 0) > now - 36e5;
1086
+ }
1087
+ function reconcileMeetings(meetings, events, now) {
1088
+ const byEvent = new Map(events.map((e) => [e.eventId, e]));
1089
+ const decisions = [];
1090
+ for (const m of meetings) {
1091
+ if (!isReconcilable(m, now) || !m.googleEventId) continue;
1092
+ const g = byEvent.get(m.googleEventId);
1093
+ if (!g || g.status === "cancelled") {
1094
+ decisions.push({
1095
+ meetingId: m.id,
1096
+ applicationId: m.applicationId,
1097
+ kind: "cancelled",
1098
+ meetingPatch: { status: "cancelled", drift: "none", adoptedFromGoogleAt: now },
1099
+ applicationPatch: { meetingAt: 0, meetingLink: "" }
1100
+ });
1101
+ } else if (g.startAt !== void 0 && g.startAt !== m.startAt) {
1102
+ const duration = (m.endAt ?? 0) - (m.startAt ?? 0);
1103
+ decisions.push({
1104
+ meetingId: m.id,
1105
+ applicationId: m.applicationId,
1106
+ kind: "moved",
1107
+ meetingPatch: { startAt: g.startAt, endAt: g.endAt ?? g.startAt + duration, drift: "none", adoptedFromGoogleAt: now },
1108
+ applicationPatch: { meetingAt: g.startAt }
1109
+ });
1110
+ }
1111
+ }
1112
+ return decisions;
1113
+ }
1114
+
1115
+ // src/worker-routes-admin.ts
1116
+ async function adminGroup(req, env, ctx, url) {
1117
+ const rawDb = ctx.makeDb(env);
1118
+ const u = await ctx.verifyUser(req, env);
1119
+ if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
1120
+ const db = rawDb;
1121
+ const groupId = url.searchParams.get("group") ?? ctx.chapter.id;
1122
+ const group = (await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } })).groups?.[0];
1123
+ if (!group) return json({ error: "not found" }, 404);
1124
+ return { db, group };
1125
+ }
1126
+ var handleAdminScheduling = async (req, url, env, ctx) => {
1127
+ if (url.pathname !== "/api/admin/scheduling" || req.method !== "GET" && req.method !== "PUT") return null;
1128
+ const got = await adminGroup(req, env, ctx, url);
1129
+ if (got instanceof Response) return got;
1130
+ const { db, group } = got;
1131
+ if (req.method === "GET") {
1132
+ return json({ scheduling: resolveScheduling(group.schedulingJson) });
1133
+ }
1134
+ let body;
1135
+ try {
1136
+ body = JSON.parse(await req.text());
1137
+ } catch {
1138
+ return json({ error: "invalid JSON body" }, 400);
1139
+ }
1140
+ let resolved;
1141
+ try {
1142
+ resolved = resolveScheduling(body);
1143
+ } catch (e) {
1144
+ return json({ error: e instanceof Error ? e.message : "invalid scheduling config" }, 400);
1145
+ }
1146
+ await db.transact([{ t: "update", ns: "groups", id: String(group.id), attrs: { schedulingJson: resolved } }]);
1147
+ return json({ scheduling: resolved });
1148
+ };
1149
+ async function upcomingEvents(env) {
1150
+ const cal = initCalendar2({ appId: env.ODLA_APP_ID, env: env.ODLA_ENV, adminToken: env.ODLA_API_KEY, endpoint: env.ODLA_PLATFORM });
1151
+ const res = await cal.availability.upcoming();
1152
+ return res.events.map((e) => ({ eventId: e.eventId, status: e.status, startAt: e.startAt, endAt: e.endAt }));
1153
+ }
1154
+ function toReconcile(rows) {
1155
+ return rows.map((m) => ({
1156
+ id: String(m.id),
1157
+ applicationId: String(m.applicationId),
1158
+ googleEventId: typeof m.googleEventId === "string" ? m.googleEventId : null,
1159
+ status: String(m.status ?? ""),
1160
+ startAt: typeof m.startAt === "number" ? m.startAt : null,
1161
+ endAt: typeof m.endAt === "number" ? m.endAt : null
1162
+ }));
1163
+ }
1164
+ var handleAdminMeetings = async (req, url, env, ctx) => {
1165
+ if (req.method !== "GET" || url.pathname !== "/api/admin/meetings") return null;
1166
+ const rawDb = ctx.makeDb(env);
1167
+ const u = await ctx.verifyUser(req, env);
1168
+ if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
1169
+ const db = rawDb;
1170
+ const query = await db.query({ meetings: { $: { where: { status: "scheduled" }, order: { startAt: "asc" }, limit: 500 } } });
1171
+ const rows = Array.isArray(query.meetings) ? query.meetings : [];
1172
+ let decisions = [];
1173
+ try {
1174
+ decisions = reconcileMeetings(toReconcile(rows), await upcomingEvents(env), Date.now());
1175
+ } catch {
1176
+ }
1177
+ const patched = /* @__PURE__ */ new Map();
1178
+ for (const d of decisions) {
1179
+ const ops = [
1180
+ { t: "update", ns: "meetings", id: d.meetingId, attrs: d.meetingPatch },
1181
+ { t: "update", ns: "applications", id: d.applicationId, attrs: d.applicationPatch }
1182
+ ];
1183
+ try {
1184
+ await db.transact(ops);
1185
+ patched.set(d.meetingId, d.meetingPatch);
1186
+ } catch {
1187
+ }
1188
+ }
1189
+ const meetings = rows.map((m) => ({ ...m, ...patched.get(String(m.id)) ?? {} })).filter((m) => m.status === "scheduled");
1190
+ return json({ meetings, adopted: decisions.length });
1191
+ };
1192
+
854
1193
  // src/worker.ts
855
- var ROUTES = [handleConfig, handleMe, handleCrm, handleNetworkShared, handleMember, handleSchedule, handlePayments];
1194
+ var BUILTIN_ROUTES = [
1195
+ handleHealth,
1196
+ handleConfig,
1197
+ handleMe,
1198
+ handleCrm,
1199
+ handleNetworkShared,
1200
+ handleMember,
1201
+ handleSchedule,
1202
+ handlePayments,
1203
+ handleAdminMeetings,
1204
+ handleAdminScheduling
1205
+ ];
856
1206
  function chapterWorker(options) {
857
1207
  const ctx = createWorkerContext(options);
1208
+ const routes = [...options.routes ?? [], ...BUILTIN_ROUTES];
858
1209
  return {
859
1210
  async fetch(req, env) {
860
1211
  const url = new URL(req.url);
861
- for (const route of ROUTES) {
1212
+ for (const route of routes) {
862
1213
  const res = await route(req, url, env, ctx);
863
1214
  if (res) return res;
864
1215
  }
@@ -867,6 +1218,7 @@ function chapterWorker(options) {
867
1218
  };
868
1219
  }
869
1220
  export {
870
- chapterWorker
1221
+ chapterWorker,
1222
+ createWorkerContext
871
1223
  };
872
1224
  //# sourceMappingURL=index.js.map