@odla-ai/chapter 0.25.5 → 0.25.8

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.
@@ -480,6 +480,9 @@ interface ChapterEnv {
480
480
  ODLA_TENANT: string;
481
481
  ODLA_PLATFORM: string;
482
482
  ODLA_APP_ID: string;
483
+ /** Registry lifetime for this exact app/database. Discussion reference
484
+ * reads fail closed when it is absent or stale. */
485
+ ODLA_APP_INCARNATION?: string;
483
486
  ODLA_ENV: string;
484
487
  ODLA_API_KEY: string;
485
488
  SEND_EMAIL?: {
@@ -480,6 +480,9 @@ interface ChapterEnv {
480
480
  ODLA_TENANT: string;
481
481
  ODLA_PLATFORM: string;
482
482
  ODLA_APP_ID: string;
483
+ /** Registry lifetime for this exact app/database. Discussion reference
484
+ * reads fail closed when it is absent or stale. */
485
+ ODLA_APP_INCARNATION?: string;
483
486
  ODLA_ENV: string;
484
487
  ODLA_API_KEY: string;
485
488
  SEND_EMAIL?: {
@@ -111,6 +111,48 @@ function createWorkerContext(options) {
111
111
  // src/worker-routes.ts
112
112
  import { createCrmRoutes } from "@odla-ai/crm";
113
113
 
114
+ // src/discussion-reference-security.ts
115
+ async function authorizeCrmDiscussionReference(req, env, fetcher = fetch) {
116
+ const match = req.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/);
117
+ const appIncarnation = env.ODLA_APP_INCARNATION;
118
+ const expectedTenant = env.ODLA_ENV === "prod" ? env.ODLA_APP_ID : `${env.ODLA_APP_ID}--${env.ODLA_ENV}`;
119
+ if (!match || env.ODLA_TENANT !== expectedTenant || typeof appIncarnation !== "string" || !/^[a-f0-9]{32}$/.test(appIncarnation)) return null;
120
+ const requestUrl = new URL(req.url);
121
+ requestUrl.search = "";
122
+ requestUrl.hash = "";
123
+ const audience = requestUrl.href;
124
+ try {
125
+ const url = new URL(
126
+ "/registry/discuss/reference-authority/verify",
127
+ env.ODLA_PLATFORM
128
+ );
129
+ const response = await fetcher(url, {
130
+ method: "POST",
131
+ headers: { "content-type": "application/json" },
132
+ body: JSON.stringify({
133
+ token: match[1],
134
+ appId: env.ODLA_APP_ID,
135
+ appIncarnation,
136
+ env: env.ODLA_ENV,
137
+ product: "crm",
138
+ projectCapability: "crm.read",
139
+ audience
140
+ }),
141
+ redirect: "manual"
142
+ });
143
+ if (!response.ok) return null;
144
+ const value = await response.json();
145
+ 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;
146
+ return {
147
+ userId: value.actor.id,
148
+ discussionReferenceScope: "all",
149
+ ...value.actor.email ? { email: value.actor.email } : {}
150
+ };
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+
114
156
  // src/application-id.ts
115
157
  var APPLICATION_ID_DOMAIN = "odla-ai/chapter/application/v1:";
116
158
  async function applicationIdForSubmission(submissionId) {
@@ -871,6 +913,7 @@ var handleCrm = async (req, url, env, ctx) => {
871
913
  if (!u || !await ctx.isAdmin(db, u)) return null;
872
914
  return u.email ? { userId: u.userId, email: u.email } : { userId: u.userId };
873
915
  },
916
+ authorizeDiscussionReferences: (r) => authorizeCrmDiscussionReference(r, env),
874
917
  sender: ctx.crmSender(env),
875
918
  from: env.EMAIL_FROM,
876
919
  envName: env.ODLA_ENV,
@@ -1170,7 +1213,11 @@ async function bookSlot(req, env, ctx) {
1170
1213
  let meetingOp;
1171
1214
  try {
1172
1215
  if (decision.reschedule && decision.eventId) {
1173
- await cal.actions.reschedule(decision.eventId, { startAt, endAt });
1216
+ await cal.actions.reschedule(decision.eventId, {
1217
+ idempotencyKey: `meeting:${String(existing?.id)}:reschedule:${startAt}:${endAt}`,
1218
+ startAt,
1219
+ endAt
1220
+ });
1174
1221
  meetUrl = existing?.meetUrl ?? null;
1175
1222
  htmlLink = existing?.htmlLink ?? null;
1176
1223
  meetingOp = { t: "update", ns: "meetings", id: String(existing?.id), attrs: meetingRescheduleUpdate(startAt, endAt) };
@@ -2200,7 +2247,11 @@ var handleAdminMeetingReschedule = async (req, url, env, ctx) => {
2200
2247
  minNoticeMs: cfg.minNoticeHours * 36e5
2201
2248
  });
2202
2249
  if (!isSlotAvailable(slots, startAt)) return json({ error: "slot no longer available", code: "calendar_slot_unavailable" }, 409);
2203
- await cal.actions.reschedule(String(meeting.googleEventId), { startAt, endAt });
2250
+ await cal.actions.reschedule(String(meeting.googleEventId), {
2251
+ idempotencyKey: `meeting:${String(meeting.id)}:reschedule:${startAt}:${endAt}`,
2252
+ startAt,
2253
+ endAt
2254
+ });
2204
2255
  } catch {
2205
2256
  return json({ error: "reschedule failed upstream" }, 502);
2206
2257
  }
@@ -2220,7 +2271,9 @@ var handleAdminMeetingCancel = async (req, url, env, ctx) => {
2220
2271
  if (meeting.status !== "scheduled") return json({ error: "already cancelled" }, 409);
2221
2272
  if (meeting.googleEventId) {
2222
2273
  try {
2223
- await calFor(env).actions.cancel(String(meeting.googleEventId));
2274
+ await calFor(env).actions.cancel(String(meeting.googleEventId), {
2275
+ idempotencyKey: `meeting:${String(meeting.id)}:cancel`
2276
+ });
2224
2277
  } catch {
2225
2278
  return json({ error: "cancel failed upstream" }, 502);
2226
2279
  }