@odla-ai/chapter 0.48.0 → 0.48.2
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/dist/{copy-context-BveBZglM.d.ts → copy-context-CMupQL9h.d.ts} +2 -2
- package/dist/index.cjs +62 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -22
- package/dist/index.d.ts +24 -22
- package/dist/index.js +62 -21
- package/dist/index.js.map +1 -1
- package/dist/ui/admin/index.d.ts +2 -2
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui/member/index.d.ts +2 -2
- package/dist/worker/index.cjs +230 -132
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +2 -2
- package/dist/worker/index.d.ts +2 -2
- package/dist/worker/index.js +230 -132
- package/dist/worker/index.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/admin/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
2
|
import { ComponentChildren, JSX } from 'preact';
|
|
3
3
|
import { CrmClient, Crm, CrmRecord, FieldType } from '@odla-ai/crm';
|
|
4
|
-
import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-
|
|
5
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
4
|
+
import { a as Chapter, b as ChapterCopy, M as MemberContentDocument, E as EffectiveMemberContent, c as MemberContentTierAuthority } from '../../copy-context-CMupQL9h.js';
|
|
5
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-CMupQL9h.js';
|
|
6
6
|
import { CrmLifecycleAdapter, CrmWorkspaceMasterContext, CrmWorkspaceRecordContext, RecordPanelTab } from '@odla-ai/crm/ui';
|
|
7
7
|
|
|
8
8
|
/** Normalized public auth configuration consumed by the Clerk gate. */
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { AdminAccountMenuProps, AdminBuilderLibraryData, AdminChrome, AdminMemberContentData, AdminNote, AdminPage, AdminResource, AdminRouteState, AdminRouteTarget, AdminRouting, AdminSection, AdminSectionContext, AdminWorkspace, AdminWorkspaceFrameProps, AdminWorkspaceNavProps, AdmissionGrantsWorkspace, AvailabilitySectionOptions, BillingSectionOptions, BuilderLibraryAdmin, ChapterAdmin, ChapterAdminAuthAdapter, ChapterAdminAuthConfig, ChapterAdminAuthLoadContext, ChapterAdminProps, ChapterAdminUser, CollectionSectionOptions, DashboardSectionOptions, EmailSectionOptions, LeaderAdmissionAction, LeaderAdmissionActionProps, LeaderNetworkOverview, MeetingsSectionOptions, MemberContentAdmin, NetworkRecordEditor, NetworkRecordsWorkspace, NetworkShareActions, NetworkShareActionsProps, Panel, PeopleSectionOptions, RecordActions, RecordActionsProps, ThemeWarning, adminFetch, adminRouteFromUrl, adminRouteHref, adminSectionFromUrl, adminSectionHref, admissionGrantsWorkspace, applicationLifecycleAdapter, availabilitySection, billingSection, builderLibrarySection, collectionSection, dashboardSection, defaultAdminSections, defaultAdminWorkspaces, emailSection, loadChapterAdminConfig, loadChapterAdminUser, meetingsSection, memberContentSection, networkRecordsWorkspace, peopleSection, useAdminResource } from './admin/index.js';
|
|
2
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-
|
|
2
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../copy-context-CMupQL9h.js';
|
|
3
3
|
export { ApiFn, BrandStyle, BrandStyleProps, GiftCodeRedeemResult, GiftCodeRedeemer, GiftCodeRedeemerCopy, GiftCodeRedeemerProps, GiftOfferCards, GiftOfferCardsMode, GiftOfferCardsProps, GiftPurchases, GiftPurchasesProps, JoinBookingSimulation, JoinConfig, JoinFlowState, JoinIsland, JoinIslandProps, JoinSimulation, JoinStepRenderContext, JoinSubmitRenderContext, JoinTier, JoinTierRenderContext, MEMBERSHIP_CARD_TOKEN_DEFAULTS, MemberContentArea, MemberContentAreaProps, MemberGifts, MemberProvisionalRenderContext, MembersArea, MembersAreaProps, MembershipCardTokenName, MembershipCards, MembershipCardsMode, MembershipCardsProps, PaymentPriceLines, PaymentStep, PaymentStepProps, PaymentStepSimulation, RescheduleProps, Rescheduler, Slot, SlotPicker, SlotPickerClasses, SlotPickerProps, SubResponse, TierPlaceholder, dayKey, dayLabel, fmtDate, fmtMoney, fullLabel, groupSlotsByDay, loadJoinResume, membershipApplicationHref, tierNeedsPayment, timeLabel, tzShort, useTierSelection } from './member/index.js';
|
|
4
4
|
import 'preact';
|
|
5
5
|
import '@odla-ai/crm';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as preact from 'preact';
|
|
2
2
|
import { ComponentChildren, JSX } from 'preact';
|
|
3
|
-
import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-
|
|
4
|
-
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-
|
|
3
|
+
import { d as MemberContentProjection, e as MemberContentPageProjection, b as ChapterCopy, f as ChapterBrand } from '../../copy-context-CMupQL9h.js';
|
|
4
|
+
export { C as ChapterCopyProvider, u as useChapterCopy } from '../../copy-context-CMupQL9h.js';
|
|
5
5
|
import { FieldType, FieldConditions } from '@odla-ai/crm';
|
|
6
6
|
|
|
7
7
|
/** A bookable slot: a start instant in epoch milliseconds. */
|
package/dist/worker/index.cjs
CHANGED
|
@@ -386,54 +386,6 @@ function createWorkerContext(options) {
|
|
|
386
386
|
};
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
// src/worker-routes.ts
|
|
390
|
-
var import_crm6 = require("@odla-ai/crm");
|
|
391
|
-
|
|
392
|
-
// src/discussion-reference-security.ts
|
|
393
|
-
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
394
|
-
const match = req.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/);
|
|
395
|
-
const appIncarnation = env.ODLA_APP_INCARNATION;
|
|
396
|
-
const expectedTenant = env.ODLA_ENV === "prod" ? env.ODLA_APP_ID : `${env.ODLA_APP_ID}--${env.ODLA_ENV}`;
|
|
397
|
-
if (!match || env.ODLA_TENANT !== expectedTenant || typeof appIncarnation !== "string" || !/^[a-f0-9]{32}$/.test(appIncarnation)) return null;
|
|
398
|
-
const requestUrl = new URL(req.url);
|
|
399
|
-
requestUrl.search = "";
|
|
400
|
-
requestUrl.hash = "";
|
|
401
|
-
const audience = requestUrl.href;
|
|
402
|
-
try {
|
|
403
|
-
const url = new URL(
|
|
404
|
-
"/registry/discuss/reference-authority/verify",
|
|
405
|
-
env.ODLA_PLATFORM
|
|
406
|
-
);
|
|
407
|
-
const response = await fetcher(url, {
|
|
408
|
-
method: "POST",
|
|
409
|
-
headers: { "content-type": "application/json" },
|
|
410
|
-
body: JSON.stringify({
|
|
411
|
-
token: match[1],
|
|
412
|
-
appId: env.ODLA_APP_ID,
|
|
413
|
-
appIncarnation,
|
|
414
|
-
env: env.ODLA_ENV,
|
|
415
|
-
product: "crm",
|
|
416
|
-
projectCapability: "crm.read",
|
|
417
|
-
audience
|
|
418
|
-
}),
|
|
419
|
-
redirect: "manual"
|
|
420
|
-
});
|
|
421
|
-
if (!response.ok) return null;
|
|
422
|
-
const value = await response.json();
|
|
423
|
-
if (value.authority?.appId !== env.ODLA_APP_ID || value.authority.appIncarnation !== appIncarnation || value.authority.appEnv !== env.ODLA_ENV || value.authority.capability !== "product.reference.read" || value.authority.product !== "crm" || value.authority.projectCapability !== "crm.read" || value.authority.audience !== audience || typeof value.actor?.id !== "string" || value.actor.kind !== "human" && value.actor.kind !== "agent" || value.actor.email !== void 0 && typeof value.actor.email !== "string") return null;
|
|
424
|
-
return {
|
|
425
|
-
userId: value.actor.id,
|
|
426
|
-
discussionReferenceScope: "all",
|
|
427
|
-
...value.actor.email ? { email: value.actor.email } : {}
|
|
428
|
-
};
|
|
429
|
-
} catch {
|
|
430
|
-
return null;
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// src/member.ts
|
|
435
|
-
var import_crm2 = require("@odla-ai/crm");
|
|
436
|
-
|
|
437
389
|
// src/application-id.ts
|
|
438
390
|
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
439
391
|
async function applicationIdForSubmission(submissionId) {
|
|
@@ -445,7 +397,26 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
445
397
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
446
398
|
}
|
|
447
399
|
|
|
400
|
+
// src/clerk-signup.ts
|
|
401
|
+
async function findSignupAccount(secret, email, fetchImpl = fetch) {
|
|
402
|
+
const response = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(normalizeChapterEmail(email))}&limit=2`, {
|
|
403
|
+
headers: { authorization: `Bearer ${secret}` },
|
|
404
|
+
redirect: "manual",
|
|
405
|
+
signal: AbortSignal.timeout(5e3)
|
|
406
|
+
});
|
|
407
|
+
if (!response.ok) throw new Error("signup account lookup unavailable");
|
|
408
|
+
const users = await response.json();
|
|
409
|
+
if (!Array.isArray(users) || users.length > 1 || users.length === 1 && (typeof users[0]?.id !== "string" || !users[0].id)) {
|
|
410
|
+
throw new Error("signup account lookup unavailable");
|
|
411
|
+
}
|
|
412
|
+
return users[0] ?? null;
|
|
413
|
+
}
|
|
414
|
+
function belongsToSignup(account, applicationId) {
|
|
415
|
+
return typeof applicationId === "string" && applicationId.length > 0 && account.private_metadata?.applicationId === applicationId;
|
|
416
|
+
}
|
|
417
|
+
|
|
448
418
|
// src/member.ts
|
|
419
|
+
var import_crm2 = require("@odla-ai/crm");
|
|
449
420
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
450
421
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
451
422
|
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
@@ -549,8 +520,10 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
549
520
|
if (fields.focus !== void 0) row2.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
550
521
|
if (opts.groupId) row2.groupId = opts.groupId;
|
|
551
522
|
if (opts.runtime) row2.stripeRuntime = opts.runtime;
|
|
523
|
+
if (chapter.account !== "none") row2.clerkAccountPending = true;
|
|
552
524
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
553
525
|
row2.emailNormalized = normalizeChapterEmail(fields.email);
|
|
526
|
+
row2.signupEmailKey = row2.emailNormalized;
|
|
554
527
|
}
|
|
555
528
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
556
529
|
row2.admissionGrantId = opts.admissionGrant.id;
|
|
@@ -574,11 +547,21 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
574
547
|
consumedApplicationId: id6
|
|
575
548
|
} });
|
|
576
549
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
550
|
+
try {
|
|
551
|
+
const { duplicate } = await db.transact(
|
|
552
|
+
ops,
|
|
553
|
+
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
554
|
+
);
|
|
555
|
+
return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (error.code === "unique_violation") return {
|
|
558
|
+
ok: false,
|
|
559
|
+
code: "account_already_exists",
|
|
560
|
+
status: 409,
|
|
561
|
+
error: "An account or application already exists for this email. Please sign in to continue."
|
|
562
|
+
};
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
582
565
|
}
|
|
583
566
|
function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
|
|
584
567
|
return {
|
|
@@ -601,6 +584,82 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
|
|
|
601
584
|
};
|
|
602
585
|
}
|
|
603
586
|
|
|
587
|
+
// src/signup-eligibility.ts
|
|
588
|
+
var signupConflict = { code: "account_already_exists", message: "An account or application already exists for this email. Please sign in to continue." };
|
|
589
|
+
async function checkSignupEligibility(db, chapter, fields, submissionId) {
|
|
590
|
+
if (typeof fields.email !== "string" || !isValidEmail(fields.email.trim())) return null;
|
|
591
|
+
const email = normalizeChapterEmail(fields.email);
|
|
592
|
+
const applicationId = submissionId ? await applicationIdForSubmission(submissionId) : void 0;
|
|
593
|
+
const conflict = () => Response.json({ error: signupConflict }, { status: 409 });
|
|
594
|
+
try {
|
|
595
|
+
const data = await db.query({
|
|
596
|
+
applications: { $: { where: { emailNormalized: email }, limit: 2 } },
|
|
597
|
+
$users: { $: { where: { email }, limit: 2 } }
|
|
598
|
+
});
|
|
599
|
+
if ((data.applications ?? []).some((row2) => row2.id !== applicationId)) return conflict();
|
|
600
|
+
const original = (data.applications ?? []).find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
|
|
601
|
+
if ((data.$users ?? []).some((row2) => row2.deleted !== true && row2.id !== original?.clerkUserId)) return conflict();
|
|
602
|
+
if (chapter.account === "none") return null;
|
|
603
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
604
|
+
if (!secret) return Response.json({ error: { code: "clerk_secret_unavailable", message: "account provisioning credential is unavailable" } }, { status: 503 });
|
|
605
|
+
const account = await findSignupAccount(secret, email);
|
|
606
|
+
if (account && !(original && (original.clerkUserId === account.id || belongsToSignup(account, original.id)))) return conflict();
|
|
607
|
+
return null;
|
|
608
|
+
} catch {
|
|
609
|
+
return Response.json({ error: { code: "signup_lookup_unavailable", message: "Signup is temporarily unavailable. Please try again." } }, { status: 503 });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function rejectUnprovisionedSignup(db, applicationId, status2) {
|
|
613
|
+
await db.transact([{ t: "update", ns: "applications", id: applicationId, attrs: { status: "declined" } }], {
|
|
614
|
+
guards: [{ ns: "applications", id: applicationId, exists: true, equals: { status: status2 }, absent: ["clerkUserId"] }]
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/worker-routes.ts
|
|
619
|
+
var import_crm6 = require("@odla-ai/crm");
|
|
620
|
+
|
|
621
|
+
// src/discussion-reference-security.ts
|
|
622
|
+
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
623
|
+
const match = req.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/);
|
|
624
|
+
const appIncarnation = env.ODLA_APP_INCARNATION;
|
|
625
|
+
const expectedTenant = env.ODLA_ENV === "prod" ? env.ODLA_APP_ID : `${env.ODLA_APP_ID}--${env.ODLA_ENV}`;
|
|
626
|
+
if (!match || env.ODLA_TENANT !== expectedTenant || typeof appIncarnation !== "string" || !/^[a-f0-9]{32}$/.test(appIncarnation)) return null;
|
|
627
|
+
const requestUrl = new URL(req.url);
|
|
628
|
+
requestUrl.search = "";
|
|
629
|
+
requestUrl.hash = "";
|
|
630
|
+
const audience = requestUrl.href;
|
|
631
|
+
try {
|
|
632
|
+
const url = new URL(
|
|
633
|
+
"/registry/discuss/reference-authority/verify",
|
|
634
|
+
env.ODLA_PLATFORM
|
|
635
|
+
);
|
|
636
|
+
const response = await fetcher(url, {
|
|
637
|
+
method: "POST",
|
|
638
|
+
headers: { "content-type": "application/json" },
|
|
639
|
+
body: JSON.stringify({
|
|
640
|
+
token: match[1],
|
|
641
|
+
appId: env.ODLA_APP_ID,
|
|
642
|
+
appIncarnation,
|
|
643
|
+
env: env.ODLA_ENV,
|
|
644
|
+
product: "crm",
|
|
645
|
+
projectCapability: "crm.read",
|
|
646
|
+
audience
|
|
647
|
+
}),
|
|
648
|
+
redirect: "manual"
|
|
649
|
+
});
|
|
650
|
+
if (!response.ok) return null;
|
|
651
|
+
const value = await response.json();
|
|
652
|
+
if (value.authority?.appId !== env.ODLA_APP_ID || value.authority.appIncarnation !== appIncarnation || value.authority.appEnv !== env.ODLA_ENV || value.authority.capability !== "product.reference.read" || value.authority.product !== "crm" || value.authority.projectCapability !== "crm.read" || value.authority.audience !== audience || typeof value.actor?.id !== "string" || value.actor.kind !== "human" && value.actor.kind !== "agent" || value.actor.email !== void 0 && typeof value.actor.email !== "string") return null;
|
|
653
|
+
return {
|
|
654
|
+
userId: value.actor.id,
|
|
655
|
+
discussionReferenceScope: "all",
|
|
656
|
+
...value.actor.email ? { email: value.actor.email } : {}
|
|
657
|
+
};
|
|
658
|
+
} catch {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
604
663
|
// src/tiers.ts
|
|
605
664
|
function tierIsFree(tier) {
|
|
606
665
|
return !(tier.priceCents > 0) && !tier.stripePriceId;
|
|
@@ -3580,13 +3639,13 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
3580
3639
|
const normalizedRows = await lookup({ emailNormalized: normalized });
|
|
3581
3640
|
const legacyRows = await lookup({ email: identity.email });
|
|
3582
3641
|
apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
|
|
3583
|
-
if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0) return null;
|
|
3642
|
+
if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
|
|
3584
3643
|
emailFallback = true;
|
|
3585
3644
|
}
|
|
3586
3645
|
let app = apps.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
3587
3646
|
if (!app) return null;
|
|
3588
3647
|
if (emailFallback) {
|
|
3589
|
-
const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status"];
|
|
3648
|
+
const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
|
|
3590
3649
|
try {
|
|
3591
3650
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity.userId } }], {
|
|
3592
3651
|
mutationId: `member-account-link:${app.id}:${identity.userId}`,
|
|
@@ -3619,8 +3678,53 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
3619
3678
|
) };
|
|
3620
3679
|
}
|
|
3621
3680
|
|
|
3681
|
+
// src/bounded-request-text.ts
|
|
3682
|
+
async function readBoundedRequestText(req, maxBytes) {
|
|
3683
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
3684
|
+
const reader = req.body?.getReader();
|
|
3685
|
+
const cancel = () => {
|
|
3686
|
+
void reader?.cancel().catch(() => {
|
|
3687
|
+
});
|
|
3688
|
+
};
|
|
3689
|
+
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
3690
|
+
let complete = false;
|
|
3691
|
+
req.signal.addEventListener("abort", cancel, { once: true });
|
|
3692
|
+
try {
|
|
3693
|
+
if (req.signal.aborted) return invalid2();
|
|
3694
|
+
const declared = req.headers.get("content-length");
|
|
3695
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
3696
|
+
return json({ error: "request body too large" }, 413);
|
|
3697
|
+
}
|
|
3698
|
+
if (!reader) return "";
|
|
3699
|
+
const decoder = new TextDecoder();
|
|
3700
|
+
let bytes = 0;
|
|
3701
|
+
let text6 = "";
|
|
3702
|
+
for (; ; ) {
|
|
3703
|
+
const { done, value } = await reader.read();
|
|
3704
|
+
if (req.signal.aborted) return invalid2();
|
|
3705
|
+
if (done) {
|
|
3706
|
+
complete = true;
|
|
3707
|
+
return text6 + decoder.decode();
|
|
3708
|
+
}
|
|
3709
|
+
bytes += value.byteLength;
|
|
3710
|
+
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
3711
|
+
text6 += decoder.decode(value, { stream: true });
|
|
3712
|
+
}
|
|
3713
|
+
} catch {
|
|
3714
|
+
return invalid2();
|
|
3715
|
+
} finally {
|
|
3716
|
+
req.signal.removeEventListener("abort", cancel);
|
|
3717
|
+
if (!complete) cancel();
|
|
3718
|
+
reader?.releaseLock();
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3622
3722
|
// src/clerk.ts
|
|
3623
|
-
|
|
3723
|
+
async function rejectedCreate(res) {
|
|
3724
|
+
const body = typeof res.json === "function" ? await res.json().catch(() => null) : null;
|
|
3725
|
+
const existed = Array.isArray(body?.errors) && body.errors.some((error) => error?.code === "form_identifier_exists" || error?.code === "duplicate_record");
|
|
3726
|
+
return { ok: false, status: res.status, ...existed ? { existed: true } : {} };
|
|
3727
|
+
}
|
|
3624
3728
|
function clerkInviteRequest(input) {
|
|
3625
3729
|
return {
|
|
3626
3730
|
path: "/v1/invitations",
|
|
@@ -3639,7 +3743,7 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
|
3639
3743
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
3640
3744
|
body: JSON.stringify(body)
|
|
3641
3745
|
});
|
|
3642
|
-
return res.ok ? { ok: true, status: res.status } :
|
|
3746
|
+
return res.ok ? { ok: true, status: res.status } : rejectedCreate(res);
|
|
3643
3747
|
}
|
|
3644
3748
|
function clerkUserRequest(input) {
|
|
3645
3749
|
return {
|
|
@@ -3665,17 +3769,6 @@ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fet
|
|
|
3665
3769
|
});
|
|
3666
3770
|
return res.ok;
|
|
3667
3771
|
}
|
|
3668
|
-
async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
|
|
3669
|
-
const id6 = await existingClerkUserId(secretKey, email, fetchImpl);
|
|
3670
|
-
return id6 ? updateClerkUserMetadata(secretKey, id6, input, fetchImpl) : false;
|
|
3671
|
-
}
|
|
3672
|
-
async function existingClerkUserId(secretKey, email, fetchImpl) {
|
|
3673
|
-
const auth = { authorization: `Bearer ${secretKey}` };
|
|
3674
|
-
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=2`, { headers: auth });
|
|
3675
|
-
if (!found.ok) return null;
|
|
3676
|
-
const users = await found.json().catch(() => null);
|
|
3677
|
-
return Array.isArray(users) && users.length === 1 && typeof users[0]?.id === "string" ? users[0].id : null;
|
|
3678
|
-
}
|
|
3679
3772
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
3680
3773
|
const { path, body } = clerkUserRequest(input);
|
|
3681
3774
|
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
@@ -3691,20 +3784,10 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
3691
3784
|
...typeof body2?.id === "string" && body2.id ? { userId: body2.id } : {}
|
|
3692
3785
|
};
|
|
3693
3786
|
}
|
|
3694
|
-
const
|
|
3695
|
-
|
|
3696
|
-
const
|
|
3697
|
-
|
|
3698
|
-
...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
|
|
3699
|
-
...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
|
|
3700
|
-
};
|
|
3701
|
-
if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
|
|
3702
|
-
const userId = await existingClerkUserId(secretKey, input.email, fetchImpl).catch(() => null);
|
|
3703
|
-
const refreshed = userId ? await updateClerkUserMetadata(secretKey, userId, {
|
|
3704
|
-
...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
|
|
3705
|
-
...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
|
|
3706
|
-
}, fetchImpl).catch(() => false) : false;
|
|
3707
|
-
return { ...healed, refreshed, ...refreshed && userId ? { userId } : {} };
|
|
3787
|
+
const failure = await rejectedCreate(res);
|
|
3788
|
+
if (!failure.existed) return failure;
|
|
3789
|
+
const account = await findSignupAccount(secretKey, input.email, fetchImpl).catch(() => null);
|
|
3790
|
+
return account && belongsToSignup(account, input.privateMetadata?.applicationId) ? { ok: true, status: res.status, existed: true, userId: account.id } : failure;
|
|
3708
3791
|
}
|
|
3709
3792
|
|
|
3710
3793
|
// src/crm-provisioned-identity.ts
|
|
@@ -3759,6 +3842,7 @@ async function markPrivateMetadataSynced(db, applicationId, clerkUserId) {
|
|
|
3759
3842
|
id: applicationId,
|
|
3760
3843
|
attrs: {
|
|
3761
3844
|
clerkPrivateMetadataSyncedAt: Date.now(),
|
|
3845
|
+
clerkAccountPending: false,
|
|
3762
3846
|
...clerkUserId ? { clerkUserId } : {}
|
|
3763
3847
|
}
|
|
3764
3848
|
}]);
|
|
@@ -3813,7 +3897,7 @@ async function provisionApplicantForRoute(db, chapter, applicationId, fields) {
|
|
|
3813
3897
|
body: {
|
|
3814
3898
|
error: {
|
|
3815
3899
|
code: error.code,
|
|
3816
|
-
message: error.code === "clerk_secret_unavailable" ? "account provisioning credential is unavailable" : "account provisioning failed"
|
|
3900
|
+
message: error.code === "account_already_exists" ? signupConflict.message : error.code === "clerk_secret_unavailable" ? "account provisioning credential is unavailable" : "account provisioning failed"
|
|
3817
3901
|
},
|
|
3818
3902
|
applicationId
|
|
3819
3903
|
}
|
|
@@ -3823,7 +3907,53 @@ async function provisionApplicantForRoute(db, chapter, applicationId, fields) {
|
|
|
3823
3907
|
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
3824
3908
|
const email = typeof fields.email === "string" ? fields.email : "";
|
|
3825
3909
|
if (!email) return;
|
|
3910
|
+
if (typeof fields.clerkPrivateMetadataSyncedAt === "number" && typeof fields.clerkUserId === "string") return;
|
|
3826
3911
|
const stringValue = (value) => typeof value === "string" ? value : void 0;
|
|
3912
|
+
if (chapter.account !== "none") {
|
|
3913
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
3914
|
+
if (!secret) throw new AccountProvisioningError("clerk_secret_unavailable", 503);
|
|
3915
|
+
try {
|
|
3916
|
+
const existing = await findSignupAccount(secret, email);
|
|
3917
|
+
if (existing) {
|
|
3918
|
+
if (existing.id !== fields.clerkUserId && !belongsToSignup(existing, applicationId)) {
|
|
3919
|
+
throw new AccountProvisioningError("account_already_exists", 409);
|
|
3920
|
+
}
|
|
3921
|
+
await markPrivateMetadataSynced(db, applicationId, existing.id);
|
|
3922
|
+
return;
|
|
3923
|
+
}
|
|
3924
|
+
const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
|
|
3925
|
+
if (chapter.account === "create") {
|
|
3926
|
+
const result = await createClerkUser(secret, {
|
|
3927
|
+
email,
|
|
3928
|
+
firstName: stringValue(fields.firstName),
|
|
3929
|
+
lastName: stringValue(fields.lastName),
|
|
3930
|
+
privateMetadata
|
|
3931
|
+
});
|
|
3932
|
+
if (!result.ok) throw new AccountProvisioningError(
|
|
3933
|
+
result.existed ? "account_already_exists" : "clerk_account_provisioning_failed",
|
|
3934
|
+
result.existed ? 409 : 502
|
|
3935
|
+
);
|
|
3936
|
+
if (!result.userId) throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3937
|
+
await markPrivateMetadataSynced(db, applicationId, result.userId);
|
|
3938
|
+
} else {
|
|
3939
|
+
if (typeof fields.clerkInvitationSentAt === "number") return;
|
|
3940
|
+
const result = await createClerkInvitation(secret, { email });
|
|
3941
|
+
if (!result.ok) throw new AccountProvisioningError(
|
|
3942
|
+
result.existed ? "account_already_exists" : "clerk_account_provisioning_failed",
|
|
3943
|
+
result.existed ? 409 : 502
|
|
3944
|
+
);
|
|
3945
|
+
await db.transact([{
|
|
3946
|
+
t: "update",
|
|
3947
|
+
ns: "applications",
|
|
3948
|
+
id: applicationId,
|
|
3949
|
+
attrs: { clerkInvitationSentAt: Date.now(), clerkAccountPending: false }
|
|
3950
|
+
}]);
|
|
3951
|
+
}
|
|
3952
|
+
} catch (error) {
|
|
3953
|
+
if (error instanceof AccountProvisioningError) throw error;
|
|
3954
|
+
throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3827
3957
|
const extra = {};
|
|
3828
3958
|
for (const field of chapter.application.crmFields) {
|
|
3829
3959
|
if (fields[field] !== void 0) extra[field] = fields[field];
|
|
@@ -3842,44 +3972,9 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
|
3842
3972
|
extra
|
|
3843
3973
|
}
|
|
3844
3974
|
);
|
|
3975
|
+
if (chapter.account === "create") await linkProvisionedApplicant(db, chapter.id, applicationId);
|
|
3845
3976
|
} catch {
|
|
3846
3977
|
}
|
|
3847
|
-
if (chapter.account === "none") return;
|
|
3848
|
-
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
3849
|
-
if (!secret) throw new AccountProvisioningError("clerk_secret_unavailable", 503);
|
|
3850
|
-
try {
|
|
3851
|
-
const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
|
|
3852
|
-
if (chapter.account === "create") {
|
|
3853
|
-
const result = await createClerkUser(secret, {
|
|
3854
|
-
email,
|
|
3855
|
-
firstName: stringValue(fields.firstName),
|
|
3856
|
-
lastName: stringValue(fields.lastName),
|
|
3857
|
-
privateMetadata,
|
|
3858
|
-
repairMetadata: {
|
|
3859
|
-
publicMetadata: publicProfileRemoval,
|
|
3860
|
-
privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
|
|
3861
|
-
}
|
|
3862
|
-
});
|
|
3863
|
-
if (!result.ok || result.existed && !result.refreshed) {
|
|
3864
|
-
throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3865
|
-
}
|
|
3866
|
-
await markPrivateMetadataSynced(db, applicationId, result.userId);
|
|
3867
|
-
await linkProvisionedApplicant(db, chapter.id, applicationId).catch(() => false);
|
|
3868
|
-
} else {
|
|
3869
|
-
const result = await createClerkInvitation(secret, { email });
|
|
3870
|
-
if (!result.ok) throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3871
|
-
if (result.existed) {
|
|
3872
|
-
const refreshed = await updateClerkUserMetadataByEmail(secret, email, {
|
|
3873
|
-
publicMetadata: publicProfileRemoval,
|
|
3874
|
-
privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
|
|
3875
|
-
});
|
|
3876
|
-
if (refreshed) await markPrivateMetadataSynced(db, applicationId);
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
} catch (error) {
|
|
3880
|
-
if (error instanceof AccountProvisioningError) throw error;
|
|
3881
|
-
throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3882
|
-
}
|
|
3883
3978
|
}
|
|
3884
3979
|
async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
|
|
3885
3980
|
try {
|
|
@@ -4042,8 +4137,8 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4042
4137
|
return resumeJoin(url, env, ctx);
|
|
4043
4138
|
}
|
|
4044
4139
|
if (req.method === "POST" && url.pathname === "/api/applications") {
|
|
4045
|
-
const raw2 = await req.
|
|
4046
|
-
if (raw2
|
|
4140
|
+
const raw2 = await readBoundedRequestText(req, chapter.application.bodyCap);
|
|
4141
|
+
if (raw2 instanceof Response) return raw2;
|
|
4047
4142
|
let parsed;
|
|
4048
4143
|
try {
|
|
4049
4144
|
parsed = JSON.parse(raw2);
|
|
@@ -4053,6 +4148,8 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4053
4148
|
}
|
|
4054
4149
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
4055
4150
|
const db = ctx.makeDb(env);
|
|
4151
|
+
const signupFailure = await checkSignupEligibility(db, chapter, parsed, submissionId);
|
|
4152
|
+
if (signupFailure) return signupFailure;
|
|
4056
4153
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
4057
4154
|
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
4058
4155
|
const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
|
|
@@ -4071,7 +4168,7 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4071
4168
|
...effectiveGrant?.decision === "waive_interview" ? { admissionGrant: effectiveGrant } : {},
|
|
4072
4169
|
...giftOffer ? { giftOfferId: giftOffer.id } : {}
|
|
4073
4170
|
});
|
|
4074
|
-
if (!result.ok) return json({ error: result.error }, 400);
|
|
4171
|
+
if (!result.ok) return json({ error: result.error, ...result.code ? { code: result.code } : {} }, result.status ?? 400);
|
|
4075
4172
|
const apps = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications;
|
|
4076
4173
|
const canonical2 = Array.isArray(apps) ? apps[0] : void 0;
|
|
4077
4174
|
if (!canonical2 || canonical2.groupId !== chapter.id) return json({ error: "application state unavailable" }, 503);
|
|
@@ -4084,13 +4181,14 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4084
4181
|
if (normalizeChapterEmail(String(canonical2.email ?? "")) !== normalizeChapterEmail(String(parsed.email ?? ""))) {
|
|
4085
4182
|
return json({ error: "submission belongs to a different application", code: "application_submission_conflict" }, 409);
|
|
4086
4183
|
}
|
|
4087
|
-
if (!result.duplicate) {
|
|
4088
|
-
if (effectiveChapter.sends.adminNotification === "submit") {
|
|
4089
|
-
await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical2);
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
4184
|
const provisioningFailure = await provisionApplicantForRoute(db, effectiveChapter, result.id, canonical2);
|
|
4093
|
-
if (provisioningFailure)
|
|
4185
|
+
if (provisioningFailure) {
|
|
4186
|
+
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical2.status);
|
|
4187
|
+
return json(provisioningFailure.body, provisioningFailure.status);
|
|
4188
|
+
}
|
|
4189
|
+
if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
|
|
4190
|
+
await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical2);
|
|
4191
|
+
}
|
|
4094
4192
|
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
4095
4193
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
4096
4194
|
const state = canonical2 && group ? await continueJoin(db, ctx, env, url.origin, canonical2, group) : null;
|
|
@@ -6935,8 +7033,8 @@ async function firstRow2(db, ns, q) {
|
|
|
6935
7033
|
async function startSubscription(req, env, ctx) {
|
|
6936
7034
|
let body;
|
|
6937
7035
|
try {
|
|
6938
|
-
const raw2 = await req.
|
|
6939
|
-
if (raw2
|
|
7036
|
+
const raw2 = await readBoundedRequestText(req, ctx.chapter.application.bodyCap);
|
|
7037
|
+
if (raw2 instanceof Response) return raw2;
|
|
6940
7038
|
body = JSON.parse(raw2);
|
|
6941
7039
|
if (!body || typeof body !== "object" || Array.isArray(body)) throw new TypeError("body");
|
|
6942
7040
|
} catch {
|
|
@@ -11798,8 +11896,8 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
11798
11896
|
});
|
|
11799
11897
|
}
|
|
11800
11898
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|
|
11801
|
-
const raw2 = await req.
|
|
11802
|
-
if (raw2
|
|
11899
|
+
const raw2 = await readBoundedRequestText(req, formation.bodyCap);
|
|
11900
|
+
if (raw2 instanceof Response) return raw2;
|
|
11803
11901
|
let body;
|
|
11804
11902
|
try {
|
|
11805
11903
|
body = JSON.parse(raw2);
|