@jskit-ai/connector-google-calendar 0.1.1

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/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Google Calendar
2
+
3
+ Import `googleCalendarDefinition` from `@jskit-ai/connector-google-calendar/shared`
4
+ for configuration and UI. Import `googleCalendarProvider` from
5
+ `@jskit-ai/connector-google-calendar/server` in server code.
6
+
7
+ The initial operations are `calendars.list` and `events.list`. Both return the
8
+ provider's page, including `nextPageToken`; callers decide whether and when to
9
+ fetch another page. Event lists accept `calendarId` (default `primary`),
10
+ `pageToken`, `maxResults`, `timeMin` and `timeMax`. They expand recurring events
11
+ and order by start time. Writes are available through `events.create`, `events.update` (PATCH) and `events.cancel`; `events.get` retrieves one event and its ETag. See the setup guide for inputs, scopes and attendee effects.
12
+
13
+ The recommended scopes are `calendar.calendarlist.readonly` and
14
+ `calendar.events.readonly`, both under `https://www.googleapis.com/auth/`.
15
+ Connection verification lists calendars, so retain a permission accepted by
16
+ `calendars.list`. Additional Calendar scopes are represented in metadata for
17
+ application-authored operations; selecting them does not implement those
18
+ operations. Calendar add-on scopes also require a Calendar add-on host.
19
+
20
+ Account ownership modes are shared, per app user, and assistant. The
21
+ application's authorization policy determines who can use each connection.
22
+ This package does not replace the application's login or user identity system.
23
+
24
+ Start with [provider setup](docs/setup.md) and the
25
+ [app-owned CLI pattern](patterns/calendar-cli/PATTERN.md).
26
+
27
+ ## Verification state
28
+
29
+ Simulated Google responses verify request construction, grants, token exchange,
30
+ refresh and failures through the shared runtime tests. No Google account has
31
+ been connected during implementation. Live consent, Workspace administrator
32
+ restrictions and production verification remain open. Each application owns its
33
+ registration, callback and runtime grants; no shared editor gateway is required.
34
+ Configuration and UI metadata are not claims of live provider approval.
package/docs/setup.md ADDED
@@ -0,0 +1,144 @@
1
+ # Register Google Calendar access
2
+
3
+ Checked against Google's documentation on 2026-09-08. Portal labels and provider
4
+ requirements can change; the links below are the source for those details.
5
+
6
+ ## Own registration
7
+
8
+ 1. Open [Google Cloud Console](https://console.cloud.google.com/). Select the
9
+ target project, or use the project selector's **New Project** action. Choose
10
+ your organization/location when required.
11
+ 2. Open **APIs & Services → Library**, find **Google Calendar API**, then enable
12
+ it for that project. [API enablement](https://developers.google.com/workspace/guides/enable-apis).
13
+ 3. Open **Google Auth platform → Branding**. On a new project, choose **Get
14
+ Started**. Enter the app name and support email; choose the audience, provide
15
+ the contact email, review the terms and create the configuration.
16
+ 4. Use **Audience** to add your test accounts for an external app in testing.
17
+ Use **Data Access → Add or Remove Scopes** to select the two recommended
18
+ read permissions listed in this package's README. Complete branding/domain
19
+ information required for your intended audience.
20
+ [Consent configuration](https://developers.google.com/workspace/guides/configure-oauth-consent).
21
+ 5. Open **Google Auth platform → Clients → Create Client**. Choose **Web
22
+ application**, name it, and add the exact backend callback under
23
+ **Authorized redirect URIs**. For the CLI example use
24
+ `http://127.0.0.1:8080/integrations/google/callback`. Create the client.
25
+ [Client creation](https://developers.google.com/workspace/guides/create-credentials).
26
+ 6. Put its client ID into `registrations.google.clientId`. Store the client
27
+ secret on the server as `GOOGLE_CLIENT_SECRET`, and set
28
+ `GOOGLE_CALLBACK_URL` to the exact registered callback. Keep only the
29
+ `env:` references in `integrations.json`. The server-side authorization
30
+ code flow uses the secret during exchange; browser code must not receive
31
+ it. [Web-server OAuth](https://developers.google.com/identity/protocols/oauth2/web-server).
32
+ 7. Run the application and connect a test account. Verify calendar listing,
33
+ event listing and reconnect behavior. Before public use, complete Google's
34
+ applicable publishing/verification requirements; a test connection does
35
+ not establish production approval.
36
+
37
+ ## Application and environment ownership
38
+
39
+ The application owns its registration, client secret and callback. Hosted and
40
+ installed editors configure the same application-owned fields; neither supplies
41
+ a shared Google client or gateway. Use `source: "own"` and keep secret values in
42
+ the application's environment. See the
43
+ [callback guide](../../connectors-core/docs/oauth-callbacks.md) for callback
44
+ routes, domain changes and preserving runtime state when moving the app.
45
+
46
+ The chosen Google Cloud project owns Calendar project quotas. Two OAuth clients
47
+ inside one project do not isolate those quotas. Google also applies other limits;
48
+ check the actual project's [Calendar usage limits](https://developers.google.com/workspace/calendar/api/guides/quota).
49
+ Changing the client requires fresh consent; do not move grants between clients.
50
+
51
+ ## What an AI can automate
52
+
53
+ | Work | Automation assessment |
54
+ |---|---|
55
+ | Create the application's Cloud project | Resource Manager has `projects.create`; requires an authenticated operator with the required parent permissions. [API](https://docs.cloud.google.com/resource-manager/reference/rest/v3/projects/create). |
56
+ | Enable Calendar API | Service Usage has `services.enable`; use `calendar-json.googleapis.com` and operator authority for the project. [API](https://docs.cloud.google.com/service-usage/docs/reference/rest/v1/services/enable). |
57
+ | Create a general Calendar OAuth web client, configure branding and consent | This packet does not establish a supported public API for all those steps. Use the console instructions; do not substitute IAP-specific client creation or service-account credentials. |
58
+ | Write configuration and app wiring | Fully scriptable using this package, ordinary files and the application's secret bindings. |
59
+ | Grant mailbox/calendar access | The account owner completes provider consent; an agent cannot manufacture that grant. |
60
+ | Verification, organization consent and quota approval | Human/provider decisions remain necessary where Google requires them. An agent can prepare configuration and evidence. |
61
+
62
+ ## Field ownership
63
+
64
+ | Input or display | Owner/destination |
65
+ |---|---|
66
+ | Integration display name | `integrations.<slot>.displayName` |
67
+ | Shared account / each user / assistant | `integrations.<slot>.accountMode`; server policy enforces ownership |
68
+ | Selected scopes | `integrations.<slot>.scopes`; actual grants live in encrypted connection state |
69
+ | Client ID | `registrations.<id>.clientId` |
70
+ | Client secret | Server secret storage; source contains `clientSecretRef` |
71
+ | Callback URL | Server environment; source contains `callbackUrlRef`; UI may display its resolved value |
72
+ | People allowed to use a connection or client | Application permission records; not a provider OAuth scope and not a grant created by editing source |
73
+ | Connected account, verification time, reconnect status | Runtime metadata after successful provider verification |
74
+
75
+ See Google's [complete scope list](https://developers.google.com/workspace/calendar/api/auth)
76
+ for permission meanings. The shared form carries every documented Calendar
77
+ scope, including scopes for advanced operations that this initial fragment does
78
+ not implement.
79
+
80
+ ## Event operations and framework wiring
81
+
82
+ For writes select **Manage events** (`calendar.events`) alongside **List
83
+ calendars**, add the scope to Google Data Access, and reconnect. A saved scope
84
+ change does not upgrade an existing grant. The connected account must also have
85
+ write access to the selected calendar; Google enforces organizer/guest rules.
86
+
87
+ Use the existing connection service in a JSKIT backend or CLI:
88
+
89
+ ```js
90
+ await connections.invoke({ context, integrationId: "calendar",
91
+ operation: "events.create", input: {
92
+ calendarId: "primary", sendUpdates: "all",
93
+ event: { summary: "Grooming appointment",
94
+ start: { dateTime: "2026-10-05T09:00:00+08:00", timeZone: "Australia/Perth" },
95
+ end: { dateTime: "2026-10-05T10:00:00+08:00", timeZone: "Australia/Perth" },
96
+ attendees: [{ email: "customer@example.com" }] }
97
+ } });
98
+ ```
99
+
100
+ Other frameworks use the same project configuration/Env and their native Google
101
+ Calendar client or HTTP transport. Vibe64 stores configuration, not events.
102
+ The application authorizes the caller and chooses a connection, calendar and
103
+ notification policy. It owns appointment screens, scheduling and conflict rules.
104
+
105
+ - `calendars.list` exposes calendar IDs and access roles. `primary` means the
106
+ connected person's primary calendar, not a global application calendar.
107
+ - `events.list` returns expanded occurrences with `nextPageToken`; use its
108
+ instance ID for a one-occurrence change. `events.get` accepts `calendarId`
109
+ and `eventId` and returns the provider event, including ETag and recurrence.
110
+ - `events.create`: `calendarId`, explicit `sendUpdates`, and `event`. The event
111
+ requires start/end. Supported fields are summary, description, location,
112
+ start/end, recurrence, attendees, transparency and visibility.
113
+ - `events.update`: same target plus `eventId`, a partial `event`, explicit
114
+ `sendUpdates`, and optional `ifMatch` from the latest ETag. Fetch current data
115
+ first. PATCH replaces whole attendee/recurrence arrays, not individual entries.
116
+ Supply both start/end when changing time; setting nonempty recurrence also
117
+ requires both. Google's 412 means refetch and review the concurrent edit.
118
+ - `events.cancel`: calendar/event IDs, explicit `sendUpdates`, optional `ifMatch`.
119
+ Successful deletion returns null. Cancelling the master affects the series;
120
+ an instance ID affects that occurrence. Application confirmation is required
121
+ before choosing a cancellation target.
122
+ - `sendUpdates` is **all**, **externalOnly** or **none**. The adapter requires an
123
+ explicit choice. Google warns that none can prevent external-calendar sync;
124
+ it is not a promise that Google sends no email. Review attendee effects before
125
+ writing. The adapter does not silently retry an uncertain write: reconcile
126
+ against the calendar before repeating an appointment creation.
127
+ - All-day events use `{date: "2026-10-05"}` and an **exclusive** end date (the next
128
+ day for a one-day event). Timed events require explicit offsets; recurring
129
+ timed events additionally require IANA timeZone on both ends so Google can
130
+ expand local times across daylight-saving changes. Recurrence is RFC5545
131
+ RRULE/RDATE/EXRULE/EXDATE text; the provider validates rule semantics.
132
+ - Display description HTML safely. Do not assume event invitation, consent or
133
+ notification delivery merely because an API response succeeded.
134
+
135
+ **Limitations:** no calendar/ACL administration, free/busy scheduling engine,
136
+ Meet creation, attachments, special event types, watch/history synchronization,
137
+ or recurring “this and following” series splitting. For example, an app can
138
+ create a weekly appointment and cancel one occurrence, but a booking conflict
139
+ solver or automatic rescheduler remains app code. Editor assistant attachment
140
+ is deferred. Fixtures do not prove live notifications or Google approval.
141
+
142
+ References: [insert](https://developers.google.com/workspace/calendar/api/v3/reference/events/insert),
143
+ [patch](https://developers.google.com/workspace/calendar/api/v3/reference/events/patch),
144
+ [delete](https://developers.google.com/workspace/calendar/api/v3/reference/events/delete).
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@jskit-ai/connector-google-calendar",
3
+ "version": "0.1.1",
4
+ "description": "Google Calendar account configuration and event operations for JSKIT connectors.",
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "node --test --test-concurrency=1"
8
+ },
9
+ "exports": {
10
+ "./shared": "./src/shared/definition.js",
11
+ "./server": "./src/server/provider.js"
12
+ },
13
+ "dependencies": {
14
+ "@jskit-ai/connectors-core": "0.1.1",
15
+ "json-rest-schema": "^1.0.17"
16
+ },
17
+ "jskit": {
18
+ "kind": "runtime",
19
+ "capabilities": {
20
+ "provides": [],
21
+ "requires": []
22
+ },
23
+ "runtime": {
24
+ "server": {
25
+ "providers": []
26
+ },
27
+ "client": {
28
+ "providers": []
29
+ }
30
+ }
31
+ },
32
+ "peerDependencies": {
33
+ "@jskit-ai/kernel": "0.1.184"
34
+ }
35
+ }
@@ -0,0 +1,106 @@
1
+ ---
2
+ id: connectors/calendar-cli
3
+ title: Google Calendar from an application-owned CLI
4
+ summary: Compose reusable connection libraries with portable configuration, application ownership and durable storage.
5
+ keywords: connectors, integrations, google, calendar, oauth, cli, permissions
6
+ requires: @jskit-ai/connectors-core, @jskit-ai/connector-google-calendar, @jskit-ai/database-runtime-mysql
7
+ ---
8
+
9
+ # Google Calendar from an application-owned CLI
10
+
11
+ ## Use when
12
+
13
+ Use this pattern for a local operator command that connects an account and reads
14
+ Calendar pages. The example deliberately selects MySQL/MariaDB. A web app should
15
+ use its existing selected database client and authenticated callback routes.
16
+
17
+ ## Do not use when
18
+
19
+ Do not use the local operator identity or loopback listener as a public web
20
+ application's authentication. Do not use this example for managed gateway
21
+ registrations, native public OAuth clients or service accounts.
22
+
23
+ ## Product decisions
24
+
25
+ Adapt the example's `integrations.json`, environment bindings, command names,
26
+ ownership policy and output handling. Install its dependencies with ordinary
27
+ `npm install`; no JSKIT CLI, generator or template installation is involved.
28
+ The example's loopback HTTP callback is local command wiring. OAuth exchange,
29
+ PKCE, state checks, refresh, provider requests, encryption and storage remain
30
+ imports from the libraries.
31
+
32
+ The example trusts the local process owner. `CONNECTOR_APPLICATION_ID` and
33
+ `CONNECTOR_SUBJECT_ID` identify its persistent connection. Do not copy that
34
+ environment-based identity policy into an HTTP API: derive its owner from the
35
+ authenticated user and check membership/operation permissions there.
36
+
37
+ ## Invariants
38
+
39
+ - One configuration file is read by CLI and UI.
40
+ - Credential values and connection grants stay outside source.
41
+ - The application authorizes owners and executes package-owned migrations.
42
+ - OAuth and persistence machinery is imported from the library.
43
+ - Provider consent and a successful account check precede Connected.
44
+
45
+ ## Framework APIs
46
+
47
+ The script imports `parseIntegrationConfiguration`, `createConnectionService`,
48
+ `createEnvironmentReferenceResolver`, `createCredentialProtection`,
49
+ `createKnexConnectionStore` and `googleCalendarProvider`. `knexfile.js` uses
50
+ `createKnexMigrationConfigFromApp` with the selected MySQL dialect.
51
+
52
+ ## Example files
53
+
54
+ `example/integrations.json` is portable source. `example/.env.example` lists
55
+ private runtime bindings. `example/scripts/calendar.js` owns command parsing
56
+ and local callback wiring. `example/package.json` and `example/knexfile.js`
57
+ provide ordinary npm and migration operations.
58
+
59
+ ## Verification
60
+
61
+ 1. Follow the package's provider setup guide. Register the exact loopback URL
62
+ from `.env.example` on your own Google OAuth web client.
63
+ 2. Adapt `example/package.json`, `knexfile.js`, `integrations.json` and
64
+ `scripts/calendar.js` into your application. Make `.env` from `.env.example`,
65
+ supply database and provider credentials, and exclude it from Git. Create
66
+ the application's migration directories with `mkdir -p migrations/constraints`.
67
+ 3. Generate a private storage key with
68
+ `node -e 'console.log(require("node:crypto").randomBytes(32).toString("base64"))'`.
69
+ Save it as `CONNECTOR_STORAGE_KEY`. Preserve it across restarts and backups.
70
+ 4. Run `npm install`, then `npm run db:migrate` against your application's
71
+ database. Package migrations are discovered directly; do not copy them.
72
+ 5. Run `npm run calendar -- validate`. This needs no database or provider
73
+ credentials and uses exactly the validator used by the UI.
74
+ 6. Run `npm run calendar -- connect`, open the displayed URL in your normal
75
+ browser and approve the account permissions. A local callback completes the
76
+ command. Ctrl-C cancels the pending attempt.
77
+ 7. Run `npm run calendar -- calendars`, then
78
+ `npm run calendar -- events '{"calendarId":"primary","maxResults":10}'`.
79
+ The output includes the provider's next-page token when present.
80
+ 8. Restart the process and run `npm run calendar -- status`. Run
81
+ `npm run calendar -- disconnect` to remove this application's local grant.
82
+
83
+ For command output, apply your application's privacy requirements before
84
+ retaining or sharing calendar data. The example prints operation results, not
85
+ credentials.
86
+
87
+ ## Variation points
88
+
89
+ Use `createConnectorsFeature()` with the ordinary JSKIT action runtime, or call
90
+ `createConnectionService()` from an existing Feature. Keep business operations
91
+ named, such as `calendar.events.list`. Use the app's authenticated context in
92
+ the authorization policy. A web callback recovers the initiating owner's
93
+ context and passes the full registered callback URL to `completeAuthorization`.
94
+
95
+ Use `IntegrationConfigurationFields` from `@jskit-ai/connectors-web/client` for
96
+ the form. The parent uses the normal resource/add-edit seam to save the same
97
+ file, detect concurrent edits and expose the existing secret-entry control.
98
+ Saving configuration and connecting an account are different operations.
99
+
100
+ ## Avoid
101
+
102
+ The source pattern is validated locally; live Google consent still requires
103
+ your credentials and account approval. The initial runtime implements own web
104
+ client registrations. The editor configures the generated application's own registration. Its backend
105
+ resolves the secret from the application environment; never ship a client secret
106
+ inside a desktop binary or browser bundle.
@@ -0,0 +1,6 @@
1
+ DATABASE_URL=
2
+ GOOGLE_CLIENT_SECRET=
3
+ GOOGLE_CALLBACK_URL=http://127.0.0.1:8080/integrations/google/callback
4
+ CONNECTOR_STORAGE_KEY=
5
+ CONNECTOR_APPLICATION_ID=calendar-example-development
6
+ CONNECTOR_SUBJECT_ID=local-operator
@@ -0,0 +1,23 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "integrations": {
4
+ "calendar": {
5
+ "provider": "google-calendar",
6
+ "displayName": "My calendar",
7
+ "accountMode": "per-user",
8
+ "scopes": [
9
+ "https://www.googleapis.com/auth/calendar.calendarlist.readonly",
10
+ "https://www.googleapis.com/auth/calendar.events.readonly"
11
+ ],
12
+ "authentication": { "method": "oauth2", "registrationRef": "google" }
13
+ }
14
+ },
15
+ "registrations": {
16
+ "google": {
17
+ "source": "own",
18
+ "clientId": "YOUR_GOOGLE_CLIENT_ID",
19
+ "clientSecretRef": "env:GOOGLE_CLIENT_SECRET",
20
+ "callbackUrlRef": "env:GOOGLE_CALLBACK_URL"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createKnexMigrationConfigFromApp } from "@jskit-ai/database-runtime/server/knexMigrationConfig";
3
+
4
+ if (existsSync(".env")) process.loadEnvFile(".env");
5
+ export default await createKnexMigrationConfigFromApp({ client: "mysql2" });
@@ -0,0 +1,16 @@
1
+ {
2
+ "private": true,
3
+ "type": "module",
4
+ "scripts": {
5
+ "calendar": "node --env-file-if-exists=.env scripts/calendar.js",
6
+ "db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
7
+ "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
8
+ },
9
+ "dependencies": {
10
+ "@jskit-ai/connectors-core": "0.1.1",
11
+ "@jskit-ai/connector-google-calendar": "0.1.1",
12
+ "@jskit-ai/database-runtime": "0.1.184",
13
+ "@jskit-ai/database-runtime-mysql": "0.1.182",
14
+ "knex": "^3.1.0"
15
+ }
16
+ }
@@ -0,0 +1,102 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { createServer } from "node:http";
3
+ import { once } from "node:events";
4
+ import createKnex from "knex";
5
+ import { parseIntegrationConfiguration } from "@jskit-ai/connectors-core/shared/configuration";
6
+ import { createConnectionService, createEnvironmentReferenceResolver } from "@jskit-ai/connectors-core/server";
7
+ import { createCredentialProtection, createKnexConnectionStore } from "@jskit-ai/connectors-core/server/storage";
8
+ import { googleCalendarProvider } from "@jskit-ai/connector-google-calendar/server";
9
+
10
+ // A local operator command owns this HTTP listener; web apps use authenticated routes.
11
+ async function connectInBrowser(service, context) {
12
+ const callback = new URL(process.env.GOOGLE_CALLBACK_URL);
13
+ if (callback.protocol !== "http:" || callback.hostname !== "127.0.0.1" || !callback.port) {
14
+ throw new Error("This CLI requires a registered http://127.0.0.1:PORT callback.");
15
+ }
16
+ let state;
17
+ let finish;
18
+ let fail;
19
+ let completed = false;
20
+ const controller = new AbortController();
21
+ const finished = new Promise((resolve, reject) => { finish = resolve; fail = reject; });
22
+ const outcome = finished.then((result) => ({ result }), (error) => ({ error }));
23
+ const server = createServer(async (request, response) => {
24
+ const url = new URL(request.url, callback.origin);
25
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
26
+ response.setHeader("Cache-Control", "no-store");
27
+ if (request.method !== "GET" || url.pathname !== callback.pathname || !state || url.searchParams.get("state") !== state) {
28
+ response.writeHead(400).end("This is not the pending authorization callback.");
29
+ return;
30
+ }
31
+ try {
32
+ const result = await service.completeAuthorization({ context, integrationId: "calendar", callbackUrl: url.href, signal: controller.signal });
33
+ completed = true;
34
+ response.end("Connected. You can close this window.");
35
+ finish(result);
36
+ } catch (error) {
37
+ response.writeHead(400).end("Connection failed. Return to the terminal.");
38
+ fail(error);
39
+ }
40
+ });
41
+ server.listen(Number(callback.port), "127.0.0.1");
42
+ await once(server, "listening");
43
+ const cancel = () => { controller.abort(); fail(new Error("Authorization cancelled.")); };
44
+ process.once("SIGINT", cancel);
45
+ const timeout = setTimeout(cancel, 10 * 60 * 1000);
46
+ try {
47
+ const start = await service.beginAuthorization({ context, integrationId: "calendar", signal: controller.signal });
48
+ state = new URL(start.authorizationUrl).searchParams.get("state");
49
+ console.log(`Open this URL in your browser:\n${start.authorizationUrl}`);
50
+ const completed = await outcome;
51
+ if (completed.error) throw completed.error;
52
+ return completed.result;
53
+ } finally {
54
+ clearTimeout(timeout);
55
+ process.off("SIGINT", cancel);
56
+ try {
57
+ if (state && !completed) await service.cancelAuthorization({ context, integrationId: "calendar", state });
58
+ } finally {
59
+ await new Promise((resolve) => server.close(resolve));
60
+ }
61
+ }
62
+ }
63
+
64
+ async function main() {
65
+ const command = process.argv[2] || "validate";
66
+ const configuration = parseIntegrationConfiguration(await readFile("integrations.json", "utf8"), { providers: [googleCalendarProvider] });
67
+ if (command === "validate") {
68
+ console.log("Integration configuration is valid.");
69
+ return;
70
+ }
71
+ if (!["connect", "status", "calendars", "events", "disconnect"].includes(command)) {
72
+ throw new Error("Use validate, connect, status, calendars, events or disconnect.");
73
+ }
74
+ for (const name of ["DATABASE_URL", "CONNECTOR_STORAGE_KEY", "CONNECTOR_APPLICATION_ID", "CONNECTOR_SUBJECT_ID"]) {
75
+ if (!process.env[name]) throw new Error(`Set ${name} before using connections.`);
76
+ }
77
+ const context = { applicationId: process.env.CONNECTOR_APPLICATION_ID, subjectId: process.env.CONNECTOR_SUBJECT_ID };
78
+ const protection = createCredentialProtection({ keys: { current: Buffer.from(process.env.CONNECTOR_STORAGE_KEY, "base64") }, activeKeyId: "current" });
79
+ const knex = createKnex({ client: "mysql2", connection: process.env.DATABASE_URL });
80
+ try {
81
+ const service = createConnectionService({
82
+ configuration, providers: [googleCalendarProvider],
83
+ store: createKnexConnectionStore({ knex, protection }),
84
+ resolveReference: createEnvironmentReferenceResolver(process.env),
85
+ authorize: async () => context
86
+ });
87
+ const request = { context, integrationId: "calendar" };
88
+ let result;
89
+ if (command === "connect") result = await connectInBrowser(service, context);
90
+ else if (command === "status") result = await service.status(request);
91
+ else if (command === "disconnect") result = await service.disconnect(request);
92
+ else result = await service.invoke({ ...request, operation: command === "events" ? "events.list" : "calendars.list", input: JSON.parse(process.argv[3] || "{}") });
93
+ console.log(JSON.stringify(result, null, 2));
94
+ } finally { await knex.destroy(); }
95
+ }
96
+
97
+ main().catch((error) => {
98
+ console.error(error.code || "calendar_command_failed");
99
+ if (error.fieldErrors) console.error(JSON.stringify(error.fieldErrors));
100
+ else if (!error.code) console.error(error.message);
101
+ process.exitCode = 1;
102
+ });
@@ -0,0 +1,113 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { googleCalendarDefinition } from "../shared/definition.js";
4
+
5
+ const scope = (name) => `https://www.googleapis.com/auth/${name}`;
6
+ const listSchema = createSchema({
7
+ pageToken: { type: "string", maxLength: 4096 },
8
+ maxResults: { type: "integer", min: 1, max: 250, defaultTo: 100 }
9
+ });
10
+ const eventsSchema = createSchema({
11
+ calendarId: { type: "string", minLength: 1, maxLength: 1024, defaultTo: "primary" },
12
+ pageToken: { type: "string", maxLength: 4096 },
13
+ maxResults: { type: "integer", min: 1, max: 2500, defaultTo: 100 },
14
+ timeMin: { type: "dateTime" },
15
+ timeMax: { type: "dateTime" }
16
+ });
17
+
18
+ function queryUrl(path, input) {
19
+ const url = new URL(`https://www.googleapis.com/calendar/v3/${path}`);
20
+ for (const [key, value] of Object.entries(input)) if (value !== undefined) url.searchParams.set(key, String(value));
21
+ return { method: "GET", url: url.href };
22
+ }
23
+
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);
26
+ const targetSchema = createSchema({
27
+ calendarId: { type: "string", minLength: 1, maxLength: 1024, defaultTo: "primary" },
28
+ eventId: { type: "string", minLength: 1, maxLength: 1024, required: true }
29
+ });
30
+ function invalid(message) { throw Object.assign(new Error(message), { statusCode: 422 }); }
31
+ function eventBody(body, creating) {
32
+ if (!body || typeof body !== "object" || Array.isArray(body)) invalid("event must be an object.");
33
+ const allowed = ["summary", "description", "location", "start", "end", "recurrence", "attendees", "transparency", "visibility"];
34
+ if (!Object.keys(body).length || Object.keys(body).some(key => !allowed.includes(key))) invalid("Use supported event fields only.");
35
+ 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
+ if (creating && (!body.start || !body.end)) invalid("start and end are required.");
37
+ 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
+ }
45
+ 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.");
52
+ if (body.transparency !== undefined && !["opaque", "transparent"].includes(body.transparency)) invalid("Invalid transparency.");
53
+ if (body.visibility !== undefined && !["default", "public", "private", "confidential"].includes(body.visibility)) invalid("Invalid visibility.");
54
+ return body;
55
+ }
56
+ function writeEvent(method) {
57
+ return {
58
+ scopes: writeScopes,
59
+ validateResult: result => method === "DELETE" ? result === null : result?.kind === "calendar#event" && typeof result.id === "string",
60
+ request(input = {}) {
61
+ const { event, sendUpdates, ifMatch, ...target } = input;
62
+ if (!["all", "externalOnly", "none"].includes(sendUpdates)) invalid("Choose sendUpdates explicitly: all, externalOnly or none.");
63
+ if (ifMatch !== undefined && (typeof ifMatch !== "string" || !ifMatch || ifMatch.length > 1024 || /[\r\n]/.test(ifMatch))) invalid("Invalid event ETag.");
64
+ 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 });
66
+ const request = queryUrl(`calendars/${encodeURIComponent(calendarId)}/events${eventId ? `/${encodeURIComponent(eventId)}` : ""}`, { sendUpdates });
67
+ return { ...request, method, ...(ifMatch ? { headers: { "If-Match": ifMatch } } : {}), ...(method !== "DELETE" ? { body: eventBody(event, method === "POST") } : {}) };
68
+ }
69
+ };
70
+ }
71
+
72
+ const googleCalendarProvider = Object.freeze({
73
+ ...googleCalendarDefinition,
74
+ oauth: {
75
+ issuer: "https://accounts.google.com",
76
+ authorization_endpoint: "https://accounts.google.com/o/oauth2/v2/auth",
77
+ token_endpoint: "https://oauth2.googleapis.com/token",
78
+ revocation_endpoint: "https://oauth2.googleapis.com/revoke"
79
+ },
80
+ authorizationParameters: { access_type: "offline", prompt: "consent" },
81
+ apiOrigins: ["https://www.googleapis.com"],
82
+ checkOperation: "calendars.list",
83
+ operations: {
84
+ "events.create": writeEvent("POST"),
85
+ "events.update": writeEvent("PATCH"),
86
+ "events.cancel": writeEvent("DELETE"),
87
+ "events.get": {
88
+ scopes: readScopes,
89
+ validateResult: result => result?.kind === "calendar#event" && typeof result.id === "string",
90
+ request(input) {
91
+ const { calendarId, eventId } = validateSchemaPayload({ schema: targetSchema, mode: "replace" }, input, { statusCode: 422 });
92
+ return queryUrl(`calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(eventId)}`, {});
93
+ }
94
+ },
95
+ "calendars.list": {
96
+ scopes: [scope("calendar.calendarlist.readonly"), scope("calendar.calendarlist"), scope("calendar.readonly"), scope("calendar")],
97
+ validateResult: (result) => result?.kind === "calendar#calendarList" && (result.items === undefined || Array.isArray(result.items)),
98
+ request(input) {
99
+ return queryUrl("users/me/calendarList", validateSchemaPayload({ schema: listSchema, mode: "replace" }, input, { statusCode: 422 }));
100
+ }
101
+ },
102
+ "events.list": {
103
+ scopes: [scope("calendar.events.readonly"), scope("calendar.events"), scope("calendar.readonly"), scope("calendar")],
104
+ validateResult: (result) => result?.kind === "calendar#events" && (result.items === undefined || Array.isArray(result.items)),
105
+ request(input) {
106
+ const { calendarId, ...query } = validateSchemaPayload({ schema: eventsSchema, mode: "replace" }, input, { statusCode: 422 });
107
+ return queryUrl(`calendars/${encodeURIComponent(calendarId)}/events`, { ...query, singleEvents: true, orderBy: "startTime" });
108
+ }
109
+ }
110
+ }
111
+ });
112
+
113
+ export { googleCalendarProvider };
@@ -0,0 +1,52 @@
1
+ const GOOGLE_SCOPE_BASE = "https://www.googleapis.com/auth/";
2
+ const googleCalendarDefinition = Object.freeze({
3
+ id: "google-calendar",
4
+ name: "Google Calendar",
5
+ description: "Read calendars and create, edit or cancel events.",
6
+ categories: ["Google", "Productivity"],
7
+ accountModes: ["shared", "per-user", "assistant"],
8
+ authenticationMethods: ["oauth2"],
9
+ setup: {
10
+ callbackPath: "/integrations/google/callback",
11
+ url: "https://developers.google.com/identity/protocols/oauth2/web-server",
12
+ steps: [
13
+ "In Google Cloud, select the project for this application. Open APIs & Services > Library, search for Google Calendar API, open it and choose Enable.",
14
+ "Open Google Auth Platform > Branding. If setup has not started, choose Get Started; enter the app name, support email and contact email, choose the audience, review the policy and finish with Create.",
15
+ "Choose External if people outside your Google Workspace organization will connect. During testing, open Audience > Test users > Add users, enter the allowed Google accounts and Save. Internal is only for your organization. External Testing refresh tokens for these API permissions expire after seven days; reconnect during testing. Public release also requires completing the applicable Google review.",
16
+ "For an external app, open Data Access > Add or Remove Scopes. Add the permissions selected in this integration and Save. Sensitive or restricted scopes can require Google verification before public use; creating a client does not complete that review.",
17
+ "Keep List calendars for the initial connection check and Read events if the application will read events. Event-only or availability-only permissions do not authorize the calendar-list check. Use an account that can access the intended calendars; selecting a scope does not grant calendar sharing access.",
18
+ "To create, edit or cancel events, select Manage events, add calendar.events in Google Data Access, and reconnect for fresh consent. Keep List calendars for verification. Calendar sharing must independently allow the connected account to write.",
19
+ "Choose the destination calendar and attendee notification policy explicitly. All-day end dates are exclusive; timed recurring events need an IANA time zone. Editing or cancelling a recurring master affects the series; use an expanded occurrence ID for one instance. Changing attendee arrays replaces the list; read the latest event first.",
20
+ "Open Clients > Create Client. Choose Web application and name it. Under Authorized redirect URIs, choose Add URI and paste the application callback shown here, then Create. The editor dashboard URL is not the callback.",
21
+ "Copy the Client ID into this form and copy or download the full client secret immediately; Google only shows it at creation. Save configuration, then use Set credential in Env to put the secret value in the variable named by Client secret reference. If lost, open Clients, select this client and Add Secret; save the new value before disabling the old secret.",
22
+ "Use Set callback in Env to save the same application callback URL. If a Configured callback URL is shown, preserve it unless intentionally changing both Env and Google. Scheme, path, case and trailing slash must match exactly.",
23
+ "For a shared account, return here and check or connect the account once the application runtime is ready. For per-user connections, each person authorizes inside your application. Saving this registration neither connects every user nor installs the callback route."
24
+ ]
25
+ },
26
+ 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" }
47
+ ],
48
+ settingsFields: [],
49
+ documentationUrl: "https://developers.google.com/workspace/calendar/api/guides/overview"
50
+ });
51
+
52
+ export { googleCalendarDefinition };