@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/worker/index.d.cts
CHANGED
|
@@ -287,7 +287,7 @@ interface LeaderFormationConfig {
|
|
|
287
287
|
maxLen?: Record<string, number>;
|
|
288
288
|
/** Per-string cap when `maxLen` has no field override. Default 4000. */
|
|
289
289
|
defaultMaxLen?: number;
|
|
290
|
-
/** Maximum JSON request size in
|
|
290
|
+
/** Maximum JSON request size in UTF-8 bytes, enforced while streaming. Default 32768. */
|
|
291
291
|
bodyCap?: number;
|
|
292
292
|
}
|
|
293
293
|
/** Fully resolved formation intake policy. */
|
|
@@ -590,7 +590,7 @@ interface ChapterApplication {
|
|
|
590
590
|
/** Per-field character cap. Fields not listed use `defaultMaxLen`. */
|
|
591
591
|
maxLen?: Record<string, number>;
|
|
592
592
|
defaultMaxLen?: number;
|
|
593
|
-
/** Max JSON request body in bytes. Default 32768. */
|
|
593
|
+
/** Max JSON request body in UTF-8 bytes, enforced while streaming. Default 32768. */
|
|
594
594
|
bodyCap?: number;
|
|
595
595
|
/** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
|
|
596
596
|
* writing a row with no consent record. Default `true`. Set `false`
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -287,7 +287,7 @@ interface LeaderFormationConfig {
|
|
|
287
287
|
maxLen?: Record<string, number>;
|
|
288
288
|
/** Per-string cap when `maxLen` has no field override. Default 4000. */
|
|
289
289
|
defaultMaxLen?: number;
|
|
290
|
-
/** Maximum JSON request size in
|
|
290
|
+
/** Maximum JSON request size in UTF-8 bytes, enforced while streaming. Default 32768. */
|
|
291
291
|
bodyCap?: number;
|
|
292
292
|
}
|
|
293
293
|
/** Fully resolved formation intake policy. */
|
|
@@ -590,7 +590,7 @@ interface ChapterApplication {
|
|
|
590
590
|
/** Per-field character cap. Fields not listed use `defaultMaxLen`. */
|
|
591
591
|
maxLen?: Record<string, number>;
|
|
592
592
|
defaultMaxLen?: number;
|
|
593
|
-
/** Max JSON request body in bytes. Default 32768. */
|
|
593
|
+
/** Max JSON request body in UTF-8 bytes, enforced while streaming. Default 32768. */
|
|
594
594
|
bodyCap?: number;
|
|
595
595
|
/** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
|
|
596
596
|
* writing a row with no consent record. Default `true`. Set `false`
|
package/dist/worker/index.js
CHANGED
|
@@ -340,54 +340,6 @@ function createWorkerContext(options) {
|
|
|
340
340
|
};
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
// src/worker-routes.ts
|
|
344
|
-
import { createCrmRoutes } from "@odla-ai/crm";
|
|
345
|
-
|
|
346
|
-
// src/discussion-reference-security.ts
|
|
347
|
-
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
348
|
-
const match = req.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/);
|
|
349
|
-
const appIncarnation = env.ODLA_APP_INCARNATION;
|
|
350
|
-
const expectedTenant = env.ODLA_ENV === "prod" ? env.ODLA_APP_ID : `${env.ODLA_APP_ID}--${env.ODLA_ENV}`;
|
|
351
|
-
if (!match || env.ODLA_TENANT !== expectedTenant || typeof appIncarnation !== "string" || !/^[a-f0-9]{32}$/.test(appIncarnation)) return null;
|
|
352
|
-
const requestUrl = new URL(req.url);
|
|
353
|
-
requestUrl.search = "";
|
|
354
|
-
requestUrl.hash = "";
|
|
355
|
-
const audience = requestUrl.href;
|
|
356
|
-
try {
|
|
357
|
-
const url = new URL(
|
|
358
|
-
"/registry/discuss/reference-authority/verify",
|
|
359
|
-
env.ODLA_PLATFORM
|
|
360
|
-
);
|
|
361
|
-
const response = await fetcher(url, {
|
|
362
|
-
method: "POST",
|
|
363
|
-
headers: { "content-type": "application/json" },
|
|
364
|
-
body: JSON.stringify({
|
|
365
|
-
token: match[1],
|
|
366
|
-
appId: env.ODLA_APP_ID,
|
|
367
|
-
appIncarnation,
|
|
368
|
-
env: env.ODLA_ENV,
|
|
369
|
-
product: "crm",
|
|
370
|
-
projectCapability: "crm.read",
|
|
371
|
-
audience
|
|
372
|
-
}),
|
|
373
|
-
redirect: "manual"
|
|
374
|
-
});
|
|
375
|
-
if (!response.ok) return null;
|
|
376
|
-
const value = await response.json();
|
|
377
|
-
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;
|
|
378
|
-
return {
|
|
379
|
-
userId: value.actor.id,
|
|
380
|
-
discussionReferenceScope: "all",
|
|
381
|
-
...value.actor.email ? { email: value.actor.email } : {}
|
|
382
|
-
};
|
|
383
|
-
} catch {
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// src/member.ts
|
|
389
|
-
import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
|
|
390
|
-
|
|
391
343
|
// src/application-id.ts
|
|
392
344
|
var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
|
|
393
345
|
async function applicationIdForSubmission(submissionId) {
|
|
@@ -399,7 +351,26 @@ async function applicationIdForSubmission(submissionId) {
|
|
|
399
351
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
400
352
|
}
|
|
401
353
|
|
|
354
|
+
// src/clerk-signup.ts
|
|
355
|
+
async function findSignupAccount(secret, email, fetchImpl = fetch) {
|
|
356
|
+
const response = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(normalizeChapterEmail(email))}&limit=2`, {
|
|
357
|
+
headers: { authorization: `Bearer ${secret}` },
|
|
358
|
+
redirect: "manual",
|
|
359
|
+
signal: AbortSignal.timeout(5e3)
|
|
360
|
+
});
|
|
361
|
+
if (!response.ok) throw new Error("signup account lookup unavailable");
|
|
362
|
+
const users = await response.json();
|
|
363
|
+
if (!Array.isArray(users) || users.length > 1 || users.length === 1 && (typeof users[0]?.id !== "string" || !users[0].id)) {
|
|
364
|
+
throw new Error("signup account lookup unavailable");
|
|
365
|
+
}
|
|
366
|
+
return users[0] ?? null;
|
|
367
|
+
}
|
|
368
|
+
function belongsToSignup(account, applicationId) {
|
|
369
|
+
return typeof applicationId === "string" && applicationId.length > 0 && account.private_metadata?.applicationId === applicationId;
|
|
370
|
+
}
|
|
371
|
+
|
|
402
372
|
// src/member.ts
|
|
373
|
+
import { assertFieldCondition, resolveFieldStates } from "@odla-ai/crm";
|
|
403
374
|
var DEFAULT_REQUIRED = ["firstName", "lastName", "email", "referral", "whoYouAre", "message"];
|
|
404
375
|
var DEFAULT_OPTIONAL = ["referralName", "linkedin", "phone", "state"];
|
|
405
376
|
var SCHEMA_FIELD = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
@@ -503,8 +474,10 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
503
474
|
if (fields.focus !== void 0) row2.focus = clampArray(fields.focus, app.maxArrayLen);
|
|
504
475
|
if (opts.groupId) row2.groupId = opts.groupId;
|
|
505
476
|
if (opts.runtime) row2.stripeRuntime = opts.runtime;
|
|
477
|
+
if (chapter.account !== "none") row2.clerkAccountPending = true;
|
|
506
478
|
if (typeof fields.email === "string" && isValidEmail(fields.email.trim())) {
|
|
507
479
|
row2.emailNormalized = normalizeChapterEmail(fields.email);
|
|
480
|
+
row2.signupEmailKey = row2.emailNormalized;
|
|
508
481
|
}
|
|
509
482
|
if (opts.admissionGrant?.decision === "waive_interview") {
|
|
510
483
|
row2.admissionGrantId = opts.admissionGrant.id;
|
|
@@ -528,11 +501,21 @@ async function submitApplication(db, chapter, fields, opts) {
|
|
|
528
501
|
consumedApplicationId: id6
|
|
529
502
|
} });
|
|
530
503
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
504
|
+
try {
|
|
505
|
+
const { duplicate } = await db.transact(
|
|
506
|
+
ops,
|
|
507
|
+
opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
|
|
508
|
+
);
|
|
509
|
+
return { ok: true, id: id6, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (error.code === "unique_violation") return {
|
|
512
|
+
ok: false,
|
|
513
|
+
code: "account_already_exists",
|
|
514
|
+
status: 409,
|
|
515
|
+
error: "An account or application already exists for this email. Please sign in to continue."
|
|
516
|
+
};
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
536
519
|
}
|
|
537
520
|
function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
|
|
538
521
|
return {
|
|
@@ -555,6 +538,82 @@ function joinConfig(group, paymentsReady, tiers = [], conditions = {}) {
|
|
|
555
538
|
};
|
|
556
539
|
}
|
|
557
540
|
|
|
541
|
+
// src/signup-eligibility.ts
|
|
542
|
+
var signupConflict = { code: "account_already_exists", message: "An account or application already exists for this email. Please sign in to continue." };
|
|
543
|
+
async function checkSignupEligibility(db, chapter, fields, submissionId) {
|
|
544
|
+
if (typeof fields.email !== "string" || !isValidEmail(fields.email.trim())) return null;
|
|
545
|
+
const email = normalizeChapterEmail(fields.email);
|
|
546
|
+
const applicationId = submissionId ? await applicationIdForSubmission(submissionId) : void 0;
|
|
547
|
+
const conflict = () => Response.json({ error: signupConflict }, { status: 409 });
|
|
548
|
+
try {
|
|
549
|
+
const data = await db.query({
|
|
550
|
+
applications: { $: { where: { emailNormalized: email }, limit: 2 } },
|
|
551
|
+
$users: { $: { where: { email }, limit: 2 } }
|
|
552
|
+
});
|
|
553
|
+
if ((data.applications ?? []).some((row2) => row2.id !== applicationId)) return conflict();
|
|
554
|
+
const original = (data.applications ?? []).find((row2) => row2.id === applicationId && row2.groupId === chapter.id);
|
|
555
|
+
if ((data.$users ?? []).some((row2) => row2.deleted !== true && row2.id !== original?.clerkUserId)) return conflict();
|
|
556
|
+
if (chapter.account === "none") return null;
|
|
557
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
558
|
+
if (!secret) return Response.json({ error: { code: "clerk_secret_unavailable", message: "account provisioning credential is unavailable" } }, { status: 503 });
|
|
559
|
+
const account = await findSignupAccount(secret, email);
|
|
560
|
+
if (account && !(original && (original.clerkUserId === account.id || belongsToSignup(account, original.id)))) return conflict();
|
|
561
|
+
return null;
|
|
562
|
+
} catch {
|
|
563
|
+
return Response.json({ error: { code: "signup_lookup_unavailable", message: "Signup is temporarily unavailable. Please try again." } }, { status: 503 });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async function rejectUnprovisionedSignup(db, applicationId, status2) {
|
|
567
|
+
await db.transact([{ t: "update", ns: "applications", id: applicationId, attrs: { status: "declined" } }], {
|
|
568
|
+
guards: [{ ns: "applications", id: applicationId, exists: true, equals: { status: status2 }, absent: ["clerkUserId"] }]
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/worker-routes.ts
|
|
573
|
+
import { createCrmRoutes } from "@odla-ai/crm";
|
|
574
|
+
|
|
575
|
+
// src/discussion-reference-security.ts
|
|
576
|
+
async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
|
|
577
|
+
const match = req.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/);
|
|
578
|
+
const appIncarnation = env.ODLA_APP_INCARNATION;
|
|
579
|
+
const expectedTenant = env.ODLA_ENV === "prod" ? env.ODLA_APP_ID : `${env.ODLA_APP_ID}--${env.ODLA_ENV}`;
|
|
580
|
+
if (!match || env.ODLA_TENANT !== expectedTenant || typeof appIncarnation !== "string" || !/^[a-f0-9]{32}$/.test(appIncarnation)) return null;
|
|
581
|
+
const requestUrl = new URL(req.url);
|
|
582
|
+
requestUrl.search = "";
|
|
583
|
+
requestUrl.hash = "";
|
|
584
|
+
const audience = requestUrl.href;
|
|
585
|
+
try {
|
|
586
|
+
const url = new URL(
|
|
587
|
+
"/registry/discuss/reference-authority/verify",
|
|
588
|
+
env.ODLA_PLATFORM
|
|
589
|
+
);
|
|
590
|
+
const response = await fetcher(url, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: { "content-type": "application/json" },
|
|
593
|
+
body: JSON.stringify({
|
|
594
|
+
token: match[1],
|
|
595
|
+
appId: env.ODLA_APP_ID,
|
|
596
|
+
appIncarnation,
|
|
597
|
+
env: env.ODLA_ENV,
|
|
598
|
+
product: "crm",
|
|
599
|
+
projectCapability: "crm.read",
|
|
600
|
+
audience
|
|
601
|
+
}),
|
|
602
|
+
redirect: "manual"
|
|
603
|
+
});
|
|
604
|
+
if (!response.ok) return null;
|
|
605
|
+
const value = await response.json();
|
|
606
|
+
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;
|
|
607
|
+
return {
|
|
608
|
+
userId: value.actor.id,
|
|
609
|
+
discussionReferenceScope: "all",
|
|
610
|
+
...value.actor.email ? { email: value.actor.email } : {}
|
|
611
|
+
};
|
|
612
|
+
} catch {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
558
617
|
// src/tiers.ts
|
|
559
618
|
function tierIsFree(tier) {
|
|
560
619
|
return !(tier.priceCents > 0) && !tier.stripePriceId;
|
|
@@ -3539,13 +3598,13 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
3539
3598
|
const normalizedRows = await lookup({ emailNormalized: normalized });
|
|
3540
3599
|
const legacyRows = await lookup({ email: identity.email });
|
|
3541
3600
|
apps = [...new Map([...normalizedRows, ...legacyRows].map((row2) => [row2.id, row2])).values()];
|
|
3542
|
-
if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0) return null;
|
|
3601
|
+
if (apps.length !== 1 || apps[0]?.clerkUserId !== void 0 || apps[0]?.clerkAccountPending === true || ["declined", "canceled", "refunded"].includes(String(apps[0]?.status))) return null;
|
|
3543
3602
|
emailFallback = true;
|
|
3544
3603
|
}
|
|
3545
3604
|
let app = apps.sort((a, b) => Number(b.createdAt ?? 0) - Number(a.createdAt ?? 0))[0];
|
|
3546
3605
|
if (!app) return null;
|
|
3547
3606
|
if (emailFallback) {
|
|
3548
|
-
const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status"];
|
|
3607
|
+
const keys3 = ["groupId", "stripeRuntime", "email", "emailNormalized", "status", "clerkAccountPending"];
|
|
3549
3608
|
try {
|
|
3550
3609
|
await db.transact([{ t: "update", ns: "applications", id: String(app.id), attrs: { clerkUserId: identity.userId } }], {
|
|
3551
3610
|
mutationId: `member-account-link:${app.id}:${identity.userId}`,
|
|
@@ -3578,8 +3637,53 @@ async function memberSessionApplication(db, chapterId, runtime, identity) {
|
|
|
3578
3637
|
) };
|
|
3579
3638
|
}
|
|
3580
3639
|
|
|
3640
|
+
// src/bounded-request-text.ts
|
|
3641
|
+
async function readBoundedRequestText(req, maxBytes) {
|
|
3642
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new TypeError("invalid request body cap");
|
|
3643
|
+
const reader = req.body?.getReader();
|
|
3644
|
+
const cancel = () => {
|
|
3645
|
+
void reader?.cancel().catch(() => {
|
|
3646
|
+
});
|
|
3647
|
+
};
|
|
3648
|
+
const invalid2 = () => json({ error: "invalid JSON body" }, 400);
|
|
3649
|
+
let complete = false;
|
|
3650
|
+
req.signal.addEventListener("abort", cancel, { once: true });
|
|
3651
|
+
try {
|
|
3652
|
+
if (req.signal.aborted) return invalid2();
|
|
3653
|
+
const declared = req.headers.get("content-length");
|
|
3654
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > maxBytes) {
|
|
3655
|
+
return json({ error: "request body too large" }, 413);
|
|
3656
|
+
}
|
|
3657
|
+
if (!reader) return "";
|
|
3658
|
+
const decoder = new TextDecoder();
|
|
3659
|
+
let bytes = 0;
|
|
3660
|
+
let text6 = "";
|
|
3661
|
+
for (; ; ) {
|
|
3662
|
+
const { done, value } = await reader.read();
|
|
3663
|
+
if (req.signal.aborted) return invalid2();
|
|
3664
|
+
if (done) {
|
|
3665
|
+
complete = true;
|
|
3666
|
+
return text6 + decoder.decode();
|
|
3667
|
+
}
|
|
3668
|
+
bytes += value.byteLength;
|
|
3669
|
+
if (bytes > maxBytes) return json({ error: "request body too large" }, 413);
|
|
3670
|
+
text6 += decoder.decode(value, { stream: true });
|
|
3671
|
+
}
|
|
3672
|
+
} catch {
|
|
3673
|
+
return invalid2();
|
|
3674
|
+
} finally {
|
|
3675
|
+
req.signal.removeEventListener("abort", cancel);
|
|
3676
|
+
if (!complete) cancel();
|
|
3677
|
+
reader?.releaseLock();
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3581
3681
|
// src/clerk.ts
|
|
3582
|
-
|
|
3682
|
+
async function rejectedCreate(res) {
|
|
3683
|
+
const body = typeof res.json === "function" ? await res.json().catch(() => null) : null;
|
|
3684
|
+
const existed = Array.isArray(body?.errors) && body.errors.some((error) => error?.code === "form_identifier_exists" || error?.code === "duplicate_record");
|
|
3685
|
+
return { ok: false, status: res.status, ...existed ? { existed: true } : {} };
|
|
3686
|
+
}
|
|
3583
3687
|
function clerkInviteRequest(input) {
|
|
3584
3688
|
return {
|
|
3585
3689
|
path: "/v1/invitations",
|
|
@@ -3598,7 +3702,7 @@ async function createClerkInvitation(secretKey, input, fetchImpl = fetch) {
|
|
|
3598
3702
|
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
3599
3703
|
body: JSON.stringify(body)
|
|
3600
3704
|
});
|
|
3601
|
-
return res.ok ? { ok: true, status: res.status } :
|
|
3705
|
+
return res.ok ? { ok: true, status: res.status } : rejectedCreate(res);
|
|
3602
3706
|
}
|
|
3603
3707
|
function clerkUserRequest(input) {
|
|
3604
3708
|
return {
|
|
@@ -3624,17 +3728,6 @@ async function updateClerkUserMetadata(secretKey, userId, input, fetchImpl = fet
|
|
|
3624
3728
|
});
|
|
3625
3729
|
return res.ok;
|
|
3626
3730
|
}
|
|
3627
|
-
async function updateClerkUserMetadataByEmail(secretKey, email, input, fetchImpl = fetch) {
|
|
3628
|
-
const id6 = await existingClerkUserId(secretKey, email, fetchImpl);
|
|
3629
|
-
return id6 ? updateClerkUserMetadata(secretKey, id6, input, fetchImpl) : false;
|
|
3630
|
-
}
|
|
3631
|
-
async function existingClerkUserId(secretKey, email, fetchImpl) {
|
|
3632
|
-
const auth = { authorization: `Bearer ${secretKey}` };
|
|
3633
|
-
const found = await fetchImpl(`https://api.clerk.com/v1/users?email_address=${encodeURIComponent(email)}&limit=2`, { headers: auth });
|
|
3634
|
-
if (!found.ok) return null;
|
|
3635
|
-
const users = await found.json().catch(() => null);
|
|
3636
|
-
return Array.isArray(users) && users.length === 1 && typeof users[0]?.id === "string" ? users[0].id : null;
|
|
3637
|
-
}
|
|
3638
3731
|
async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
3639
3732
|
const { path, body } = clerkUserRequest(input);
|
|
3640
3733
|
const res = await fetchImpl(`https://api.clerk.com${path}`, {
|
|
@@ -3650,20 +3743,10 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
3650
3743
|
...typeof body2?.id === "string" && body2.id ? { userId: body2.id } : {}
|
|
3651
3744
|
};
|
|
3652
3745
|
}
|
|
3653
|
-
const
|
|
3654
|
-
|
|
3655
|
-
const
|
|
3656
|
-
|
|
3657
|
-
...input.publicMetadata !== void 0 ? { publicMetadata: input.publicMetadata } : {},
|
|
3658
|
-
...input.privateMetadata !== void 0 ? { privateMetadata: input.privateMetadata } : {}
|
|
3659
|
-
};
|
|
3660
|
-
if (!healed.existed || repair.publicMetadata === void 0 && repair.privateMetadata === void 0) return healed;
|
|
3661
|
-
const userId = await existingClerkUserId(secretKey, input.email, fetchImpl).catch(() => null);
|
|
3662
|
-
const refreshed = userId ? await updateClerkUserMetadata(secretKey, userId, {
|
|
3663
|
-
...repair.publicMetadata !== void 0 ? { publicMetadata: repair.publicMetadata } : {},
|
|
3664
|
-
...repair.privateMetadata !== void 0 ? { privateMetadata: repair.privateMetadata } : {}
|
|
3665
|
-
}, fetchImpl).catch(() => false) : false;
|
|
3666
|
-
return { ...healed, refreshed, ...refreshed && userId ? { userId } : {} };
|
|
3746
|
+
const failure = await rejectedCreate(res);
|
|
3747
|
+
if (!failure.existed) return failure;
|
|
3748
|
+
const account = await findSignupAccount(secretKey, input.email, fetchImpl).catch(() => null);
|
|
3749
|
+
return account && belongsToSignup(account, input.privateMetadata?.applicationId) ? { ok: true, status: res.status, existed: true, userId: account.id } : failure;
|
|
3667
3750
|
}
|
|
3668
3751
|
|
|
3669
3752
|
// src/crm-provisioned-identity.ts
|
|
@@ -3718,6 +3801,7 @@ async function markPrivateMetadataSynced(db, applicationId, clerkUserId) {
|
|
|
3718
3801
|
id: applicationId,
|
|
3719
3802
|
attrs: {
|
|
3720
3803
|
clerkPrivateMetadataSyncedAt: Date.now(),
|
|
3804
|
+
clerkAccountPending: false,
|
|
3721
3805
|
...clerkUserId ? { clerkUserId } : {}
|
|
3722
3806
|
}
|
|
3723
3807
|
}]);
|
|
@@ -3772,7 +3856,7 @@ async function provisionApplicantForRoute(db, chapter, applicationId, fields) {
|
|
|
3772
3856
|
body: {
|
|
3773
3857
|
error: {
|
|
3774
3858
|
code: error.code,
|
|
3775
|
-
message: error.code === "clerk_secret_unavailable" ? "account provisioning credential is unavailable" : "account provisioning failed"
|
|
3859
|
+
message: error.code === "account_already_exists" ? signupConflict.message : error.code === "clerk_secret_unavailable" ? "account provisioning credential is unavailable" : "account provisioning failed"
|
|
3776
3860
|
},
|
|
3777
3861
|
applicationId
|
|
3778
3862
|
}
|
|
@@ -3782,7 +3866,53 @@ async function provisionApplicantForRoute(db, chapter, applicationId, fields) {
|
|
|
3782
3866
|
async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
3783
3867
|
const email = typeof fields.email === "string" ? fields.email : "";
|
|
3784
3868
|
if (!email) return;
|
|
3869
|
+
if (typeof fields.clerkPrivateMetadataSyncedAt === "number" && typeof fields.clerkUserId === "string") return;
|
|
3785
3870
|
const stringValue = (value) => typeof value === "string" ? value : void 0;
|
|
3871
|
+
if (chapter.account !== "none") {
|
|
3872
|
+
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
3873
|
+
if (!secret) throw new AccountProvisioningError("clerk_secret_unavailable", 503);
|
|
3874
|
+
try {
|
|
3875
|
+
const existing = await findSignupAccount(secret, email);
|
|
3876
|
+
if (existing) {
|
|
3877
|
+
if (existing.id !== fields.clerkUserId && !belongsToSignup(existing, applicationId)) {
|
|
3878
|
+
throw new AccountProvisioningError("account_already_exists", 409);
|
|
3879
|
+
}
|
|
3880
|
+
await markPrivateMetadataSynced(db, applicationId, existing.id);
|
|
3881
|
+
return;
|
|
3882
|
+
}
|
|
3883
|
+
const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
|
|
3884
|
+
if (chapter.account === "create") {
|
|
3885
|
+
const result = await createClerkUser(secret, {
|
|
3886
|
+
email,
|
|
3887
|
+
firstName: stringValue(fields.firstName),
|
|
3888
|
+
lastName: stringValue(fields.lastName),
|
|
3889
|
+
privateMetadata
|
|
3890
|
+
});
|
|
3891
|
+
if (!result.ok) throw new AccountProvisioningError(
|
|
3892
|
+
result.existed ? "account_already_exists" : "clerk_account_provisioning_failed",
|
|
3893
|
+
result.existed ? 409 : 502
|
|
3894
|
+
);
|
|
3895
|
+
if (!result.userId) throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3896
|
+
await markPrivateMetadataSynced(db, applicationId, result.userId);
|
|
3897
|
+
} else {
|
|
3898
|
+
if (typeof fields.clerkInvitationSentAt === "number") return;
|
|
3899
|
+
const result = await createClerkInvitation(secret, { email });
|
|
3900
|
+
if (!result.ok) throw new AccountProvisioningError(
|
|
3901
|
+
result.existed ? "account_already_exists" : "clerk_account_provisioning_failed",
|
|
3902
|
+
result.existed ? 409 : 502
|
|
3903
|
+
);
|
|
3904
|
+
await db.transact([{
|
|
3905
|
+
t: "update",
|
|
3906
|
+
ns: "applications",
|
|
3907
|
+
id: applicationId,
|
|
3908
|
+
attrs: { clerkInvitationSentAt: Date.now(), clerkAccountPending: false }
|
|
3909
|
+
}]);
|
|
3910
|
+
}
|
|
3911
|
+
} catch (error) {
|
|
3912
|
+
if (error instanceof AccountProvisioningError) throw error;
|
|
3913
|
+
throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3786
3916
|
const extra = {};
|
|
3787
3917
|
for (const field of chapter.application.crmFields) {
|
|
3788
3918
|
if (fields[field] !== void 0) extra[field] = fields[field];
|
|
@@ -3801,44 +3931,9 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
|
|
|
3801
3931
|
extra
|
|
3802
3932
|
}
|
|
3803
3933
|
);
|
|
3934
|
+
if (chapter.account === "create") await linkProvisionedApplicant(db, chapter.id, applicationId);
|
|
3804
3935
|
} catch {
|
|
3805
3936
|
}
|
|
3806
|
-
if (chapter.account === "none") return;
|
|
3807
|
-
const secret = await getVaultSecret(db, "clerk_secret_key");
|
|
3808
|
-
if (!secret) throw new AccountProvisioningError("clerk_secret_unavailable", 503);
|
|
3809
|
-
try {
|
|
3810
|
-
const privateMetadata = applicantPrivateMetadata(chapter, applicationId, fields);
|
|
3811
|
-
if (chapter.account === "create") {
|
|
3812
|
-
const result = await createClerkUser(secret, {
|
|
3813
|
-
email,
|
|
3814
|
-
firstName: stringValue(fields.firstName),
|
|
3815
|
-
lastName: stringValue(fields.lastName),
|
|
3816
|
-
privateMetadata,
|
|
3817
|
-
repairMetadata: {
|
|
3818
|
-
publicMetadata: publicProfileRemoval,
|
|
3819
|
-
privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
|
|
3820
|
-
}
|
|
3821
|
-
});
|
|
3822
|
-
if (!result.ok || result.existed && !result.refreshed) {
|
|
3823
|
-
throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3824
|
-
}
|
|
3825
|
-
await markPrivateMetadataSynced(db, applicationId, result.userId);
|
|
3826
|
-
await linkProvisionedApplicant(db, chapter.id, applicationId).catch(() => false);
|
|
3827
|
-
} else {
|
|
3828
|
-
const result = await createClerkInvitation(secret, { email });
|
|
3829
|
-
if (!result.ok) throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3830
|
-
if (result.existed) {
|
|
3831
|
-
const refreshed = await updateClerkUserMetadataByEmail(secret, email, {
|
|
3832
|
-
publicMetadata: publicProfileRemoval,
|
|
3833
|
-
privateMetadata: applicantPrivateMetadata(chapter, applicationId, fields, true)
|
|
3834
|
-
});
|
|
3835
|
-
if (refreshed) await markPrivateMetadataSynced(db, applicationId);
|
|
3836
|
-
}
|
|
3837
|
-
}
|
|
3838
|
-
} catch (error) {
|
|
3839
|
-
if (error instanceof AccountProvisioningError) throw error;
|
|
3840
|
-
throw new AccountProvisioningError("clerk_account_provisioning_failed", 502);
|
|
3841
|
-
}
|
|
3842
3937
|
}
|
|
3843
3938
|
async function notifyAdminOfApplication(db, env, chapterId, applicationId, fields) {
|
|
3844
3939
|
try {
|
|
@@ -4001,8 +4096,8 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4001
4096
|
return resumeJoin(url, env, ctx);
|
|
4002
4097
|
}
|
|
4003
4098
|
if (req.method === "POST" && url.pathname === "/api/applications") {
|
|
4004
|
-
const raw2 = await req.
|
|
4005
|
-
if (raw2
|
|
4099
|
+
const raw2 = await readBoundedRequestText(req, chapter.application.bodyCap);
|
|
4100
|
+
if (raw2 instanceof Response) return raw2;
|
|
4006
4101
|
let parsed;
|
|
4007
4102
|
try {
|
|
4008
4103
|
parsed = JSON.parse(raw2);
|
|
@@ -4012,6 +4107,8 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4012
4107
|
}
|
|
4013
4108
|
const submissionId = typeof parsed.submissionId === "string" ? parsed.submissionId : void 0;
|
|
4014
4109
|
const db = ctx.makeDb(env);
|
|
4110
|
+
const signupFailure = await checkSignupEligibility(db, chapter, parsed, submissionId);
|
|
4111
|
+
if (signupFailure) return signupFailure;
|
|
4015
4112
|
const activeControl = chapter.signupControl ? await loadActiveSignupControl(db, chapter.id, env.ODLA_ENV) : null;
|
|
4016
4113
|
const effectiveChapter = activeControl ? { ...chapter, application: controlledApplication(chapter.application, activeControl.document) } : chapter;
|
|
4017
4114
|
const offeredTierIds = typeof parsed.tierId === "string" && parsed.tierId ? (await loadTiers(db, { id: chapter.id })).map((tier) => tier.id) : void 0;
|
|
@@ -4030,7 +4127,7 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4030
4127
|
...effectiveGrant?.decision === "waive_interview" ? { admissionGrant: effectiveGrant } : {},
|
|
4031
4128
|
...giftOffer ? { giftOfferId: giftOffer.id } : {}
|
|
4032
4129
|
});
|
|
4033
|
-
if (!result.ok) return json({ error: result.error }, 400);
|
|
4130
|
+
if (!result.ok) return json({ error: result.error, ...result.code ? { code: result.code } : {} }, result.status ?? 400);
|
|
4034
4131
|
const apps = (await db.query({ applications: { $: { where: { id: result.id }, limit: 1 } } })).applications;
|
|
4035
4132
|
const canonical2 = Array.isArray(apps) ? apps[0] : void 0;
|
|
4036
4133
|
if (!canonical2 || canonical2.groupId !== chapter.id) return json({ error: "application state unavailable" }, 503);
|
|
@@ -4043,13 +4140,14 @@ var handleMember = async (req, url, env, ctx) => {
|
|
|
4043
4140
|
if (normalizeChapterEmail(String(canonical2.email ?? "")) !== normalizeChapterEmail(String(parsed.email ?? ""))) {
|
|
4044
4141
|
return json({ error: "submission belongs to a different application", code: "application_submission_conflict" }, 409);
|
|
4045
4142
|
}
|
|
4046
|
-
if (!result.duplicate) {
|
|
4047
|
-
if (effectiveChapter.sends.adminNotification === "submit") {
|
|
4048
|
-
await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical2);
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
4143
|
const provisioningFailure = await provisionApplicantForRoute(db, effectiveChapter, result.id, canonical2);
|
|
4052
|
-
if (provisioningFailure)
|
|
4144
|
+
if (provisioningFailure) {
|
|
4145
|
+
if (provisioningFailure.status === 409) await rejectUnprovisionedSignup(db, result.id, canonical2.status);
|
|
4146
|
+
return json(provisioningFailure.body, provisioningFailure.status);
|
|
4147
|
+
}
|
|
4148
|
+
if (!result.duplicate && effectiveChapter.sends.adminNotification === "submit") {
|
|
4149
|
+
await notifyAdminOfApplication(db, env, chapter.id, result.id, canonical2);
|
|
4150
|
+
}
|
|
4053
4151
|
const groups = (await db.query({ groups: { $: { where: { id: chapter.id }, limit: 1 } } })).groups;
|
|
4054
4152
|
const group = Array.isArray(groups) ? groups[0] : void 0;
|
|
4055
4153
|
const state = canonical2 && group ? await continueJoin(db, ctx, env, url.origin, canonical2, group) : null;
|
|
@@ -6894,8 +6992,8 @@ async function firstRow2(db, ns, q) {
|
|
|
6894
6992
|
async function startSubscription(req, env, ctx) {
|
|
6895
6993
|
let body;
|
|
6896
6994
|
try {
|
|
6897
|
-
const raw2 = await req.
|
|
6898
|
-
if (raw2
|
|
6995
|
+
const raw2 = await readBoundedRequestText(req, ctx.chapter.application.bodyCap);
|
|
6996
|
+
if (raw2 instanceof Response) return raw2;
|
|
6899
6997
|
body = JSON.parse(raw2);
|
|
6900
6998
|
if (!body || typeof body !== "object" || Array.isArray(body)) throw new TypeError("body");
|
|
6901
6999
|
} catch {
|
|
@@ -11766,8 +11864,8 @@ var handleFormation = async (req, url, env, ctx) => {
|
|
|
11766
11864
|
});
|
|
11767
11865
|
}
|
|
11768
11866
|
if (req.method !== "POST" || url.pathname !== "/api/formation/applications") return null;
|
|
11769
|
-
const raw2 = await req.
|
|
11770
|
-
if (raw2
|
|
11867
|
+
const raw2 = await readBoundedRequestText(req, formation.bodyCap);
|
|
11868
|
+
if (raw2 instanceof Response) return raw2;
|
|
11771
11869
|
let body;
|
|
11772
11870
|
try {
|
|
11773
11871
|
body = JSON.parse(raw2);
|