@odla-ai/chapter 0.22.0 → 0.23.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.
- package/README.md +112 -41
- package/dist/chunk-NTZSXAFT.js +411 -0
- package/dist/chunk-NTZSXAFT.js.map +1 -0
- package/dist/{chunk-ILGUJXSB.js → chunk-OYXNI6VR.js} +275 -93
- package/dist/chunk-OYXNI6VR.js.map +1 -0
- package/dist/{chunk-IWW5VAAC.js → chunk-VCC4S6JU.js} +230 -202
- package/dist/chunk-VCC4S6JU.js.map +1 -0
- package/dist/{types-CgWUQ2MH.d.ts → copy-context-pDogTR4x.d.ts} +78 -20
- package/dist/index.cjs +322 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +90 -24
- package/dist/index.d.ts +90 -24
- package/dist/index.js +322 -3
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +31 -28
- package/dist/ui/admin/index.js +8 -3
- package/dist/ui/index.d.ts +3 -3
- package/dist/ui/index.js +11 -5
- package/dist/ui/member/index.d.ts +128 -12
- package/dist/ui/member/index.js +10 -4
- package/dist/worker/index.cjs +180 -130
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +67 -19
- package/dist/worker/index.d.ts +67 -19
- package/dist/worker/index.js +180 -130
- package/dist/worker/index.js.map +1 -1
- package/package.json +11 -11
- package/runbooks/adopt-existing.md +23 -5
- package/runbooks/greenfield.md +95 -15
- package/dist/chunk-ILGUJXSB.js.map +0 -1
- package/dist/chunk-IWW5VAAC.js.map +0 -1
- package/dist/chunk-VZPBTTO3.js +0 -91
- package/dist/chunk-VZPBTTO3.js.map +0 -1
package/dist/worker/index.js
CHANGED
|
@@ -111,68 +111,6 @@ function createWorkerContext(options) {
|
|
|
111
111
|
// src/worker-routes.ts
|
|
112
112
|
import { createCrmRoutes } from "@odla-ai/crm";
|
|
113
113
|
|
|
114
|
-
// src/clerk.ts
|
|
115
|
-
var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
|
|
116
|
-
function clerkInviteRequest(input) {
|
|
117
|
-
return {
|
|
118
|
-
path: "/v1/invitations",
|
|
119
|
-
body: {
|
|
120
|
-
email_address: input.email,
|
|
121
|
-
notify: true,
|
|
122
|
-
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
|
|
123
|
-
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
128
|
-
const { path, body } = clerkInviteRequest(input);
|
|
129
|
-
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
130
|
-
method: "POST",
|
|
131
|
-
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
132
|
-
body: JSON.stringify(body)
|
|
133
|
-
});
|
|
134
|
-
return res.ok ? { ok: true, status: res.status } : heal(res.status);
|
|
135
|
-
}
|
|
136
|
-
function clerkUserRequest(input) {
|
|
137
|
-
return {
|
|
138
|
-
path: "/v1/users",
|
|
139
|
-
body: {
|
|
140
|
-
email_address: [input.email],
|
|
141
|
-
skip_password_requirement: true,
|
|
142
|
-
...input.firstName ? { first_name: input.firstName } : {},
|
|
143
|
-
...input.lastName ? { last_name: input.lastName } : {},
|
|
144
|
-
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
|
|
149
|
-
const auth = { authorization: `Bearer ${secretKey}` };
|
|
150
|
-
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
151
|
-
if (!found.ok) return false;
|
|
152
|
-
const users = await found.json().catch(() => null);
|
|
153
|
-
const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
154
|
-
if (!id) return false;
|
|
155
|
-
const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id}/metadata`, {
|
|
156
|
-
method: "PATCH",
|
|
157
|
-
headers: { ...auth, "content-type": "application/json" },
|
|
158
|
-
body: JSON.stringify({ public_metadata: publicMetadata })
|
|
159
|
-
});
|
|
160
|
-
return patched.ok;
|
|
161
|
-
}
|
|
162
|
-
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
163
|
-
const { path, body } = clerkUserRequest(input);
|
|
164
|
-
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
165
|
-
method: "POST",
|
|
166
|
-
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
167
|
-
body: JSON.stringify(body)
|
|
168
|
-
});
|
|
169
|
-
if (res.ok) return { ok: true, status: res.status };
|
|
170
|
-
const healed = heal(res.status);
|
|
171
|
-
if (!healed.existed || !input.publicMetadata) return healed;
|
|
172
|
-
const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
|
|
173
|
-
return { ...healed, refreshed };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
114
|
// src/member.ts
|
|
177
115
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
178
116
|
function isValidEmail(value) {
|
|
@@ -188,7 +126,7 @@ function hasDisclaimerAck(fields) {
|
|
|
188
126
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
189
127
|
function applicantProfile(chapter, fields) {
|
|
190
128
|
const app = chapter.application;
|
|
191
|
-
const allowed = (f) => app.profileFields
|
|
129
|
+
const allowed = (f) => app.profileFields.includes(f);
|
|
192
130
|
const profile = {};
|
|
193
131
|
for (const f of [...app.required, ...app.optional]) {
|
|
194
132
|
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
@@ -506,6 +444,19 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
506
444
|
};
|
|
507
445
|
}
|
|
508
446
|
|
|
447
|
+
// src/pipeline.ts
|
|
448
|
+
function canTransition(from, to, p) {
|
|
449
|
+
const fi = p.stages.indexOf(from);
|
|
450
|
+
const ti = p.stages.indexOf(to);
|
|
451
|
+
return fi >= 0 && ti >= 0 && ti >= fi;
|
|
452
|
+
}
|
|
453
|
+
function canBook(status, p) {
|
|
454
|
+
return p.bookableFrom.includes(status);
|
|
455
|
+
}
|
|
456
|
+
function canApprove(status, p) {
|
|
457
|
+
return p.approvableFrom.includes(status);
|
|
458
|
+
}
|
|
459
|
+
|
|
509
460
|
// src/session.ts
|
|
510
461
|
function applicationSummary(app) {
|
|
511
462
|
return {
|
|
@@ -538,6 +489,68 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
538
489
|
return { ...summary, meetingAt, meetUrl, timezone };
|
|
539
490
|
}
|
|
540
491
|
|
|
492
|
+
// src/clerk.ts
|
|
493
|
+
var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
|
|
494
|
+
function clerkInviteRequest(input) {
|
|
495
|
+
return {
|
|
496
|
+
path: "/v1/invitations",
|
|
497
|
+
body: {
|
|
498
|
+
email_address: input.email,
|
|
499
|
+
notify: true,
|
|
500
|
+
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
|
|
501
|
+
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
506
|
+
const { path, body } = clerkInviteRequest(input);
|
|
507
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
510
|
+
body: JSON.stringify(body)
|
|
511
|
+
});
|
|
512
|
+
return res.ok ? { ok: true, status: res.status } : heal(res.status);
|
|
513
|
+
}
|
|
514
|
+
function clerkUserRequest(input) {
|
|
515
|
+
return {
|
|
516
|
+
path: "/v1/users",
|
|
517
|
+
body: {
|
|
518
|
+
email_address: [input.email],
|
|
519
|
+
skip_password_requirement: true,
|
|
520
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
521
|
+
...input.lastName ? { last_name: input.lastName } : {},
|
|
522
|
+
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
|
|
527
|
+
const auth = { authorization: `Bearer ${secretKey}` };
|
|
528
|
+
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
529
|
+
if (!found.ok) return false;
|
|
530
|
+
const users = await found.json().catch(() => null);
|
|
531
|
+
const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
532
|
+
if (!id) return false;
|
|
533
|
+
const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id}/metadata`, {
|
|
534
|
+
method: "PATCH",
|
|
535
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
536
|
+
body: JSON.stringify({ public_metadata: publicMetadata })
|
|
537
|
+
});
|
|
538
|
+
return patched.ok;
|
|
539
|
+
}
|
|
540
|
+
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
541
|
+
const { path, body } = clerkUserRequest(input);
|
|
542
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
543
|
+
method: "POST",
|
|
544
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
545
|
+
body: JSON.stringify(body)
|
|
546
|
+
});
|
|
547
|
+
if (res.ok) return { ok: true, status: res.status };
|
|
548
|
+
const healed = heal(res.status);
|
|
549
|
+
if (!healed.existed || !input.publicMetadata) return healed;
|
|
550
|
+
const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
|
|
551
|
+
return { ...healed, refreshed };
|
|
552
|
+
}
|
|
553
|
+
|
|
541
554
|
// src/email.ts
|
|
542
555
|
function render(template, vars) {
|
|
543
556
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -641,36 +654,47 @@ function emailGroupFrom(row) {
|
|
|
641
654
|
};
|
|
642
655
|
}
|
|
643
656
|
|
|
644
|
-
// src/worker-
|
|
657
|
+
// src/worker-provisioning.ts
|
|
645
658
|
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
646
659
|
const email = typeof fields.email === "string" ? fields.email : "";
|
|
647
660
|
if (!email) return;
|
|
648
|
-
const
|
|
661
|
+
const stringValue = (value) => typeof value === "string" ? value : void 0;
|
|
649
662
|
const extra = {};
|
|
650
|
-
for (const
|
|
651
|
-
if (fields[
|
|
663
|
+
for (const field of chapter.application.crmFields) {
|
|
664
|
+
if (fields[field] !== void 0) extra[field] = fields[field];
|
|
652
665
|
}
|
|
653
666
|
try {
|
|
654
667
|
await projectApplicant(
|
|
655
668
|
{ crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
656
|
-
{
|
|
669
|
+
{
|
|
670
|
+
applicationId,
|
|
671
|
+
email,
|
|
672
|
+
firstName: stringValue(fields.firstName),
|
|
673
|
+
lastName: stringValue(fields.lastName),
|
|
674
|
+
phone: stringValue(fields.phone),
|
|
675
|
+
linkedin: stringValue(fields.linkedin),
|
|
676
|
+
extra
|
|
677
|
+
}
|
|
657
678
|
);
|
|
658
679
|
} catch {
|
|
659
680
|
}
|
|
660
|
-
if (chapter.account
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
681
|
+
if (chapter.account === "none") return;
|
|
682
|
+
try {
|
|
683
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
684
|
+
if (!secret) return;
|
|
685
|
+
const profile = applicantProfile(chapter, fields);
|
|
686
|
+
const publicMetadata = { applicationId, ...profile ? { profile } : {} };
|
|
687
|
+
if (chapter.account === "create") {
|
|
688
|
+
await createClerkUser(secret, {
|
|
689
|
+
email,
|
|
690
|
+
firstName: stringValue(fields.firstName),
|
|
691
|
+
lastName: stringValue(fields.lastName),
|
|
692
|
+
publicMetadata
|
|
693
|
+
});
|
|
694
|
+
} else {
|
|
695
|
+
await createClerkInvitation(secret, { email, publicMetadata });
|
|
673
696
|
}
|
|
697
|
+
} catch {
|
|
674
698
|
}
|
|
675
699
|
}
|
|
676
700
|
async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
|
|
@@ -678,14 +702,27 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
|
|
|
678
702
|
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
679
703
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
680
704
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
681
|
-
const
|
|
705
|
+
const stringValue = (value) => typeof value === "string" ? value : "";
|
|
682
706
|
await sendTemplated(
|
|
683
|
-
{
|
|
707
|
+
{
|
|
708
|
+
db,
|
|
709
|
+
envName: env.ODLA_ENV,
|
|
710
|
+
sender: env.SEND_EMAIL,
|
|
711
|
+
from: env.EMAIL_FROM,
|
|
712
|
+
now: () => Date.now(),
|
|
713
|
+
newId: () => crypto.randomUUID()
|
|
714
|
+
},
|
|
684
715
|
{
|
|
685
716
|
group: emailGroupFrom(group),
|
|
686
717
|
template: "adminNotification",
|
|
687
718
|
to: group.notificationEmail,
|
|
688
|
-
vars: {
|
|
719
|
+
vars: {
|
|
720
|
+
firstName: stringValue(fields.firstName),
|
|
721
|
+
lastName: stringValue(fields.lastName),
|
|
722
|
+
email: stringValue(fields.email),
|
|
723
|
+
phone: stringValue(fields.phone),
|
|
724
|
+
state: stringValue(fields.state)
|
|
725
|
+
},
|
|
689
726
|
dedupeKey: `apply:${applicationId}:admin`,
|
|
690
727
|
applicationId
|
|
691
728
|
}
|
|
@@ -693,6 +730,8 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
|
|
|
693
730
|
} catch {
|
|
694
731
|
}
|
|
695
732
|
}
|
|
733
|
+
|
|
734
|
+
// src/worker-routes.ts
|
|
696
735
|
async function memberSessionApplication(db, chapterId, email) {
|
|
697
736
|
const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: "desc" }, limit: 1 } } })).applications;
|
|
698
737
|
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
@@ -797,7 +836,54 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
797
836
|
if (!group) return json({ error: "not found" }, 404);
|
|
798
837
|
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
799
838
|
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
800
|
-
return json(joinConfig(group, paymentsReady));
|
|
839
|
+
return json({ ...joinConfig(group, paymentsReady), copy: chapter.copy.join });
|
|
840
|
+
}
|
|
841
|
+
if (req.method === "GET" && url.pathname === "/api/join/resume") {
|
|
842
|
+
const applicationId = url.searchParams.get("application") ?? "";
|
|
843
|
+
if (!applicationId) return json({ error: "application is required" }, 400);
|
|
844
|
+
const db = ctx.makeDb(env);
|
|
845
|
+
const apps = (await db.query({
|
|
846
|
+
applications: { $: { where: { id: applicationId }, limit: 1 } }
|
|
847
|
+
})).applications;
|
|
848
|
+
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
849
|
+
if (!app || String(app.groupId ?? "") !== chapter.id) return json({ error: "not found" }, 404);
|
|
850
|
+
const meetings = (await db.query({
|
|
851
|
+
meetings: {
|
|
852
|
+
$: {
|
|
853
|
+
where: { applicationId, status: "scheduled" },
|
|
854
|
+
order: { createdAt: "desc" },
|
|
855
|
+
limit: 1
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
})).meetings;
|
|
859
|
+
const meeting = Array.isArray(meetings) ? meetings[0] : void 0;
|
|
860
|
+
if (meeting && typeof meeting.startAt === "number") {
|
|
861
|
+
const groups2 = (await db.query({
|
|
862
|
+
groups: { $: { where: { id: chapter.id }, limit: 1 } }
|
|
863
|
+
})).groups;
|
|
864
|
+
const group2 = Array.isArray(groups2) ? groups2[0] : void 0;
|
|
865
|
+
const timezone = resolveScheduling(group2?.schedulingJson).timezone;
|
|
866
|
+
return json({
|
|
867
|
+
state: "done",
|
|
868
|
+
applicationId,
|
|
869
|
+
booked: { startAt: meeting.startAt, timezone }
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const groups = (await db.query({
|
|
873
|
+
groups: { $: { where: { id: chapter.id }, limit: 1 } }
|
|
874
|
+
})).groups;
|
|
875
|
+
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
876
|
+
if (!group) return json({ error: "not found" }, 404);
|
|
877
|
+
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
878
|
+
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
879
|
+
const status = String(app.status ?? "");
|
|
880
|
+
if (!paymentsReady) return json({ state: "booking", applicationId });
|
|
881
|
+
if (status === chapter.pipeline.initial && app.stripeSubscriptionId) {
|
|
882
|
+
return json({ state: "paymentPending", applicationId });
|
|
883
|
+
}
|
|
884
|
+
if (status === chapter.pipeline.initial) return json({ state: "payment", applicationId });
|
|
885
|
+
if (canBook(status, chapter.pipeline)) return json({ state: "booking", applicationId });
|
|
886
|
+
return json({ error: `application cannot resume from status "${status}"` }, 409);
|
|
801
887
|
}
|
|
802
888
|
if (req.method === "POST" && url.pathname === "/api/applications") {
|
|
803
889
|
const raw = await req.text();
|
|
@@ -837,21 +923,6 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
837
923
|
|
|
838
924
|
// src/worker-routes-schedule.ts
|
|
839
925
|
import { computeBookableSlots, initCalendar } from "@odla-ai/calendar";
|
|
840
|
-
|
|
841
|
-
// src/pipeline.ts
|
|
842
|
-
function canTransition(from, to, p) {
|
|
843
|
-
const fi = p.stages.indexOf(from);
|
|
844
|
-
const ti = p.stages.indexOf(to);
|
|
845
|
-
return fi >= 0 && ti >= 0 && ti >= fi;
|
|
846
|
-
}
|
|
847
|
-
function canBook(status, p) {
|
|
848
|
-
return p.bookableFrom.includes(status);
|
|
849
|
-
}
|
|
850
|
-
function canApprove(status, p) {
|
|
851
|
-
return p.approvableFrom.includes(status);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// src/worker-routes-schedule.ts
|
|
855
926
|
function errCode(err) {
|
|
856
927
|
if (err && typeof err === "object") {
|
|
857
928
|
const code = err.code;
|
|
@@ -1311,37 +1382,10 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
1311
1382
|
}
|
|
1312
1383
|
return json({ ok: true });
|
|
1313
1384
|
}
|
|
1314
|
-
async function refundApplication(req, url, env, ctx) {
|
|
1315
|
-
const rawDb = ctx.makeDb(env);
|
|
1316
|
-
const u = await ctx.verifyUser(req, env);
|
|
1317
|
-
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1318
|
-
const id = url.pathname.split("/")[4] ?? "";
|
|
1319
|
-
const db = rawDb;
|
|
1320
|
-
const app = await firstRow2(db, "applications", { where: { id }, limit: 1 });
|
|
1321
|
-
if (!app) return json({ error: "not found" }, 404);
|
|
1322
|
-
if (app.status === "refunded") return json({ error: "already refunded" }, 409);
|
|
1323
|
-
if (app.status === "approved") return json({ error: "approved memberships are non-refundable" }, 409);
|
|
1324
|
-
if (!app.stripeSubscriptionId) return json({ error: "no subscription on file" }, 409);
|
|
1325
|
-
if (!app.stripeCustomerId) return json({ error: "no customer on file" }, 409);
|
|
1326
|
-
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
1327
|
-
if (!secretKey) return json({ error: "payments not configured" }, 503);
|
|
1328
|
-
try {
|
|
1329
|
-
const result = await createStripeProvider({ secretKey }).refund({
|
|
1330
|
-
customerId: String(app.stripeCustomerId),
|
|
1331
|
-
subscriptionId: String(app.stripeSubscriptionId)
|
|
1332
|
-
});
|
|
1333
|
-
return json({ ok: true, refundedCents: result.refundedCents, subscriptionCanceled: result.subscriptionCanceled });
|
|
1334
|
-
} catch (err) {
|
|
1335
|
-
const code = codeOf(err);
|
|
1336
|
-
return json({ error: "refund failed", code }, code === "no_charge" ? 409 : 502);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
var REFUND_PATH = /^\/api\/admin\/applications\/[^/]+\/refund$/;
|
|
1340
1385
|
var handlePayments = async (req, url, env, ctx) => {
|
|
1341
1386
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
1342
1387
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
1343
1388
|
if (req.method === "POST" && url.pathname === "/api/webhooks/stripe") return ingestWebhook(req, env, ctx);
|
|
1344
|
-
if (req.method === "POST" && REFUND_PATH.test(url.pathname)) return refundApplication(req, url, env, ctx);
|
|
1345
1389
|
return null;
|
|
1346
1390
|
};
|
|
1347
1391
|
|
|
@@ -1384,7 +1428,8 @@ function reconcileMeetings(meetings, events, now) {
|
|
|
1384
1428
|
async function adminGroup(req, env, ctx, url) {
|
|
1385
1429
|
const rawDb = ctx.makeDb(env);
|
|
1386
1430
|
const u = await ctx.verifyUser(req, env);
|
|
1387
|
-
if (!u
|
|
1431
|
+
if (!u) return json({ error: "unauthorized" }, 401);
|
|
1432
|
+
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1388
1433
|
const db = rawDb;
|
|
1389
1434
|
const groupId = url.searchParams.get("group") ?? ctx.chapter.id;
|
|
1390
1435
|
const group = (await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } })).groups?.[0];
|
|
@@ -1429,7 +1474,8 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
1429
1474
|
if (req.method !== "GET" || url.pathname !== "/api/admin/meetings") return null;
|
|
1430
1475
|
const rawDb = ctx.makeDb(env);
|
|
1431
1476
|
const u = await ctx.verifyUser(req, env);
|
|
1432
|
-
if (!u
|
|
1477
|
+
if (!u) return json({ error: "unauthorized" }, 401);
|
|
1478
|
+
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1433
1479
|
const db = rawDb;
|
|
1434
1480
|
const all = url.searchParams.get("all") === "1";
|
|
1435
1481
|
const from = Number(url.searchParams.get("from"));
|
|
@@ -2341,7 +2387,11 @@ var BUILTIN_ROUTES = [
|
|
|
2341
2387
|
handleAdminComms,
|
|
2342
2388
|
// Leader → follower record delivery
|
|
2343
2389
|
handleAdminNetworkTargets,
|
|
2344
|
-
handleAdminNetworkPush
|
|
2390
|
+
handleAdminNetworkPush,
|
|
2391
|
+
// API requests must never fall through to an SPA asset response. Hosts still
|
|
2392
|
+
// get first refusal through options.routes, then this terminates unknown API
|
|
2393
|
+
// paths with an explicit machine-readable 404.
|
|
2394
|
+
async (_req, url) => url.pathname.startsWith("/api/") ? json({ error: "not found" }, 404) : null
|
|
2345
2395
|
];
|
|
2346
2396
|
function chapterWorker(options) {
|
|
2347
2397
|
const ctx = createWorkerContext(options);
|