@jskit-ai/connector-google-calendar 0.1.62 → 0.1.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/connector-google-calendar",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "Google Calendar account configuration and event operations for JSKIT connectors.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -11,7 +11,7 @@
11
11
  "./server": "./src/server/provider.js"
12
12
  },
13
13
  "dependencies": {
14
- "@jskit-ai/connectors-core": "0.1.62",
14
+ "@jskit-ai/connectors-core": "0.1.63",
15
15
  "json-rest-schema": "^1.0.17"
16
16
  },
17
17
  "jskit": {
@@ -30,6 +30,6 @@
30
30
  }
31
31
  },
32
32
  "peerDependencies": {
33
- "@jskit-ai/kernel": "0.1.245"
33
+ "@jskit-ai/kernel": "0.1.246"
34
34
  }
35
35
  }
@@ -7,10 +7,10 @@
7
7
  "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
8
8
  },
9
9
  "dependencies": {
10
- "@jskit-ai/connectors-core": "0.1.62",
11
- "@jskit-ai/connector-google-calendar": "0.1.62",
12
- "@jskit-ai/database-runtime": "0.1.245",
13
- "@jskit-ai/database-runtime-mysql": "0.1.243",
10
+ "@jskit-ai/connectors-core": "0.1.63",
11
+ "@jskit-ai/connector-google-calendar": "0.1.63",
12
+ "@jskit-ai/database-runtime": "0.1.246",
13
+ "@jskit-ai/database-runtime-mysql": "0.1.244",
14
14
  "knex": "^3.1.0"
15
15
  }
16
16
  }
@@ -1,14 +1,15 @@
1
1
  import { createSchema } from "json-rest-schema";
2
2
  import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
- import { googleCalendarDefinition } from "../shared/definition.js";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { googleCalendarDefinition, googleScope } from "../shared/definition.js";
4
5
 
5
- const scope = (name) => `https://www.googleapis.com/auth/${name}`;
6
+ const calendarIdField = { type: "string", minLength: 1, maxLength: 1024, defaultTo: "primary" };
6
7
  const listSchema = createSchema({
7
8
  pageToken: { type: "string", maxLength: 4096 },
8
9
  maxResults: { type: "integer", min: 1, max: 250, defaultTo: 100 }
9
10
  });
10
11
  const eventsSchema = createSchema({
11
- calendarId: { type: "string", minLength: 1, maxLength: 1024, defaultTo: "primary" },
12
+ calendarId: calendarIdField,
12
13
  pageToken: { type: "string", maxLength: 4096 },
13
14
  maxResults: { type: "integer", min: 1, max: 2500, defaultTo: 100 },
14
15
  timeMin: { type: "dateTime" },
@@ -21,13 +22,48 @@ function queryUrl(path, input) {
21
22
  return { method: "GET", url: url.href };
22
23
  }
23
24
 
24
- const writeScopes = ["calendar.events", "calendar", "calendar.events.owned", "calendar.app.created"].map(scope);
25
- const readScopes = ["calendar.events.readonly", "calendar.events", "calendar.readonly", "calendar", "calendar.events.owned", "calendar.events.owned.readonly", "calendar.app.created"].map(scope);
25
+ const writeScopes = ["calendar.events", "calendar", "calendar.events.owned", "calendar.app.created"].map(googleScope);
26
+ const readScopes = ["calendar.events.readonly", "calendar.events", "calendar.readonly", "calendar", "calendar.events.owned", "calendar.events.owned.readonly", "calendar.app.created"].map(googleScope);
27
+ const calendarListScopes = ["calendar.calendarlist.readonly", "calendar.calendarlist", "calendar.readonly", "calendar"].map(googleScope);
28
+ const eventListScopes = ["calendar.events.readonly", "calendar.events", "calendar.readonly", "calendar"].map(googleScope);
29
+ const calendarSchema = createSchema({ calendarId: calendarIdField });
26
30
  const targetSchema = createSchema({
27
- calendarId: { type: "string", minLength: 1, maxLength: 1024, defaultTo: "primary" },
31
+ calendarId: calendarIdField,
28
32
  eventId: { type: "string", minLength: 1, maxLength: 1024, required: true }
29
33
  });
30
- function invalid(message) { throw Object.assign(new Error(message), { statusCode: 422 }); }
34
+ function invalid(message) { throw new ConnectorError("connector_input_invalid", message, { statusCode: 422 }); }
35
+ function validateEventTime(point) {
36
+ if (typeof point !== "object" || Array.isArray(point) || Object.keys(point).some(key => !["date", "dateTime", "timeZone"].includes(key))) invalid("Use date or dateTime and optional timeZone.");
37
+ if (Boolean(point.date) === Boolean(point.dateTime)) invalid("Choose an all-day date or a timed dateTime.");
38
+ if (point.date && (typeof point.date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(point.date) ||
39
+ !Number.isFinite(Date.parse(point.date)) || new Date(point.date).toISOString().slice(0, 10) !== point.date)) invalid("Invalid all-day date.");
40
+ if (point.dateTime && (typeof point.dateTime !== "string" || !/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(point.dateTime) ||
41
+ !Number.isFinite(Date.parse(point.dateTime)))) invalid("dateTime requires an explicit timezone offset.");
42
+ if (point.timeZone !== undefined) {
43
+ try {
44
+ if (typeof point.timeZone !== "string") throw new Error();
45
+ new Intl.DateTimeFormat("en", { timeZone: point.timeZone });
46
+ } catch { invalid("Use an IANA timeZone."); }
47
+ }
48
+ }
49
+ function validateRecurrence(body) {
50
+ const { recurrence } = body;
51
+ if (!Array.isArray(recurrence) || recurrence.length > 100 || recurrence.some(line => typeof line !== "string" ||
52
+ line.length > 2048 || !/^(RRULE|RDATE|EXRULE|EXDATE)[:;]/.test(line) || /[\r\n]/.test(line))) invalid("recurrence must be an array of RFC5545 recurrence lines.");
53
+ if (recurrence.length && body.start?.dateTime && (!body.start.timeZone || !body.end.timeZone)) invalid("Timed recurrence requires start/end timeZone.");
54
+ if (recurrence.length && !body.start) invalid("Supply start and end when setting recurrence.");
55
+ }
56
+ function validAttendee(person) {
57
+ return person && typeof person.email === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(person.email) &&
58
+ Object.keys(person).every(key => ["email", "displayName", "optional"].includes(key)) &&
59
+ (person.displayName === undefined || typeof person.displayName === "string") &&
60
+ (person.optional === undefined || typeof person.optional === "boolean");
61
+ }
62
+ function validateAttendees(attendees) {
63
+ if (!Array.isArray(attendees) || attendees.length > 200 || !attendees.every(validAttendee)) {
64
+ invalid("attendees requires up to 200 email entries with optional displayName/optional.");
65
+ }
66
+ }
31
67
  function eventBody(body, creating) {
32
68
  if (!body || typeof body !== "object" || Array.isArray(body)) invalid("event must be an object.");
33
69
  const allowed = ["summary", "description", "location", "start", "end", "recurrence", "attendees", "transparency", "visibility"];
@@ -35,20 +71,10 @@ function eventBody(body, creating) {
35
71
  for (const key of ["summary", "description", "location"]) if (body[key] !== undefined && (typeof body[key] !== "string" || body[key].length > 16384)) invalid(`${key} must be text of at most 16384 characters.`);
36
72
  if (creating && (!body.start || !body.end)) invalid("start and end are required.");
37
73
  if (Boolean(body.start) !== Boolean(body.end)) invalid("Supply start and end together when changing event times.");
38
- for (const point of [body.start, body.end].filter(Boolean)) {
39
- if (typeof point !== "object" || Array.isArray(point) || Object.keys(point).some(key => !["date", "dateTime", "timeZone"].includes(key))) invalid("Use date or dateTime and optional timeZone.");
40
- if (Boolean(point.date) === Boolean(point.dateTime)) invalid("Choose an all-day date or a timed dateTime.");
41
- if (point.date && (typeof point.date !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(point.date) || !Number.isFinite(Date.parse(point.date)) || new Date(point.date).toISOString().slice(0, 10) !== point.date)) invalid("Invalid all-day date.");
42
- if (point.dateTime && (typeof point.dateTime !== "string" || !/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(point.dateTime) || !Number.isFinite(Date.parse(point.dateTime)))) invalid("dateTime requires an explicit timezone offset.");
43
- if (point.timeZone !== undefined) { try { if (typeof point.timeZone !== "string") throw new Error(); new Intl.DateTimeFormat("en", { timeZone: point.timeZone }); } catch { invalid("Use an IANA timeZone."); } }
44
- }
74
+ for (const point of [body.start, body.end].filter(Boolean)) validateEventTime(point);
45
75
  if (body.start && (Boolean(body.start.date) !== Boolean(body.end.date) || Date.parse(body.end.date || body.end.dateTime) <= Date.parse(body.start.date || body.start.dateTime))) invalid("Use matching date types with end after start; all-day end is exclusive.");
46
- if (body.recurrence !== undefined) {
47
- if (!Array.isArray(body.recurrence) || body.recurrence.length > 100 || body.recurrence.some(line => typeof line !== "string" || line.length > 2048 || !/^(RRULE|RDATE|EXRULE|EXDATE)[:;]/.test(line) || /[\r\n]/.test(line))) invalid("recurrence must be an array of RFC5545 recurrence lines.");
48
- if (body.recurrence.length && body.start?.dateTime && (!body.start.timeZone || !body.end.timeZone)) invalid("Timed recurrence requires start/end timeZone.");
49
- if (body.recurrence.length && !body.start) invalid("Supply start and end when setting recurrence.");
50
- }
51
- if (body.attendees !== undefined && (!Array.isArray(body.attendees) || body.attendees.length > 200 || body.attendees.some(person => !person || typeof person.email !== "string" || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(person.email) || Object.keys(person).some(key => !["email", "displayName", "optional"].includes(key)) || (person.displayName !== undefined && typeof person.displayName !== "string") || (person.optional !== undefined && typeof person.optional !== "boolean")))) invalid("attendees requires up to 200 email entries with optional displayName/optional.");
76
+ if (body.recurrence !== undefined) validateRecurrence(body);
77
+ if (body.attendees !== undefined) validateAttendees(body.attendees);
52
78
  if (body.transparency !== undefined && !["opaque", "transparent"].includes(body.transparency)) invalid("Invalid transparency.");
53
79
  if (body.visibility !== undefined && !["default", "public", "private", "confidential"].includes(body.visibility)) invalid("Invalid visibility.");
54
80
  return body;
@@ -62,7 +88,7 @@ function writeEvent(method) {
62
88
  if (!["all", "externalOnly", "none"].includes(sendUpdates)) invalid("Choose sendUpdates explicitly: all, externalOnly or none.");
63
89
  if (ifMatch !== undefined && (typeof ifMatch !== "string" || !ifMatch || ifMatch.length > 1024 || /[\r\n]/.test(ifMatch))) invalid("Invalid event ETag.");
64
90
  if (method === "DELETE" && event !== undefined) invalid("Cancellation has no event body.");
65
- const { calendarId, eventId } = validateSchemaPayload({ schema: method === "POST" ? createSchema({ calendarId: { type: "string", minLength: 1, maxLength: 1024, defaultTo: "primary" } }) : targetSchema, mode: "replace" }, target, { statusCode: 422 });
91
+ const { calendarId, eventId } = validateSchemaPayload({ schema: method === "POST" ? calendarSchema : targetSchema, mode: "replace" }, target, { statusCode: 422 });
66
92
  const request = queryUrl(`calendars/${encodeURIComponent(calendarId)}/events${eventId ? `/${encodeURIComponent(eventId)}` : ""}`, { sendUpdates });
67
93
  return { ...request, method, ...(ifMatch ? { headers: { "If-Match": ifMatch } } : {}), ...(method !== "DELETE" ? { body: eventBody(event, method === "POST") } : {}) };
68
94
  }
@@ -93,14 +119,14 @@ const googleCalendarProvider = Object.freeze({
93
119
  }
94
120
  },
95
121
  "calendars.list": {
96
- scopes: [scope("calendar.calendarlist.readonly"), scope("calendar.calendarlist"), scope("calendar.readonly"), scope("calendar")],
122
+ scopes: calendarListScopes,
97
123
  validateResult: (result) => result?.kind === "calendar#calendarList" && (result.items === undefined || Array.isArray(result.items)),
98
124
  request(input) {
99
125
  return queryUrl("users/me/calendarList", validateSchemaPayload({ schema: listSchema, mode: "replace" }, input, { statusCode: 422 }));
100
126
  }
101
127
  },
102
128
  "events.list": {
103
- scopes: [scope("calendar.events.readonly"), scope("calendar.events"), scope("calendar.readonly"), scope("calendar")],
129
+ scopes: eventListScopes,
104
130
  validateResult: (result) => result?.kind === "calendar#events" && (result.items === undefined || Array.isArray(result.items)),
105
131
  request(input) {
106
132
  const { calendarId, ...query } = validateSchemaPayload({ schema: eventsSchema, mode: "replace" }, input, { statusCode: 422 });
@@ -1,9 +1,8 @@
1
- const GOOGLE_SCOPE_BASE = "https://www.googleapis.com/auth/";
1
+ const googleScope = (name) => `https://www.googleapis.com/auth/${name}`;
2
2
  const googleCalendarDefinition = Object.freeze({
3
3
  id: "google-calendar",
4
4
  name: "Google Calendar",
5
5
  description: "Read calendars and create, edit or cancel events.",
6
- categories: ["Google", "Productivity"],
7
6
  accountModes: ["shared", "per-user", "assistant"],
8
7
  authenticationMethods: ["oauth2"],
9
8
  setup: {
@@ -24,29 +23,29 @@ const googleCalendarDefinition = Object.freeze({
24
23
  ]
25
24
  },
26
25
  scopes: [
27
- { value: `${GOOGLE_SCOPE_BASE}calendar.calendarlist.readonly`, label: "List calendars", recommended: true },
28
- { value: `${GOOGLE_SCOPE_BASE}calendar.events.readonly`, label: "Read events", recommended: true },
29
- { value: `${GOOGLE_SCOPE_BASE}calendar.readonly`, label: "Read all calendar data" },
30
- { value: `${GOOGLE_SCOPE_BASE}calendar`, label: "Manage calendars and events" },
31
- { value: `${GOOGLE_SCOPE_BASE}calendar.calendarlist`, label: "Manage calendar subscriptions" },
32
- { value: `${GOOGLE_SCOPE_BASE}calendar.events`, label: "Manage events" },
33
- { value: `${GOOGLE_SCOPE_BASE}calendar.freebusy`, label: "Read your availability" },
34
- { value: `${GOOGLE_SCOPE_BASE}calendar.settings.readonly`, label: "Read calendar settings" },
35
- { value: `${GOOGLE_SCOPE_BASE}calendar.events.owned`, label: "Manage events in calendars you own" },
36
- { value: `${GOOGLE_SCOPE_BASE}calendar.events.owned.readonly`, label: "Read events in calendars you own" },
37
- { value: `${GOOGLE_SCOPE_BASE}calendar.events.freebusy`, label: "Read availability in accessible calendars" },
38
- { value: `${GOOGLE_SCOPE_BASE}calendar.app.created`, label: "Create and manage app-created calendars" },
39
- { value: `${GOOGLE_SCOPE_BASE}calendar.calendars`, label: "Manage calendar properties" },
40
- { value: `${GOOGLE_SCOPE_BASE}calendar.calendars.readonly`, label: "Read calendar properties" },
41
- { value: `${GOOGLE_SCOPE_BASE}calendar.acls`, label: "Manage calendar sharing permissions" },
42
- { value: `${GOOGLE_SCOPE_BASE}calendar.acls.readonly`, label: "Read calendar sharing permissions" },
43
- { value: `${GOOGLE_SCOPE_BASE}calendar.events.public.readonly`, label: "Read public calendar events" },
44
- { value: `${GOOGLE_SCOPE_BASE}calendar.addons.execute`, label: "Run a Calendar add-on" },
45
- { value: `${GOOGLE_SCOPE_BASE}calendar.addons.current.event.read`, label: "Read the event open in a Calendar add-on" },
46
- { value: `${GOOGLE_SCOPE_BASE}calendar.addons.current.event.write`, label: "Edit the event open in a Calendar add-on" }
26
+ { value: googleScope("calendar.calendarlist.readonly"), label: "List calendars", recommended: true },
27
+ { value: googleScope("calendar.events.readonly"), label: "Read events", recommended: true },
28
+ { value: googleScope("calendar.readonly"), label: "Read all calendar data" },
29
+ { value: googleScope("calendar"), label: "Manage calendars and events" },
30
+ { value: googleScope("calendar.calendarlist"), label: "Manage calendar subscriptions" },
31
+ { value: googleScope("calendar.events"), label: "Manage events" },
32
+ { value: googleScope("calendar.freebusy"), label: "Read your availability" },
33
+ { value: googleScope("calendar.settings.readonly"), label: "Read calendar settings" },
34
+ { value: googleScope("calendar.events.owned"), label: "Manage events in calendars you own" },
35
+ { value: googleScope("calendar.events.owned.readonly"), label: "Read events in calendars you own" },
36
+ { value: googleScope("calendar.events.freebusy"), label: "Read availability in accessible calendars" },
37
+ { value: googleScope("calendar.app.created"), label: "Create and manage app-created calendars" },
38
+ { value: googleScope("calendar.calendars"), label: "Manage calendar properties" },
39
+ { value: googleScope("calendar.calendars.readonly"), label: "Read calendar properties" },
40
+ { value: googleScope("calendar.acls"), label: "Manage calendar sharing permissions" },
41
+ { value: googleScope("calendar.acls.readonly"), label: "Read calendar sharing permissions" },
42
+ { value: googleScope("calendar.events.public.readonly"), label: "Read public calendar events" },
43
+ { value: googleScope("calendar.addons.execute"), label: "Run a Calendar add-on" },
44
+ { value: googleScope("calendar.addons.current.event.read"), label: "Read the event open in a Calendar add-on" },
45
+ { value: googleScope("calendar.addons.current.event.write"), label: "Edit the event open in a Calendar add-on" }
47
46
  ],
48
47
  settingsFields: [],
49
48
  documentationUrl: "https://developers.google.com/workspace/calendar/api/guides/overview"
50
49
  });
51
50
 
52
- export { googleCalendarDefinition };
51
+ export { googleCalendarDefinition, googleScope };