@odla-ai/chapter 0.10.1 → 0.11.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.
@@ -154,6 +154,12 @@ interface ChapterApplication {
154
154
  defaultMaxLen?: number;
155
155
  /** Max JSON request body in bytes. Default 32768. */
156
156
  bodyCap?: number;
157
+ /** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
158
+ * writing a row with no consent record. Default `false` for back-compat —
159
+ * but turn it on if the disclaimer is a compliance record: a missing ack is
160
+ * otherwise silent, permanent and unreconstructible. Failure is deterministic
161
+ * and surfaces on the first test submit, not intermittently in production. */
162
+ requireDisclaimerAck?: boolean;
157
163
  }
158
164
  /** The fully-resolved application config carried on the {@link Chapter}. */
159
165
  interface ResolvedApplication {
@@ -162,6 +168,7 @@ interface ResolvedApplication {
162
168
  maxLen: Record<string, number>;
163
169
  defaultMaxLen: number;
164
170
  bodyCap: number;
171
+ requireDisclaimerAck: boolean;
165
172
  }
166
173
  /** The `defineChapter()` config a site fills in. */
167
174
  interface ChapterConfig {
@@ -154,6 +154,12 @@ interface ChapterApplication {
154
154
  defaultMaxLen?: number;
155
155
  /** Max JSON request body in bytes. Default 32768. */
156
156
  bodyCap?: number;
157
+ /** Reject a submit that carries no truthy `disclaimerAck` (400), instead of
158
+ * writing a row with no consent record. Default `false` for back-compat —
159
+ * but turn it on if the disclaimer is a compliance record: a missing ack is
160
+ * otherwise silent, permanent and unreconstructible. Failure is deterministic
161
+ * and surfaces on the first test submit, not intermittently in production. */
162
+ requireDisclaimerAck?: boolean;
157
163
  }
158
164
  /** The fully-resolved application config carried on the {@link Chapter}. */
159
165
  interface ResolvedApplication {
@@ -162,6 +168,7 @@ interface ResolvedApplication {
162
168
  maxLen: Record<string, number>;
163
169
  defaultMaxLen: number;
164
170
  bodyCap: number;
171
+ requireDisclaimerAck: boolean;
165
172
  }
166
173
  /** The `defineChapter()` config a site fills in. */
167
174
  interface ChapterConfig {
@@ -157,6 +157,9 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
157
157
  }
158
158
 
159
159
  // src/member.ts
160
+ function hasDisclaimerAck(fields) {
161
+ return fields.disclaimerAck === true || fields.disclaimerAck === "true";
162
+ }
160
163
  var IDENTITY_FIELDS = /* @__PURE__ */ new Set(["email", "firstName", "lastName"]);
161
164
  function applicantProfile(chapter, fields) {
162
165
  const profile = {};
@@ -179,6 +182,10 @@ async function submitApplication(db, chapter, fields, opts) {
179
182
  const cap = app.maxLen[f] ?? app.defaultMaxLen;
180
183
  if (typeof v === "string" && v.length > cap) return { ok: false, error: `${f} exceeds ${cap} characters` };
181
184
  }
185
+ const acked = hasDisclaimerAck(fields);
186
+ if (app.requireDisclaimerAck && !acked) {
187
+ return { ok: false, error: "disclaimerAck is required" };
188
+ }
182
189
  const id = opts.newId();
183
190
  const row = { id, status: chapter.pipeline.initial, createdAt: opts.now };
184
191
  for (const f of [...app.required, ...app.optional]) {
@@ -186,12 +193,12 @@ async function submitApplication(db, chapter, fields, opts) {
186
193
  }
187
194
  if (fields.focus !== void 0) row.focus = fields.focus;
188
195
  if (opts.groupId) row.groupId = opts.groupId;
189
- if (fields.disclaimerAck === true || fields.disclaimerAck === "true") row.disclaimerAckAt = opts.now;
196
+ if (acked) row.disclaimerAckAt = opts.now;
190
197
  const { duplicate } = await db.transact(
191
198
  [{ t: "update", ns: "applications", id, attrs: row }],
192
199
  opts.submissionId ? { mutationId: `join:${opts.submissionId}` } : void 0
193
200
  );
194
- return { ok: true, id, duplicate, status: chapter.pipeline.initial };
201
+ return { ok: true, id, duplicate, status: chapter.pipeline.initial, disclaimerAckAt: acked ? opts.now : null };
195
202
  }
196
203
  function joinConfig(group, paymentsReady) {
197
204
  return {
@@ -647,7 +654,14 @@ var handleMember = async (req, url, env, ctx) => {
647
654
  }
648
655
  await provisionApplicant(db, chapter, result.id, parsed);
649
656
  }
650
- return json({ id: result.id, duplicate: result.duplicate, status: result.status });
657
+ return json({
658
+ id: result.id,
659
+ duplicate: result.duplicate,
660
+ status: result.status,
661
+ // Echoed so a site can see (and assert in an integration test) whether its
662
+ // join page actually posted the ack. Absent consent is otherwise invisible.
663
+ disclaimerAckAt: result.disclaimerAckAt
664
+ });
651
665
  }
652
666
  return null;
653
667
  };