@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.
- package/dist/{chunk-5SWVMJZN.js → chunk-4FW35E3P.js} +14 -1
- package/dist/chunk-4FW35E3P.js.map +1 -0
- package/dist/ui/admin/index.js +1 -1
- package/dist/ui/index.js +1 -1
- package/dist/worker/index.cjs +56 -3
- package/dist/worker/index.cjs.map +1 -1
- package/dist/worker/index.d.cts +3 -0
- package/dist/worker/index.d.ts +3 -0
- package/dist/worker/index.js +56 -3
- package/dist/worker/index.js.map +1 -1
- package/package.json +2 -2
- package/runbooks/adopt-existing.md +10 -1
- package/runbooks/greenfield.md +2 -1
- package/dist/chunk-5SWVMJZN.js.map +0 -1
package/dist/worker/index.d.cts
CHANGED
|
@@ -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?: {
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -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?: {
|
package/dist/worker/index.js
CHANGED
|
@@ -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, {
|
|
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), {
|
|
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
|
}
|