@odla-ai/chapter 0.10.1 → 0.12.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.
@@ -184,15 +184,28 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
184
184
  }
185
185
 
186
186
  // src/member.ts
187
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
188
+ function isValidEmail(value) {
189
+ return typeof value === "string" && EMAIL_RE.test(value);
190
+ }
191
+ function clampArray(value, max) {
192
+ if (!Array.isArray(value)) return value;
193
+ return value.filter((x) => typeof x === "string" || typeof x === "number" || typeof x === "boolean").slice(0, max);
194
+ }
195
+ function hasDisclaimerAck(fields) {
196
+ return fields.disclaimerAck === true || fields.disclaimerAck === "true";
197
+ }
187
198
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
188
199
  function applicantProfile(chapter, fields) {
200
+ const app = chapter.application;
201
+ const allowed = (f) => app.profileFields === null || app.profileFields.includes(f);
189
202
  const profile = {};
190
- for (const f of [...chapter.application.required, ...chapter.application.optional]) {
191
- if (IDENTITY_FIELDS.has(f)) continue;
203
+ for (const f of [...app.required, ...app.optional]) {
204
+ if (IDENTITY_FIELDS.has(f) || !allowed(f)) continue;
192
205
  const v = fields[f];
193
206
  if (typeof v === "string" && v.trim() !== "") profile[f] = v.trim();
194
207
  }
195
- if (fields.focus !== void 0) profile.focus = fields.focus;
208
+ if (fields.focus !== void 0 && allowed("focus")) profile.focus = clampArray(fields.focus, app.maxArrayLen);
196
209
  return Object.keys(profile).length > 0 ? profile : void 0;
197
210
  }
198
211
  async function submitApplication(db, chapter, fields, opts) {
@@ -206,19 +219,26 @@ async function submitApplication(db, chapter, fields, opts) {
206
219
  const cap = app.maxLen[f] ?? app.defaultMaxLen;
207
220
  if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
208
221
  }
222
+ if (app.validateEmail && typeof fields.email === "string" && !isValidEmail(fields.email)) {
223
+ return { ok: false, error: "email must be a valid email address" };
224
+ }
225
+ const acked = hasDisclaimerAck(fields);
226
+ if (app.requireDisclaimerAck && !acked) {
227
+ return { ok: false, error: "disclaimerAck is required" };
228
+ }
209
229
  const id = opts.newId();
210
230
  const row = { id, status: chapter.pipeline.initial, createdAt: opts.now };
211
231
  for (const f of [...app.required, ...app.optional]) {
212
232
  if (typeof fields[f] === "string") row[f] = fields[f].trim();
213
233
  }
214
- if (fields.focus !== void 0) row.focus = fields.focus;
234
+ if (fields.focus !== void 0) row.focus = clampArray(fields.focus, app.maxArrayLen);
215
235
  if (opts.groupId) row.groupId = opts.groupId;
216
- if (fields.disclaimerAck === true || fields.disclaimerAck === "true") row.disclaimerAckAt = opts.now;
236
+ if (acked) row.disclaimerAckAt = opts.now;
217
237
  const { duplicate } = await db.transact(
218
238
  [{ t: "update", ns: "applications", id, attrs: row }],
219
239
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
220
240
  );
221
- return { ok: true, id, duplicate, status: chapter.pipeline.initial };
241
+ return { ok: true, id, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
222
242
  }
223
243
  function joinConfig(group, paymentsReady) {
224
244
  return {
@@ -264,7 +284,7 @@ async function projectSharedRecord(deps, person) {
264
284
  return upsertPerson(deps, { email: person.email, input: sharedPersonInput(person), mutationId: `share:${person.hubRecordId}` });
265
285
  }
266
286
  async function projectApplicant(deps, applicant) {
267
- const input = sharedPersonInput({
287
+ const base = sharedPersonInput({
268
288
  email: applicant.email,
269
289
  firstName: applicant.firstName,
270
290
  lastName: applicant.lastName,
@@ -272,7 +292,14 @@ async function projectApplicant(deps, applicant) {
272
292
  linkedin: applicant.linkedin,
273
293
  hubRecordId: applicant.applicationId
274
294
  });
275
- return upsertPerson(deps, { email: applicant.email, input, mutationId: `apply:${applicant.applicationId}` });
295
+ const mutationId = `apply:${applicant.applicationId}`;
296
+ const extra = applicant.extra ?? {};
297
+ if (Object.keys(extra).length === 0) return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
298
+ try {
299
+ return await upsertPerson(deps, { email: applicant.email, input: { ...base, ...extra }, mutationId });
300
+ } catch {
301
+ return upsertPerson(deps, { email: applicant.email, input: base, mutationId });
302
+ }
276
303
  }
277
304
 
278
305
  // src/scheduling.ts
@@ -516,10 +543,14 @@ async function provisionApplicant(db, chapter, applicationId, fields) {
516
543
  const email = typeof fields.email === "string" ? fields.email : "";
517
544
  if (!email) return;
518
545
  const s = (v) => typeof v === "string" ? v : void 0;
546
+ const extra = {};
547
+ for (const f of chapter.application.crmFields) {
548
+ if (fields[f] !== void 0) extra[f] = fields[f];
549
+ }
519
550
  try {
520
551
  await projectApplicant(
521
552
  { crm: chapter.crm, db, now: () => Date.now(), newId: () => crypto.randomUUID() },
522
- { applicationId, email, firstName: s(fields.firstName), lastName: s(fields.lastName), phone: s(fields.phone), linkedin: s(fields.linkedin) }
553
+ { applicationId, email, firstName: s(fields.firstName), lastName: s(fields.lastName), phone: s(fields.phone), linkedin: s(fields.linkedin), extra }
523
554
  );
524
555
  } catch {
525
556
  }
@@ -674,7 +705,14 @@ var handleMember = async (req, url, env, ctx) => {
674
705
  }
675
706
  await provisionApplicant(db, chapter, result.id, parsed);
676
707
  }
677
- return json({ id: result.id, duplicate: result.duplicate, status: result.status });
708
+ return json({
709
+ id: result.id,
710
+ duplicate: result.duplicate,
711
+ status: result.status,
712
+ // Echoed so a site can see (and assert in an integration test) whether its
713
+ // join page actually posted the ack. Absent consent is otherwise invisible.
714
+ disclaimerAckAt: result.disclaimerAckAt
715
+ });
678
716
  }
679
717
  return null;
680
718
  };