@okostream/mcp 0.9.2 → 0.9.3

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/tools.js CHANGED
@@ -868,13 +868,6 @@ function eventLinks(eventId) {
868
868
  analyticsUrl: analyticsUrl(eventId),
869
869
  };
870
870
  }
871
- function assertEventReadable(data, uid) {
872
- const access = Array.isArray(data.accessibleBy) ? data.accessibleBy : [];
873
- const editors = Array.isArray(data.editors) ? data.editors : [];
874
- if (data.ownerId !== uid && !access.includes(uid) && !editors.includes(uid)) {
875
- throw new Error("Event is not accessible to this organizer.");
876
- }
877
- }
878
871
  const EVENT_ACCESS_FIELD_BY_MODE = {
879
872
  read: "accessibleBy",
880
873
  edit: "editors",
@@ -886,22 +879,44 @@ const EVENT_ACCESS_FIELD_BY_MODE = {
886
879
  moderate: "chatModerators",
887
880
  notify: "notificationSenders",
888
881
  };
889
- function assertEventAccess(data, uid, mode) {
882
+ function grantAllowsMode(data, mode) {
890
883
  const field = EVENT_ACCESS_FIELD_BY_MODE[mode];
891
- const principals = Array.isArray(data[field]) ? data[field] : [];
892
- if (data.ownerId !== uid && !principals.includes(uid)) {
893
- throw new Error(`Event does not grant ${mode} access to this organizer.`);
894
- }
884
+ return asRecord(data.capabilities)[field] === true;
885
+ }
886
+ function legacyEventAllowsMode(data, uid, mode) {
887
+ if (data.ownerId === uid)
888
+ return true;
889
+ const fields = mode === "read"
890
+ ? ["accessibleBy", "editors"]
891
+ : [EVENT_ACCESS_FIELD_BY_MODE[mode]];
892
+ return fields.some((field) => (Array.isArray(data[field]) && data[field].includes(uid)));
893
+ }
894
+ function isFirestorePermissionDenied(error) {
895
+ if (!error || typeof error !== "object")
896
+ return false;
897
+ const code = error.code;
898
+ return code === "permission-denied"
899
+ || code === "firestore/permission-denied"
900
+ || code === "PERMISSION_DENIED";
895
901
  }
896
902
  async function readEvent(eventId, uid, mode = "read") {
897
- const snap = await getDoc(doc(firestore, "events", eventId));
903
+ const [snap, grant] = await Promise.all([
904
+ getDoc(doc(firestore, "events", eventId)),
905
+ getDoc(doc(firestore, "eventAccess", uid, "grants", eventId)).catch((error) => {
906
+ if (!isFirestorePermissionDenied(error))
907
+ throw error;
908
+ return null;
909
+ }),
910
+ ]);
898
911
  if (!snap.exists())
899
912
  throw new Error("Event not found.");
900
913
  const data = snap.data();
901
- if (mode === "read")
902
- assertEventReadable(data, uid);
903
- else
904
- assertEventAccess(data, uid, mode);
914
+ const allowed = grant === null
915
+ ? legacyEventAllowsMode(data, uid, mode)
916
+ : data.ownerId === uid || (grant.exists() && grantAllowsMode(grant.data(), mode));
917
+ if (!allowed) {
918
+ throw new Error(`Event does not grant ${mode} access to this organizer.`);
919
+ }
905
920
  if (data.trashedAt && mode !== "read" && mode !== "delete") {
906
921
  throw new Error("Event is in trash. Restore it before making changes or live actions.");
907
922
  }
@@ -1047,7 +1062,7 @@ function initialEventAccess(uid) {
1047
1062
  notificationSenders: [uid],
1048
1063
  };
1049
1064
  }
1050
- async function addEventWithRtdbMirror(uid, workspaceId, eventData) {
1065
+ async function addEventWithRtdbMirror(eventData) {
1051
1066
  const eventRef = await addDoc(collection(firestore, "events"), eventData);
1052
1067
  try {
1053
1068
  const ensureRuntime = httpsCallable(functions, "ensureEventRuntime");
@@ -1466,7 +1481,7 @@ export function registerTools(server) {
1466
1481
  args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "permissions: 9 workspace permission booleans.", "confirm: must be true."],
1467
1482
  returns: "workspaceId, uid, and updated=true.",
1468
1483
  examples: ["Use to change an existing member's permissions, or direct-add a known uid."],
1469
- notes: ["Changing a share triggers OKO CF18 fan-out to event access, RTDB workspaceMembers, and chat roles."],
1484
+ notes: ["Changing a share materializes lightweight event grants and updates RTDB workspace/chat mirrors."],
1470
1485
  }),
1471
1486
  inputSchema: {
1472
1487
  workspaceId: z.string().min(1),
@@ -1498,7 +1513,7 @@ export function registerTools(server) {
1498
1513
  args: ["workspaceId: workspace owned by the organizer.", "uid: member uid.", "confirm: must be true."],
1499
1514
  returns: "workspaceId, uid, removed=true.",
1500
1515
  examples: ["Use after listing workspace access and confirming the exact uid."],
1501
- notes: ["Removing a share triggers OKO CF18 reverse fan-out."],
1516
+ notes: ["Removing a share revokes its event grants and RTDB workspace/chat mirrors."],
1502
1517
  }),
1503
1518
  inputSchema: { workspaceId: z.string().min(1), uid: z.string().min(1), confirm: z.boolean().optional().describe("Required true for member removal.") },
1504
1519
  outputSchema: z.object({ workspaceId: z.string(), uid: z.string(), removed: z.boolean() }).passthrough(),
@@ -1583,7 +1598,7 @@ export function registerTools(server) {
1583
1598
  args: [
1584
1599
  "limit: maximum events to return, default 30, max 100.",
1585
1600
  "cursor: timestamp cursor returned as nextCursor by the previous page.",
1586
- "workspaceId: optional client-side workspace filter.",
1601
+ "workspaceId: optional workspace filter.",
1587
1602
  ],
1588
1603
  returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
1589
1604
  examples: ["Use limit=10 first; if hasMore is true, call again with cursor=nextCursor."],
@@ -1598,17 +1613,42 @@ export function registerTools(server) {
1598
1613
  }, async (args) => {
1599
1614
  const { uid } = await authed(["events:read"]);
1600
1615
  const constraints = [
1601
- where("accessibleBy", "array-contains", uid),
1616
+ ...(args.workspaceId ? [where("workspaceId", "==", args.workspaceId)] : []),
1602
1617
  orderBy("createdAt", "desc"),
1603
1618
  limit(args.limit + 1),
1604
1619
  ];
1605
1620
  if (args.cursor)
1606
- constraints.splice(2, 0, startAfter(parseTimestampCursor(args.cursor)));
1607
- const snap = await getDocs(query(collection(firestore, "events"), ...constraints));
1608
- const { page, hasMore } = pickPage(snap.docs, args.limit);
1609
- let events = page.map((item) => ({ id: item.id, ...serialize(item.data()) }));
1610
- if (args.workspaceId)
1611
- events = events.filter((event) => event.workspaceId === args.workspaceId);
1621
+ constraints.splice(constraints.length - 1, 0, startAfter(parseTimestampCursor(args.cursor)));
1622
+ const snap = await getDocs(query(collection(firestore, "eventAccess", uid, "grants"), ...constraints)).catch((error) => {
1623
+ if (!isFirestorePermissionDenied(error))
1624
+ throw error;
1625
+ return null;
1626
+ });
1627
+ if (snap !== null) {
1628
+ const { page, hasMore } = pickPage(snap.docs, args.limit);
1629
+ const eventDocuments = await Promise.all(page.map((item) => (getDoc(doc(firestore, "events", item.id)))));
1630
+ const events = eventDocuments.flatMap((item) => (item.exists()
1631
+ ? [{ id: item.id, ...serialize(item.data()) }]
1632
+ : []));
1633
+ const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().createdAt) : null;
1634
+ return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) });
1635
+ }
1636
+ const legacyConstraints = [
1637
+ where("accessibleBy", "array-contains", uid),
1638
+ orderBy("createdAt", "desc"),
1639
+ limit(args.limit + 1),
1640
+ ];
1641
+ if (args.cursor) {
1642
+ legacyConstraints.splice(legacyConstraints.length - 1, 0, startAfter(parseTimestampCursor(args.cursor)));
1643
+ }
1644
+ const legacySnap = await getDocs(query(collection(firestore, "events"), ...legacyConstraints));
1645
+ const { page, hasMore } = pickPage(legacySnap.docs, args.limit);
1646
+ const events = page
1647
+ .map((item) => ({
1648
+ id: item.id,
1649
+ ...serialize(item.data()),
1650
+ }))
1651
+ .filter((event) => !args.workspaceId || event.workspaceId === args.workspaceId);
1612
1652
  const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().createdAt) : null;
1613
1653
  return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) });
1614
1654
  });
@@ -1679,7 +1719,7 @@ export function registerTools(server) {
1679
1719
  const workspaceId = args.workspaceId ?? uid;
1680
1720
  await assertCanCreateInWorkspace(uid, workspaceId);
1681
1721
  const video = normalizeVideo(args.videoUrl);
1682
- const docRef = await addEventWithRtdbMirror(uid, workspaceId, {
1722
+ const docRef = await addEventWithRtdbMirror({
1683
1723
  ownerId: uid,
1684
1724
  workspaceId,
1685
1725
  ...initialEventAccess(uid),
@@ -2222,7 +2262,7 @@ export function registerTools(server) {
2222
2262
  const source = await readEvent(eventId, uid, "read");
2223
2263
  const workspaceId = targetWorkspaceId ?? safeString(source.workspaceId, uid);
2224
2264
  await assertCanCreateInWorkspace(uid, workspaceId);
2225
- const newRef = await addEventWithRtdbMirror(uid, workspaceId, {
2265
+ const newRef = await addEventWithRtdbMirror({
2226
2266
  ownerId: uid,
2227
2267
  workspaceId,
2228
2268
  ...initialEventAccess(uid),
package/dist/utils.js CHANGED
@@ -19,9 +19,6 @@ export function serialize(value) {
19
19
  }
20
20
  return out;
21
21
  }
22
- export function docJson(snap) {
23
- return { id: snap.id, ...serialize(snap.data()) };
24
- }
25
22
  function compactForText(value, depth = 0) {
26
23
  if (typeof value === "string" && value.length > MAX_COMPACT_STRING_LENGTH) {
27
24
  return `${value.slice(0, MAX_COMPACT_STRING_LENGTH)}... [truncated ${value.length - MAX_COMPACT_STRING_LENGTH} chars]`;
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/generateMcpVersion.mjs from mcp/package.json. Do not edit.
2
- export const OKO_MCP_VERSION = "0.9.2";
2
+ export const OKO_MCP_VERSION = "0.9.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {