@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.cjs
CHANGED
|
@@ -138,68 +138,6 @@ function createWorkerContext(options) {
|
|
|
138
138
|
// src/worker-routes.ts
|
|
139
139
|
var import_crm2 = require("@odla-ai/crm");
|
|
140
140
|
|
|
141
|
-
// src/clerk.ts
|
|
142
|
-
var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
|
|
143
|
-
function clerkInviteRequest(input) {
|
|
144
|
-
return {
|
|
145
|
-
path: "/v1/invitations",
|
|
146
|
-
body: {
|
|
147
|
-
email_address: input.email,
|
|
148
|
-
notify: true,
|
|
149
|
-
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
|
|
150
|
-
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
155
|
-
const { path, body } = clerkInviteRequest(input);
|
|
156
|
-
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
157
|
-
method: "POST",
|
|
158
|
-
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
159
|
-
body: JSON.stringify(body)
|
|
160
|
-
});
|
|
161
|
-
return res.ok ? { ok: true, status: res.status } : heal(res.status);
|
|
162
|
-
}
|
|
163
|
-
function clerkUserRequest(input) {
|
|
164
|
-
return {
|
|
165
|
-
path: "/v1/users",
|
|
166
|
-
body: {
|
|
167
|
-
email_address: [input.email],
|
|
168
|
-
skip_password_requirement: true,
|
|
169
|
-
...input.firstName ? { first_name: input.firstName } : {},
|
|
170
|
-
...input.lastName ? { last_name: input.lastName } : {},
|
|
171
|
-
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
|
|
176
|
-
const auth = { authorization: `Bearer ${secretKey}` };
|
|
177
|
-
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
178
|
-
if (!found.ok) return false;
|
|
179
|
-
const users = await found.json().catch(() => null);
|
|
180
|
-
const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
181
|
-
if (!id) return false;
|
|
182
|
-
const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id}/metadata`, {
|
|
183
|
-
method: "PATCH",
|
|
184
|
-
headers: { ...auth, "content-type": "application/json" },
|
|
185
|
-
body: JSON.stringify({ public_metadata: publicMetadata })
|
|
186
|
-
});
|
|
187
|
-
return patched.ok;
|
|
188
|
-
}
|
|
189
|
-
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
190
|
-
const { path, body } = clerkUserRequest(input);
|
|
191
|
-
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
192
|
-
method: "POST",
|
|
193
|
-
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
194
|
-
body: JSON.stringify(body)
|
|
195
|
-
});
|
|
196
|
-
if (res.ok) return { ok: true, status: res.status };
|
|
197
|
-
const healed = heal(res.status);
|
|
198
|
-
if (!healed.existed || !input.publicMetadata) return healed;
|
|
199
|
-
const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
|
|
200
|
-
return { ...healed, refreshed };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
141
|
// src/member.ts
|
|
204
142
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
205
143
|
function isValidEmail(value) {
|
|
@@ -215,7 +153,7 @@ function hasDisclaimerAck(fields) {
|
|
|
215
153
|
var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
|
|
216
154
|
function applicantProfile(chapter, fields) {
|
|
217
155
|
const app = chapter.application;
|
|
218
|
-
const allowed = (f) => app.profileFields
|
|
156
|
+
const allowed = (f) => app.profileFields.includes(f);
|
|
219
157
|
const profile = {};
|
|
220
158
|
for (const f of [...app.required, ...app.optional]) {
|
|
221
159
|
if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
|
|
@@ -533,6 +471,19 @@ function applicationBookingUpdate(currentStatus, startAt, htmlLink) {
|
|
|
533
471
|
};
|
|
534
472
|
}
|
|
535
473
|
|
|
474
|
+
// src/pipeline.ts
|
|
475
|
+
function canTransition(from, to, p) {
|
|
476
|
+
const fi = p.stages.indexOf(from);
|
|
477
|
+
const ti = p.stages.indexOf(to);
|
|
478
|
+
return fi >= 0 && ti >= 0 && ti >= fi;
|
|
479
|
+
}
|
|
480
|
+
function canBook(status, p) {
|
|
481
|
+
return p.bookableFrom.includes(status);
|
|
482
|
+
}
|
|
483
|
+
function canApprove(status, p) {
|
|
484
|
+
return p.approvableFrom.includes(status);
|
|
485
|
+
}
|
|
486
|
+
|
|
536
487
|
// src/session.ts
|
|
537
488
|
function applicationSummary(app) {
|
|
538
489
|
return {
|
|
@@ -565,6 +516,68 @@ function memberApplication(app, meeting, defaultTimezone) {
|
|
|
565
516
|
return { ...summary, meetingAt, meetUrl, timezone };
|
|
566
517
|
}
|
|
567
518
|
|
|
519
|
+
// src/clerk.ts
|
|
520
|
+
var heal = (status) => status === 422 ? { ok: true, status, existed: true } : { ok: false, status };
|
|
521
|
+
function clerkInviteRequest(input) {
|
|
522
|
+
return {
|
|
523
|
+
path: "/v1/invitations",
|
|
524
|
+
body: {
|
|
525
|
+
email_address: input.email,
|
|
526
|
+
notify: true,
|
|
527
|
+
...input.redirectUrl ? { redirect_url: input.redirectUrl } : {},
|
|
528
|
+
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
533
|
+
const { path, body } = clerkInviteRequest(input);
|
|
534
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
535
|
+
method: "POST",
|
|
536
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
537
|
+
body: JSON.stringify(body)
|
|
538
|
+
});
|
|
539
|
+
return res.ok ? { ok: true, status: res.status } : heal(res.status);
|
|
540
|
+
}
|
|
541
|
+
function clerkUserRequest(input) {
|
|
542
|
+
return {
|
|
543
|
+
path: "/v1/users",
|
|
544
|
+
body: {
|
|
545
|
+
email_address: [input.email],
|
|
546
|
+
skip_password_requirement: true,
|
|
547
|
+
...input.firstName ? { first_name: input.firstName } : {},
|
|
548
|
+
...input.lastName ? { last_name: input.lastName } : {},
|
|
549
|
+
...input.publicMetadata ? { public_metadata: input.publicMetadata } : {}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function refreshUserMetadata(secretKey, email, publicMetadata, fetchImpl) {
|
|
554
|
+
const auth = { authorization: `Bearer ${secretKey}` };
|
|
555
|
+
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, { headers: auth });
|
|
556
|
+
if (!found.ok) return false;
|
|
557
|
+
const users = await found.json().catch(() => null);
|
|
558
|
+
const id = Array.isArray(users) && typeof users[0]?.id === "string" ? users[0].id : void 0;
|
|
559
|
+
if (!id) return false;
|
|
560
|
+
const patched = await fetchImpl(`https://api.clerk.com/v1/users/${id}/metadata`, {
|
|
561
|
+
method: "PATCH",
|
|
562
|
+
headers: { ...auth, "content-type": "application/json" },
|
|
563
|
+
body: JSON.stringify({ public_metadata: publicMetadata })
|
|
564
|
+
});
|
|
565
|
+
return patched.ok;
|
|
566
|
+
}
|
|
567
|
+
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
568
|
+
const { path, body } = clerkUserRequest(input);
|
|
569
|
+
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
570
|
+
method: "POST",
|
|
571
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
572
|
+
body: JSON.stringify(body)
|
|
573
|
+
});
|
|
574
|
+
if (res.ok) return { ok: true, status: res.status };
|
|
575
|
+
const healed = heal(res.status);
|
|
576
|
+
if (!healed.existed || !input.publicMetadata) return healed;
|
|
577
|
+
const refreshed = await refreshUserMetadata(secretKey, input.email, input.publicMetadata, fetchImpl).catch(() => false);
|
|
578
|
+
return { ...healed, refreshed };
|
|
579
|
+
}
|
|
580
|
+
|
|
568
581
|
// src/email.ts
|
|
569
582
|
function render(template, vars) {
|
|
570
583
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? "");
|
|
@@ -668,36 +681,47 @@ function emailGroupFrom(row) {
|
|
|
668
681
|
};
|
|
669
682
|
}
|
|
670
683
|
|
|
671
|
-
// src/worker-
|
|
684
|
+
// src/worker-provisioning.ts
|
|
672
685
|
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
673
686
|
const email = typeof fields.email === "string" ? fields.email : "";
|
|
674
687
|
if (!email) return;
|
|
675
|
-
const
|
|
688
|
+
const stringValue = (value) => typeof value === "string" ? value : void 0;
|
|
676
689
|
const extra = {};
|
|
677
|
-
for (const
|
|
678
|
-
if (fields[
|
|
690
|
+
for (const field of chapter.application.crmFields) {
|
|
691
|
+
if (fields[field] !== void 0) extra[field] = fields[field];
|
|
679
692
|
}
|
|
680
693
|
try {
|
|
681
694
|
await projectApplicant(
|
|
682
695
|
{ crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
|
|
683
|
-
{
|
|
696
|
+
{
|
|
697
|
+
applicationId,
|
|
698
|
+
email,
|
|
699
|
+
firstName: stringValue(fields.firstName),
|
|
700
|
+
lastName: stringValue(fields.lastName),
|
|
701
|
+
phone: stringValue(fields.phone),
|
|
702
|
+
linkedin: stringValue(fields.linkedin),
|
|
703
|
+
extra
|
|
704
|
+
}
|
|
684
705
|
);
|
|
685
706
|
} catch {
|
|
686
707
|
}
|
|
687
|
-
if (chapter.account
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
708
|
+
if (chapter.account === "none") return;
|
|
709
|
+
try {
|
|
710
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
711
|
+
if (!secret) return;
|
|
712
|
+
const profile = applicantProfile(chapter, fields);
|
|
713
|
+
const publicMetadata = { applicationId, ...profile ? { profile } : {} };
|
|
714
|
+
if (chapter.account === "create") {
|
|
715
|
+
await createClerkUser(secret, {
|
|
716
|
+
email,
|
|
717
|
+
firstName: stringValue(fields.firstName),
|
|
718
|
+
lastName: stringValue(fields.lastName),
|
|
719
|
+
publicMetadata
|
|
720
|
+
});
|
|
721
|
+
} else {
|
|
722
|
+
await createClerkInvitation(secret, { email, publicMetadata });
|
|
700
723
|
}
|
|
724
|
+
} catch {
|
|
701
725
|
}
|
|
702
726
|
}
|
|
703
727
|
async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
|
|
@@ -705,14 +729,27 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
|
|
|
705
729
|
const groups = (await db.query({ groups: { $: { where: { id: chapterId }, limit: 1 } } })).groups;
|
|
706
730
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
707
731
|
if (!group || typeof group.notificationEmail !== "string" || !group.notificationEmail) return;
|
|
708
|
-
const
|
|
732
|
+
const stringValue = (value) => typeof value === "string" ? value : "";
|
|
709
733
|
await sendTemplated(
|
|
710
|
-
{
|
|
734
|
+
{
|
|
735
|
+
db,
|
|
736
|
+
envName: env.ODLA_ENV,
|
|
737
|
+
sender: env.SEND_EMAIL,
|
|
738
|
+
from: env.EMAIL_FROM,
|
|
739
|
+
now: () => Date.now(),
|
|
740
|
+
newId: () => crypto.randomUUID()
|
|
741
|
+
},
|
|
711
742
|
{
|
|
712
743
|
group: emailGroupFrom(group),
|
|
713
744
|
template: "adminNotification",
|
|
714
745
|
to: group.notificationEmail,
|
|
715
|
-
vars: {
|
|
746
|
+
vars: {
|
|
747
|
+
firstName: stringValue(fields.firstName),
|
|
748
|
+
lastName: stringValue(fields.lastName),
|
|
749
|
+
email: stringValue(fields.email),
|
|
750
|
+
phone: stringValue(fields.phone),
|
|
751
|
+
state: stringValue(fields.state)
|
|
752
|
+
},
|
|
716
753
|
dedupeKey: `apply:${applicationId}:admin`,
|
|
717
754
|
applicationId
|
|
718
755
|
}
|
|
@@ -720,6 +757,8 @@ async function notifyAdminOfApplication(db, env, chapterId, applicationId, field
|
|
|
720
757
|
} catch {
|
|
721
758
|
}
|
|
722
759
|
}
|
|
760
|
+
|
|
761
|
+
// src/worker-routes.ts
|
|
723
762
|
async function memberSessionApplication(db, chapterId, email) {
|
|
724
763
|
const apps = (await db.query({ applications: { $: { where: { email }, order: { createdAt: "desc" }, limit: 1 } } })).applications;
|
|
725
764
|
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
@@ -824,7 +863,54 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
824
863
|
if (!group) return json({ error: "not found" }, 404);
|
|
825
864
|
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
826
865
|
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
827
|
-
return json(joinConfig(group, paymentsReady));
|
|
866
|
+
return json({ ...joinConfig(group, paymentsReady), copy: chapter.copy.join });
|
|
867
|
+
}
|
|
868
|
+
if (req.method === "GET" && url.pathname === "/api/join/resume") {
|
|
869
|
+
const applicationId = url.searchParams.get("application") ?? "";
|
|
870
|
+
if (!applicationId) return json({ error: "application is required" }, 400);
|
|
871
|
+
const db = ctx.makeDb(env);
|
|
872
|
+
const apps = (await db.query({
|
|
873
|
+
applications: { $: { where: { id: applicationId }, limit: 1 } }
|
|
874
|
+
})).applications;
|
|
875
|
+
const app = Array.isArray(apps) ? apps[0] : void 0;
|
|
876
|
+
if (!app || String(app.groupId ?? "") !== chapter.id) return json({ error: "not found" }, 404);
|
|
877
|
+
const meetings = (await db.query({
|
|
878
|
+
meetings: {
|
|
879
|
+
$: {
|
|
880
|
+
where: { applicationId, status: "scheduled" },
|
|
881
|
+
order: { createdAt: "desc" },
|
|
882
|
+
limit: 1
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
})).meetings;
|
|
886
|
+
const meeting = Array.isArray(meetings) ? meetings[0] : void 0;
|
|
887
|
+
if (meeting && typeof meeting.startAt === "number") {
|
|
888
|
+
const groups2 = (await db.query({
|
|
889
|
+
groups: { $: { where: { id: chapter.id }, limit: 1 } }
|
|
890
|
+
})).groups;
|
|
891
|
+
const group2 = Array.isArray(groups2) ? groups2[0] : void 0;
|
|
892
|
+
const timezone = resolveScheduling(group2?.schedulingJson).timezone;
|
|
893
|
+
return json({
|
|
894
|
+
state: "done",
|
|
895
|
+
applicationId,
|
|
896
|
+
booked: { startAt: meeting.startAt, timezone }
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
const groups = (await db.query({
|
|
900
|
+
groups: { $: { where: { id: chapter.id }, limit: 1 } }
|
|
901
|
+
})).groups;
|
|
902
|
+
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
903
|
+
if (!group) return json({ error: "not found" }, 404);
|
|
904
|
+
const stripeKey = await getVaultSecret(db, "stripe_secret_key");
|
|
905
|
+
const paymentsReady = Boolean(group.stripePublishableKey && group.stripePriceId && stripeKey);
|
|
906
|
+
const status = String(app.status ?? "");
|
|
907
|
+
if (!paymentsReady) return json({ state: "booking", applicationId });
|
|
908
|
+
if (status === chapter.pipeline.initial && app.stripeSubscriptionId) {
|
|
909
|
+
return json({ state: "paymentPending", applicationId });
|
|
910
|
+
}
|
|
911
|
+
if (status === chapter.pipeline.initial) return json({ state: "payment", applicationId });
|
|
912
|
+
if (canBook(status, chapter.pipeline)) return json({ state: "booking", applicationId });
|
|
913
|
+
return json({ error: `application cannot resume from status "${status}"` }, 409);
|
|
828
914
|
}
|
|
829
915
|
if (req.method === "POST" && url.pathname === "/api/applications") {
|
|
830
916
|
const raw = await req.text();
|
|
@@ -864,21 +950,6 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
864
950
|
|
|
865
951
|
// src/worker-routes-schedule.ts
|
|
866
952
|
var import_calendar = require("@odla-ai/calendar");
|
|
867
|
-
|
|
868
|
-
// src/pipeline.ts
|
|
869
|
-
function canTransition(from, to, p) {
|
|
870
|
-
const fi = p.stages.indexOf(from);
|
|
871
|
-
const ti = p.stages.indexOf(to);
|
|
872
|
-
return fi >= 0 && ti >= 0 && ti >= fi;
|
|
873
|
-
}
|
|
874
|
-
function canBook(status, p) {
|
|
875
|
-
return p.bookableFrom.includes(status);
|
|
876
|
-
}
|
|
877
|
-
function canApprove(status, p) {
|
|
878
|
-
return p.approvableFrom.includes(status);
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// src/worker-routes-schedule.ts
|
|
882
953
|
function errCode(err) {
|
|
883
954
|
if (err && typeof err === "object") {
|
|
884
955
|
const code = err.code;
|
|
@@ -1338,37 +1409,10 @@ async function ingestWebhook(req, env, ctx) {
|
|
|
1338
1409
|
}
|
|
1339
1410
|
return json({ ok: true });
|
|
1340
1411
|
}
|
|
1341
|
-
async function refundApplication(req, url, env, ctx) {
|
|
1342
|
-
const rawDb = ctx.makeDb(env);
|
|
1343
|
-
const u = await ctx.verifyUser(req, env);
|
|
1344
|
-
if (!u || !await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1345
|
-
const id = url.pathname.split("/")[4] ?? "";
|
|
1346
|
-
const db = rawDb;
|
|
1347
|
-
const app = await firstRow2(db, "applications", { where: { id }, limit: 1 });
|
|
1348
|
-
if (!app) return json({ error: "not found" }, 404);
|
|
1349
|
-
if (app.status === "refunded") return json({ error: "already refunded" }, 409);
|
|
1350
|
-
if (app.status === "approved") return json({ error: "approved memberships are non-refundable" }, 409);
|
|
1351
|
-
if (!app.stripeSubscriptionId) return json({ error: "no subscription on file" }, 409);
|
|
1352
|
-
if (!app.stripeCustomerId) return json({ error: "no customer on file" }, 409);
|
|
1353
|
-
const secretKey = await getVaultSecret(db, "stripe_secret_key");
|
|
1354
|
-
if (!secretKey) return json({ error: "payments not configured" }, 503);
|
|
1355
|
-
try {
|
|
1356
|
-
const result = await createStripeProvider({ secretKey }).refund({
|
|
1357
|
-
customerId: String(app.stripeCustomerId),
|
|
1358
|
-
subscriptionId: String(app.stripeSubscriptionId)
|
|
1359
|
-
});
|
|
1360
|
-
return json({ ok: true, refundedCents: result.refundedCents, subscriptionCanceled: result.subscriptionCanceled });
|
|
1361
|
-
} catch (err) {
|
|
1362
|
-
const code = codeOf(err);
|
|
1363
|
-
return json({ error: "refund failed", code }, code === "no_charge" ? 409 : 502);
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
var REFUND_PATH = /^\/api\/admin\/applications\/[^/]+\/refund$/;
|
|
1367
1412
|
var handlePayments = async (req, url, env, ctx) => {
|
|
1368
1413
|
if (ctx.chapter.mode !== "chapter") return null;
|
|
1369
1414
|
if (req.method === "POST" && url.pathname === "/api/payments/subscription") return startSubscription(req, env, ctx);
|
|
1370
1415
|
if (req.method === "POST" && url.pathname === "/api/webhooks/stripe") return ingestWebhook(req, env, ctx);
|
|
1371
|
-
if (req.method === "POST" && REFUND_PATH.test(url.pathname)) return refundApplication(req, url, env, ctx);
|
|
1372
1416
|
return null;
|
|
1373
1417
|
};
|
|
1374
1418
|
|
|
@@ -1411,7 +1455,8 @@ function reconcileMeetings(meetings, events, now) {
|
|
|
1411
1455
|
async function adminGroup(req, env, ctx, url) {
|
|
1412
1456
|
const rawDb = ctx.makeDb(env);
|
|
1413
1457
|
const u = await ctx.verifyUser(req, env);
|
|
1414
|
-
if (!u
|
|
1458
|
+
if (!u) return json({ error: "unauthorized" }, 401);
|
|
1459
|
+
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1415
1460
|
const db = rawDb;
|
|
1416
1461
|
const groupId = url.searchParams.get("group") ?? ctx.chapter.id;
|
|
1417
1462
|
const group = (await db.query({ groups: { $: { where: { id: groupId }, limit: 1 } } })).groups?.[0];
|
|
@@ -1456,7 +1501,8 @@ var handleAdminMeetings = async (req, url, env, ctx) => {
|
|
|
1456
1501
|
if (req.method !== "GET" || url.pathname !== "/api/admin/meetings") return null;
|
|
1457
1502
|
const rawDb = ctx.makeDb(env);
|
|
1458
1503
|
const u = await ctx.verifyUser(req, env);
|
|
1459
|
-
if (!u
|
|
1504
|
+
if (!u) return json({ error: "unauthorized" }, 401);
|
|
1505
|
+
if (!await ctx.isAdmin(rawDb, u)) return json({ error: "forbidden" }, 403);
|
|
1460
1506
|
const db = rawDb;
|
|
1461
1507
|
const all = url.searchParams.get("all") === "1";
|
|
1462
1508
|
const from = Number(url.searchParams.get("from"));
|
|
@@ -2368,7 +2414,11 @@ var BUILTIN_ROUTES = [
|
|
|
2368
2414
|
handleAdminComms,
|
|
2369
2415
|
// Leader → follower record delivery
|
|
2370
2416
|
handleAdminNetworkTargets,
|
|
2371
|
-
handleAdminNetworkPush
|
|
2417
|
+
handleAdminNetworkPush,
|
|
2418
|
+
// API requests must never fall through to an SPA asset response. Hosts still
|
|
2419
|
+
// get first refusal through options.routes, then this terminates unknown API
|
|
2420
|
+
// paths with an explicit machine-readable 404.
|
|
2421
|
+
async (_req, url) => url.pathname.startsWith("/api/") ? json({ error: "not found" }, 404) : null
|
|
2372
2422
|
];
|
|
2373
2423
|
function chapterWorker(options) {
|
|
2374
2424
|
const ctx = createWorkerContext(options);
|