@squadbase/vite-server 0.1.7 → 0.1.8-dev.468a970
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/cli/cpufeatures-ORCDQN2Y.node +0 -0
- package/dist/cli/index.js +676 -623
- package/dist/cli/sshcrypto-P3UBA7BP.node +0 -0
- package/dist/connectors/gmail.js +214 -348
- package/dist/connectors/google-calendar.js +398 -449
- package/dist/connectors/salesforce.js +1 -1
- package/dist/cpufeatures-ORCDQN2Y.node +0 -0
- package/dist/index.js +676 -623
- package/dist/main.js +676 -623
- package/dist/sshcrypto-P3UBA7BP.node +0 -0
- package/dist/vite-plugin.js +676 -623
- package/package.json +4 -3
- package/dist/cli/cpufeatures-FL6HDURN.node +0 -0
- package/dist/cli/sshcrypto-XARBAYLB.node +0 -0
- package/dist/cpufeatures-FL6HDURN.node +0 -0
- package/dist/sshcrypto-XARBAYLB.node +0 -0
|
@@ -42,82 +42,28 @@ var ParameterDefinition = class {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
// ../connectors/src/connectors/google-calendar/sdk/index.ts
|
|
46
|
-
import * as crypto from "crypto";
|
|
47
|
-
|
|
48
45
|
// ../connectors/src/connectors/google-calendar/parameters.ts
|
|
49
46
|
var parameters = {
|
|
50
47
|
serviceAccountKeyJsonBase64: new ParameterDefinition({
|
|
51
48
|
slug: "service-account-key-json-base64",
|
|
52
49
|
name: "Google Cloud Service Account JSON",
|
|
53
|
-
description: "The service account JSON key
|
|
50
|
+
description: "The service account JSON key. Used for both Domain-wide Delegation (impersonating a Workspace user) and direct service-account access (calendars explicitly shared with the SA email). The authentication path is selected per call by the tool used.",
|
|
54
51
|
envVarBaseKey: "GOOGLE_CALENDAR_SERVICE_ACCOUNT_JSON_BASE64",
|
|
55
52
|
type: "base64EncodedJson",
|
|
56
53
|
secret: true,
|
|
57
54
|
required: true
|
|
58
55
|
})
|
|
59
56
|
};
|
|
60
|
-
var impersonateEmailParameter = new ParameterDefinition({
|
|
61
|
-
slug: "impersonate-email",
|
|
62
|
-
name: "User Email Address(es)",
|
|
63
|
-
description: "The email address(es) of the Google Workspace user(s) whose calendar is accessed via Domain-wide Delegation. Collected during the setup flow.",
|
|
64
|
-
envVarBaseKey: "GOOGLE_CALENDAR_IMPERSONATE_EMAIL",
|
|
65
|
-
type: "text",
|
|
66
|
-
secret: false,
|
|
67
|
-
required: false
|
|
68
|
-
});
|
|
69
|
-
var calendarIdParameter = new ParameterDefinition({
|
|
70
|
-
slug: "calendar-id",
|
|
71
|
-
name: "Default Calendar ID",
|
|
72
|
-
description: "The default Google Calendar ID to use (e.g., 'primary' or an email address like 'user@example.com'). If not set, 'primary' is used.",
|
|
73
|
-
envVarBaseKey: "GOOGLE_CALENDAR_CALENDAR_ID",
|
|
74
|
-
type: "text",
|
|
75
|
-
secret: false,
|
|
76
|
-
required: false
|
|
77
|
-
});
|
|
78
57
|
|
|
79
58
|
// ../connectors/src/connectors/google-calendar/sdk/index.ts
|
|
80
|
-
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
81
59
|
var BASE_URL = "https://www.googleapis.com/calendar/v3";
|
|
82
|
-
var SCOPE = "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.readonly";
|
|
83
|
-
function base64url(input) {
|
|
84
|
-
const buf = typeof input === "string" ? Buffer.from(input) : input;
|
|
85
|
-
return buf.toString("base64url");
|
|
86
|
-
}
|
|
87
|
-
function buildJwt(clientEmail, privateKey, nowSec, subject) {
|
|
88
|
-
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
89
|
-
const claims = {
|
|
90
|
-
iss: clientEmail,
|
|
91
|
-
scope: SCOPE,
|
|
92
|
-
aud: TOKEN_URL,
|
|
93
|
-
iat: nowSec,
|
|
94
|
-
exp: nowSec + 3600
|
|
95
|
-
};
|
|
96
|
-
if (subject) {
|
|
97
|
-
claims.sub = subject;
|
|
98
|
-
}
|
|
99
|
-
const payload = base64url(JSON.stringify(claims));
|
|
100
|
-
const signingInput = `${header}.${payload}`;
|
|
101
|
-
const sign = crypto.createSign("RSA-SHA256");
|
|
102
|
-
sign.update(signingInput);
|
|
103
|
-
sign.end();
|
|
104
|
-
const signature = base64url(sign.sign(privateKey));
|
|
105
|
-
return `${signingInput}.${signature}`;
|
|
106
|
-
}
|
|
107
60
|
function createClient(params) {
|
|
108
61
|
const serviceAccountKeyJsonBase64 = params[parameters.serviceAccountKeyJsonBase64.slug];
|
|
109
|
-
const impersonateEmail = params[impersonateEmailParameter.slug];
|
|
110
|
-
const defaultCalendarId = params[calendarIdParameter.slug] ?? "primary";
|
|
111
62
|
if (!serviceAccountKeyJsonBase64) {
|
|
112
63
|
throw new Error(
|
|
113
64
|
`google-calendar: missing required parameter: ${parameters.serviceAccountKeyJsonBase64.slug}`
|
|
114
65
|
);
|
|
115
66
|
}
|
|
116
|
-
if (!impersonateEmail) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`google-calendar: missing required parameter: ${impersonateEmailParameter.slug}`
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
67
|
let serviceAccountKey;
|
|
122
68
|
try {
|
|
123
69
|
const decoded = Buffer.from(
|
|
@@ -135,101 +81,48 @@ function createClient(params) {
|
|
|
135
81
|
"google-calendar: service account key JSON must contain client_email and private_key"
|
|
136
82
|
);
|
|
137
83
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let tokenExpiresAt = 0;
|
|
141
|
-
async function getAccessToken2() {
|
|
142
|
-
const nowSec = Math.floor(Date.now() / 1e3);
|
|
143
|
-
if (cachedToken && nowSec < tokenExpiresAt - 60) {
|
|
144
|
-
return cachedToken;
|
|
145
|
-
}
|
|
146
|
-
const jwt = buildJwt(
|
|
147
|
-
serviceAccountKey.client_email,
|
|
148
|
-
serviceAccountKey.private_key,
|
|
149
|
-
nowSec,
|
|
150
|
-
subject
|
|
151
|
-
);
|
|
152
|
-
const response = await fetch(TOKEN_URL, {
|
|
153
|
-
method: "POST",
|
|
154
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
155
|
-
body: new URLSearchParams({
|
|
156
|
-
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
157
|
-
assertion: jwt
|
|
158
|
-
})
|
|
159
|
-
});
|
|
160
|
-
if (!response.ok) {
|
|
161
|
-
const text = await response.text();
|
|
162
|
-
throw new Error(
|
|
163
|
-
`google-calendar: token exchange failed (${response.status}): ${text}`
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
const data = await response.json();
|
|
167
|
-
cachedToken = data.access_token;
|
|
168
|
-
tokenExpiresAt = nowSec + data.expires_in;
|
|
169
|
-
return cachedToken;
|
|
170
|
-
}
|
|
171
|
-
function resolveCalendarId(override) {
|
|
172
|
-
return override ?? defaultCalendarId;
|
|
84
|
+
function buildUrl(path2) {
|
|
85
|
+
return `${BASE_URL}${path2.startsWith("/") ? "" : "/"}${path2}`;
|
|
173
86
|
}
|
|
174
87
|
return {
|
|
175
|
-
async
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return fetch(url, { ...init, headers });
|
|
185
|
-
},
|
|
186
|
-
async listCalendars() {
|
|
187
|
-
const response = await this.request("/users/me/calendarList", {
|
|
188
|
-
method: "GET"
|
|
88
|
+
async requestWithDelegation(path2, { subject, scopes, init }) {
|
|
89
|
+
const { GoogleAuth } = await import("google-auth-library");
|
|
90
|
+
const auth = new GoogleAuth({
|
|
91
|
+
credentials: {
|
|
92
|
+
client_email: serviceAccountKey.client_email,
|
|
93
|
+
private_key: serviceAccountKey.private_key
|
|
94
|
+
},
|
|
95
|
+
scopes,
|
|
96
|
+
clientOptions: { subject }
|
|
189
97
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new Error(
|
|
193
|
-
`google-calendar: listCalendars failed (${response.status}): ${text}`
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
const data = await response.json();
|
|
197
|
-
return data.items ?? [];
|
|
198
|
-
},
|
|
199
|
-
async listEvents(options, calendarId) {
|
|
200
|
-
const cid = resolveCalendarId(calendarId);
|
|
201
|
-
const searchParams = new URLSearchParams();
|
|
202
|
-
if (options?.timeMin) searchParams.set("timeMin", options.timeMin);
|
|
203
|
-
if (options?.timeMax) searchParams.set("timeMax", options.timeMax);
|
|
204
|
-
if (options?.maxResults)
|
|
205
|
-
searchParams.set("maxResults", String(options.maxResults));
|
|
206
|
-
if (options?.q) searchParams.set("q", options.q);
|
|
207
|
-
if (options?.singleEvents != null)
|
|
208
|
-
searchParams.set("singleEvents", String(options.singleEvents));
|
|
209
|
-
if (options?.orderBy) searchParams.set("orderBy", options.orderBy);
|
|
210
|
-
if (options?.pageToken) searchParams.set("pageToken", options.pageToken);
|
|
211
|
-
const qs = searchParams.toString();
|
|
212
|
-
const path2 = `/calendars/${encodeURIComponent(cid)}/events${qs ? `?${qs}` : ""}`;
|
|
213
|
-
const response = await this.request(path2, { method: "GET" });
|
|
214
|
-
if (!response.ok) {
|
|
215
|
-
const text = await response.text();
|
|
98
|
+
const token = await auth.getAccessToken();
|
|
99
|
+
if (!token) {
|
|
216
100
|
throw new Error(
|
|
217
|
-
`google-calendar:
|
|
101
|
+
`google-calendar: failed to obtain access token (subject=${subject})`
|
|
218
102
|
);
|
|
219
103
|
}
|
|
220
|
-
|
|
104
|
+
const headers = new Headers(init?.headers);
|
|
105
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
106
|
+
return fetch(buildUrl(path2), { ...init, headers });
|
|
221
107
|
},
|
|
222
|
-
async
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
108
|
+
async request(path2, { scopes, init }) {
|
|
109
|
+
const { GoogleAuth } = await import("google-auth-library");
|
|
110
|
+
const auth = new GoogleAuth({
|
|
111
|
+
credentials: {
|
|
112
|
+
client_email: serviceAccountKey.client_email,
|
|
113
|
+
private_key: serviceAccountKey.private_key
|
|
114
|
+
},
|
|
115
|
+
scopes
|
|
116
|
+
});
|
|
117
|
+
const token = await auth.getAccessToken();
|
|
118
|
+
if (!token) {
|
|
228
119
|
throw new Error(
|
|
229
|
-
|
|
120
|
+
"google-calendar: failed to obtain access token (no subject)"
|
|
230
121
|
);
|
|
231
122
|
}
|
|
232
|
-
|
|
123
|
+
const headers = new Headers(init?.headers);
|
|
124
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
125
|
+
return fetch(buildUrl(path2), { ...init, headers });
|
|
233
126
|
}
|
|
234
127
|
};
|
|
235
128
|
}
|
|
@@ -381,97 +274,47 @@ var AUTH_TYPES = {
|
|
|
381
274
|
USER_PASSWORD: "user-password"
|
|
382
275
|
};
|
|
383
276
|
|
|
384
|
-
// ../connectors/src/connectors/google-calendar/tools/
|
|
385
|
-
import * as crypto2 from "crypto";
|
|
277
|
+
// ../connectors/src/connectors/google-calendar/tools/request.ts
|
|
386
278
|
import { z } from "zod";
|
|
387
|
-
var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
|
|
388
279
|
var BASE_URL2 = "https://www.googleapis.com/calendar/v3";
|
|
389
|
-
var SCOPE2 = "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/calendar.events.readonly";
|
|
390
280
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
391
|
-
function
|
|
392
|
-
const
|
|
393
|
-
return
|
|
394
|
-
}
|
|
395
|
-
function buildJwt2(clientEmail, privateKey, nowSec, subject) {
|
|
396
|
-
const header = base64url2(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
|
397
|
-
const payload = base64url2(
|
|
398
|
-
JSON.stringify({
|
|
399
|
-
iss: clientEmail,
|
|
400
|
-
sub: subject,
|
|
401
|
-
scope: SCOPE2,
|
|
402
|
-
aud: TOKEN_URL2,
|
|
403
|
-
iat: nowSec,
|
|
404
|
-
exp: nowSec + 3600
|
|
405
|
-
})
|
|
406
|
-
);
|
|
407
|
-
const signingInput = `${header}.${payload}`;
|
|
408
|
-
const sign = crypto2.createSign("RSA-SHA256");
|
|
409
|
-
sign.update(signingInput);
|
|
410
|
-
sign.end();
|
|
411
|
-
const signature = base64url2(sign.sign(privateKey));
|
|
412
|
-
return `${signingInput}.${signature}`;
|
|
413
|
-
}
|
|
414
|
-
async function getAccessToken(serviceAccount, subject) {
|
|
415
|
-
const nowSec = Math.floor(Date.now() / 1e3);
|
|
416
|
-
const jwt = buildJwt2(
|
|
417
|
-
serviceAccount.client_email,
|
|
418
|
-
serviceAccount.private_key,
|
|
419
|
-
nowSec,
|
|
420
|
-
subject
|
|
421
|
-
);
|
|
422
|
-
const response = await fetch(TOKEN_URL2, {
|
|
423
|
-
method: "POST",
|
|
424
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
425
|
-
body: new URLSearchParams({
|
|
426
|
-
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
427
|
-
assertion: jwt
|
|
428
|
-
})
|
|
429
|
-
});
|
|
430
|
-
if (!response.ok) {
|
|
431
|
-
const text = await response.text();
|
|
432
|
-
throw new Error(
|
|
433
|
-
`token exchange failed for ${subject} (${response.status}): ${text}`
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
const data = await response.json();
|
|
437
|
-
return data.access_token;
|
|
281
|
+
function decodeServiceAccount(keyJsonBase64) {
|
|
282
|
+
const decoded = Buffer.from(keyJsonBase64, "base64").toString("utf-8");
|
|
283
|
+
return JSON.parse(decoded);
|
|
438
284
|
}
|
|
439
285
|
var inputSchema = z.object({
|
|
440
286
|
toolUseIntent: z.string().optional().describe(
|
|
441
287
|
"Brief description of what you intend to accomplish with this tool call"
|
|
442
288
|
),
|
|
443
|
-
connectionId: z.string().describe("ID of the Google Calendar connection to use")
|
|
289
|
+
connectionId: z.string().describe("ID of the Google Calendar connection to use"),
|
|
290
|
+
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method"),
|
|
291
|
+
path: z.string().describe(
|
|
292
|
+
"API path appended to https://www.googleapis.com/calendar/v3 (e.g., '/users/me/calendarList', '/calendars/team@example.com/events'). Write the calendar ID directly into the path \u2014 there is no placeholder substitution."
|
|
293
|
+
),
|
|
294
|
+
scopes: z.array(z.string()).describe(
|
|
295
|
+
"OAuth scopes the token must include. This connector currently supports read-only operations only \u2014 pass one of ['https://www.googleapis.com/auth/calendar.readonly'] (calendars + events read), ['https://www.googleapis.com/auth/calendar.events.readonly'] (events read only), or ['https://www.googleapis.com/auth/calendar.freebusy'] (busy/free queries only). Per-endpoint scope reference: https://developers.google.com/calendar/api/auth"
|
|
296
|
+
),
|
|
297
|
+
queryParams: z.record(z.string(), z.string()).optional().describe("Query parameters to append to the URL"),
|
|
298
|
+
body: z.record(z.string(), z.unknown()).optional().describe("JSON request body for POST/PUT/PATCH")
|
|
444
299
|
});
|
|
445
300
|
var outputSchema = z.discriminatedUnion("success", [
|
|
446
301
|
z.object({
|
|
447
302
|
success: z.literal(true),
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
impersonateEmail: z.string(),
|
|
451
|
-
id: z.string(),
|
|
452
|
-
summary: z.string(),
|
|
453
|
-
primary: z.boolean().optional(),
|
|
454
|
-
accessRole: z.string()
|
|
455
|
-
})
|
|
456
|
-
),
|
|
457
|
-
errors: z.array(
|
|
458
|
-
z.object({
|
|
459
|
-
impersonateEmail: z.string(),
|
|
460
|
-
error: z.string()
|
|
461
|
-
})
|
|
462
|
-
)
|
|
303
|
+
status: z.number(),
|
|
304
|
+
data: z.record(z.string(), z.unknown())
|
|
463
305
|
}),
|
|
464
306
|
z.object({
|
|
465
307
|
success: z.literal(false),
|
|
466
|
-
error: z.string()
|
|
308
|
+
error: z.string(),
|
|
309
|
+
serviceAccountEmail: z.string().optional()
|
|
467
310
|
})
|
|
468
311
|
]);
|
|
469
|
-
var
|
|
470
|
-
name: "
|
|
471
|
-
description: "
|
|
312
|
+
var requestTool = new ConnectorTool({
|
|
313
|
+
name: "request",
|
|
314
|
+
description: "Call the Google Calendar API as the service account itself (no delegation). Read-only operations only. Only calendars explicitly shared with the service account email are accessible. Pass `scopes` as a read-only Calendar scope (e.g., ['https://www.googleapis.com/auth/calendar.readonly']). Use this tool when the project knowledge records the calendar with `(service-account, ...)` (no `subject`).",
|
|
472
315
|
inputSchema,
|
|
473
316
|
outputSchema,
|
|
474
|
-
async execute({ connectionId }, connections) {
|
|
317
|
+
async execute({ connectionId, method, path: path2, scopes, queryParams, body }, connections) {
|
|
475
318
|
const connection2 = connections.find((c) => c.id === connectionId);
|
|
476
319
|
if (!connection2) {
|
|
477
320
|
return {
|
|
@@ -479,144 +322,89 @@ var listCalendarsTool = new ConnectorTool({
|
|
|
479
322
|
error: `Connection ${connectionId} not found`
|
|
480
323
|
};
|
|
481
324
|
}
|
|
482
|
-
const
|
|
483
|
-
const emails = impersonateEmailRaw.split(",").map((e) => e.trim()).filter((e) => e.length > 0);
|
|
484
|
-
if (emails.length === 0) {
|
|
485
|
-
return {
|
|
486
|
-
success: false,
|
|
487
|
-
error: "impersonate-email parameter is empty"
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
console.log(
|
|
491
|
-
`[connector-request] google-calendar/${connection2.name}: listCalendars for ${emails.join(",")}`
|
|
492
|
-
);
|
|
325
|
+
const keyJsonBase64 = parameters.serviceAccountKeyJsonBase64.getValue(connection2);
|
|
493
326
|
let serviceAccount;
|
|
494
327
|
try {
|
|
495
|
-
|
|
496
|
-
const decoded = Buffer.from(keyJsonBase64, "base64").toString("utf-8");
|
|
497
|
-
serviceAccount = JSON.parse(decoded);
|
|
328
|
+
serviceAccount = decodeServiceAccount(keyJsonBase64);
|
|
498
329
|
} catch (err) {
|
|
499
330
|
const msg = err instanceof Error ? err.message : String(err);
|
|
500
331
|
return {
|
|
501
332
|
success: false,
|
|
502
|
-
error: `
|
|
333
|
+
error: `Failed to decode service account key: ${msg}`
|
|
503
334
|
};
|
|
504
335
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
336
|
+
const serviceAccountEmail = serviceAccount.client_email;
|
|
337
|
+
console.log(
|
|
338
|
+
`[connector-request] google-calendar/${connection2.name}: ${method} ${path2} (service account)`
|
|
339
|
+
);
|
|
340
|
+
try {
|
|
341
|
+
const { GoogleAuth } = await import("google-auth-library");
|
|
342
|
+
const auth = new GoogleAuth({
|
|
343
|
+
credentials: {
|
|
344
|
+
client_email: serviceAccount.client_email,
|
|
345
|
+
private_key: serviceAccount.private_key
|
|
346
|
+
},
|
|
347
|
+
scopes
|
|
348
|
+
});
|
|
349
|
+
const token = await auth.getAccessToken();
|
|
350
|
+
if (!token) {
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
error: "Failed to obtain access token",
|
|
354
|
+
serviceAccountEmail
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
let url = `${BASE_URL2}${path2.startsWith("/") ? "" : "/"}${path2}`;
|
|
358
|
+
if (queryParams) {
|
|
359
|
+
const searchParams = new URLSearchParams(queryParams);
|
|
360
|
+
url += `?${searchParams.toString()}`;
|
|
361
|
+
}
|
|
514
362
|
const controller = new AbortController();
|
|
515
363
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
516
364
|
try {
|
|
517
|
-
const
|
|
518
|
-
const response = await fetch(
|
|
519
|
-
method
|
|
520
|
-
headers: {
|
|
365
|
+
const hasBody = body != null && ["POST", "PUT", "PATCH"].includes(method);
|
|
366
|
+
const response = await fetch(url, {
|
|
367
|
+
method,
|
|
368
|
+
headers: {
|
|
369
|
+
Authorization: `Bearer ${token}`,
|
|
370
|
+
"Content-Type": "application/json"
|
|
371
|
+
},
|
|
372
|
+
body: hasBody ? JSON.stringify(body) : void 0,
|
|
521
373
|
signal: controller.signal
|
|
522
374
|
});
|
|
523
|
-
const data = await response.json();
|
|
375
|
+
const data = await response.json().catch(() => ({}));
|
|
524
376
|
if (!response.ok) {
|
|
525
377
|
const errorObj = data?.error;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const items = data.items ?? [];
|
|
533
|
-
for (const c of items) {
|
|
534
|
-
aggregated.push({
|
|
535
|
-
impersonateEmail: email,
|
|
536
|
-
id: c.id,
|
|
537
|
-
summary: c.summary,
|
|
538
|
-
primary: c.primary,
|
|
539
|
-
accessRole: c.accessRole
|
|
540
|
-
});
|
|
378
|
+
const errorMessage = errorObj?.message ?? (typeof data?.message === "string" ? data.message : `HTTP ${response.status} ${response.statusText}`);
|
|
379
|
+
return {
|
|
380
|
+
success: false,
|
|
381
|
+
error: errorMessage,
|
|
382
|
+
serviceAccountEmail
|
|
383
|
+
};
|
|
541
384
|
}
|
|
542
|
-
|
|
543
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
544
|
-
errors.push({ impersonateEmail: email, error: msg });
|
|
385
|
+
return { success: true, status: response.status, data };
|
|
545
386
|
} finally {
|
|
546
387
|
clearTimeout(timeout);
|
|
547
388
|
}
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
391
|
+
return {
|
|
392
|
+
success: false,
|
|
393
|
+
error: msg,
|
|
394
|
+
serviceAccountEmail
|
|
395
|
+
};
|
|
548
396
|
}
|
|
549
|
-
return {
|
|
550
|
-
success: true,
|
|
551
|
-
calendars: aggregated,
|
|
552
|
-
errors
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// ../connectors/src/connectors/google-calendar/setup.ts
|
|
558
|
-
var listCalendarsToolName = `google-calendar-service-account_${listCalendarsTool.name}`;
|
|
559
|
-
var googleCalendarOnboarding = new ConnectorOnboarding({
|
|
560
|
-
connectionSetupInstructions: {
|
|
561
|
-
ja: `\u4EE5\u4E0B\u306E\u624B\u9806\u3067Google Calendar\u30B3\u30CD\u30AF\u30B7\u30E7\u30F3\u306E\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u3092\u884C\u3063\u3066\u304F\u3060\u3055\u3044\u3002\u63A5\u7D9A\u4F5C\u6210\u6642\u306B\u306F\u30B5\u30FC\u30D3\u30B9\u30A2\u30AB\u30A6\u30F3\u30C8JSON\u306E\u307F\u304C\u8A2D\u5B9A\u6E08\u307F\u3067\u3001\u5BFE\u8C61\u30E6\u30FC\u30B6\u30FC\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3084\u30AB\u30EC\u30F3\u30C0\u30FCID\u306F\u3053\u306E\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u4E2D\u306B\u53D6\u5F97\u3057\u307E\u3059\u3002
|
|
562
|
-
|
|
563
|
-
1. \`askUserQuestion\` \u3067\u30E6\u30FC\u30B6\u30FC\u306B\u3001\u30B5\u30FC\u30D3\u30B9\u30A2\u30AB\u30A6\u30F3\u30C8\u304CDomain-wide Delegation\u3067\u4EE3\u7406\u30A2\u30AF\u30BB\u30B9\u3059\u308BGoogle Workspace\u30E6\u30FC\u30B6\u30FC\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u30D2\u30A2\u30EA\u30F3\u30B0\u3059\u308B:
|
|
564
|
-
- \`type\`: \`"freeText"\`
|
|
565
|
-
- \`question\`: \u300C\u30A2\u30AF\u30BB\u30B9\u3057\u305F\u3044\u30AB\u30EC\u30F3\u30C0\u30FC\u3092\u6301\u3064\u30E6\u30FC\u30B6\u30FC\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u8907\u6570\u3042\u308B\u5834\u5408\u306F\u30AB\u30F3\u30DE\u533A\u5207\u308A\u3067\u5165\u529B\u53EF\uFF09\u300D
|
|
566
|
-
- \`placeholder\`: \`"user@example.com, admin@example.com"\`
|
|
567
|
-
2. \u30E6\u30FC\u30B6\u30FC\u304B\u3089\u53D7\u3051\u53D6\u3063\u305F\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\uFF08\u30AB\u30F3\u30DE\u533A\u5207\u308A\u5BFE\u5FDC\uFF09\u3092 \`updateConnectionParameters\` \u3067\u4FDD\u5B58\u3059\u308B:
|
|
568
|
-
- \`parameterSlug\`: \`"impersonate-email"\`
|
|
569
|
-
- \`options\`: \`[{ value: <\u5165\u529B\u3055\u308C\u305F\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u6587\u5B57\u5217>, label: <\u540C\u3058\u5024> }]\`\uFF081\u4EF6\u306E\u307F\u306E\u30AA\u30D7\u30B7\u30E7\u30F3\u306F\u81EA\u52D5\u9078\u629E\u3055\u308C\u308B\uFF09
|
|
570
|
-
3. \`${listCalendarsToolName}\` \u3092\u547C\u3073\u51FA\u3057\u3001\u4FDD\u5B58\u3057\u305F\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3067\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u306A\u30AB\u30EC\u30F3\u30C0\u30FC\u4E00\u89A7\u3092\u53D6\u5F97\u3059\u308B\u3002
|
|
571
|
-
- \`errors\` \u306B\u30A8\u30E9\u30FC\u304C\u3042\u308A \`calendars\` \u304C\u7A7A\u306E\u5834\u5408\uFF08\u3059\u3079\u3066\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3067\u5931\u6557\uFF09\u3001\u5165\u529B\u3055\u308C\u305F\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u304C\u5B58\u5728\u3057\u306A\u3044\u53EF\u80FD\u6027\u304C\u3042\u308B\u3002\`askUserQuestion\` \u3067\u300C{\u5165\u529B\u3057\u305F\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9} \u306E\u30AB\u30EC\u30F3\u30C0\u30FC\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u30A2\u30C9\u30EC\u30B9\u306B\u8AA4\u308A\u304C\u3042\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002\u4F3C\u305F\u30A2\u30C9\u30EC\u30B9\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u304B\uFF1F\u300D\u3068\u805E\u304D\u8FD4\u3057\u3001\u30B9\u30C6\u30C3\u30D72\u304B\u3089\u518D\u5EA6\u5B9F\u884C\u3059\u308B
|
|
572
|
-
4. \u8FD4\u5374\u3055\u308C\u305F \`calendars\` \u914D\u5217\uFF08\u5404\u8981\u7D20: \`{ impersonateEmail, id, summary, primary, accessRole }\`\uFF09\u3092\u5143\u306B\u300C\u4F7F\u7528\u3059\u308B\u30AB\u30EC\u30F3\u30C0\u30FC\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u300D\u3068\u77ED\u304F\u4F1D\u3048\u305F\u4E0A\u3067\u3001\`updateConnectionParameters\` \u3092\u547C\u3073\u51FA\u3059:
|
|
573
|
-
- \`parameterSlug\`: \`"calendar-id"\`
|
|
574
|
-
- \`options\`: \u5404 option \u306E \`label\` \u306F \`\u30AB\u30EC\u30F3\u30C0\u30FC\u540D (owner: impersonateEmail)\` \u306E\u5F62\u5F0F\u3001\`value\` \u306F\u30AB\u30EC\u30F3\u30C0\u30FCID
|
|
575
|
-
- \`errors\` \u306B\u5931\u6557\u3057\u305F\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u304C\u3042\u308B\u5834\u5408\u306F\u3001\u305D\u306E\u65E8\u3092\u77ED\u304F\u4F1D\u3048\u308B
|
|
576
|
-
5. \u30E6\u30FC\u30B6\u30FC\u304C\u9078\u629E\u3057\u305F\u30AB\u30EC\u30F3\u30C0\u30FC\u306E \`label\` \u304B\u3089 owner \u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u62BD\u51FA\u3057\u3001\`updateConnectionParameters\` \u3092\u547C\u3073\u51FA\u3057\u3066 \`impersonate-email\` \u3092\u6700\u7D42\u5024\u3067\u4E0A\u66F8\u304D\u3059\u308B:
|
|
577
|
-
- \`parameterSlug\`: \`"impersonate-email"\`
|
|
578
|
-
- \`options\`: \`[{ value: <ownerEmail>, label: <ownerEmail> }]\`
|
|
579
|
-
|
|
580
|
-
#### \u5236\u7D04
|
|
581
|
-
- **\u30B5\u30FC\u30D3\u30B9\u30A2\u30AB\u30A6\u30F3\u30C8\u306E\u30C9\u30E1\u30A4\u30F3\u5168\u4F53\u306E\u59D4\u4EFB\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059**\u3002\`${listCalendarsToolName}\` \u306E \`errors\` \u306B\u6A29\u9650\u30A8\u30E9\u30FC\u304C\u51FA\u308B\u5834\u5408\u3001Google Workspace\u7BA1\u7406\u8005\u306BDomain-wide Delegation\u306E\u8A2D\u5B9A\u78BA\u8A8D\u3092\u4FC3\u3057\u3066\u304F\u3060\u3055\u3044
|
|
582
|
-
- \u30C4\u30FC\u30EB\u9593\u306F1\u6587\u3060\u3051\u66F8\u3044\u3066\u5373\u6B21\u306E\u30C4\u30FC\u30EB\u547C\u3073\u51FA\u3057\u3002\u4E0D\u8981\u306A\u8AAC\u660E\u306F\u7701\u7565\u3057\u3001\u52B9\u7387\u7684\u306B\u9032\u3081\u308B`,
|
|
583
|
-
en: `Follow these steps to set up the Google Calendar connection. Only the service account JSON is provided at connection creation time \u2014 the target user email and calendar ID are collected during this setup flow.
|
|
584
|
-
|
|
585
|
-
1. Call \`askUserQuestion\` to ask the user for the Google Workspace user email the service account will impersonate via Domain-wide Delegation:
|
|
586
|
-
- \`type\`: \`"freeText"\`
|
|
587
|
-
- \`question\`: "Please enter the email address of the user whose calendar you want to access (comma-separated list allowed for multiple users)"
|
|
588
|
-
- \`placeholder\`: \`"user@example.com, admin@example.com"\`
|
|
589
|
-
2. Save the email(s) the user provided (comma-separated supported) via \`updateConnectionParameters\`:
|
|
590
|
-
- \`parameterSlug\`: \`"impersonate-email"\`
|
|
591
|
-
- \`options\`: \`[{ value: <the email string entered>, label: <same value> }]\` (a single option is auto-selected)
|
|
592
|
-
3. Call \`${listCalendarsToolName}\` to list calendars accessible via the saved email(s).
|
|
593
|
-
- If \`errors\` is non-empty and \`calendars\` is empty (all emails failed), the entered address may not exist. Use \`askUserQuestion\` to ask: "Could not access the calendar for {entered email}. The address may be incorrect \u2014 did you mean a similar address?" Then re-run from step 2 with the new input
|
|
594
|
-
4. Using the returned \`calendars\` array (each item: \`{ impersonateEmail, id, summary, primary, accessRole }\`), briefly say "Please select a calendar." then call \`updateConnectionParameters\`:
|
|
595
|
-
- \`parameterSlug\`: \`"calendar-id"\`
|
|
596
|
-
- \`options\`: Each option's \`label\` should be \`Calendar Name (owner: impersonateEmail)\`, \`value\` should be the calendar ID
|
|
597
|
-
- If \`errors\` contains failing email addresses, briefly mention them
|
|
598
|
-
5. Extract the owner email from the \`label\` of the user's selected calendar, then call \`updateConnectionParameters\` to overwrite \`impersonate-email\` with the final value:
|
|
599
|
-
- \`parameterSlug\`: \`"impersonate-email"\`
|
|
600
|
-
- \`options\`: \`[{ value: <ownerEmail>, label: <ownerEmail> }]\`
|
|
601
|
-
|
|
602
|
-
#### Constraints
|
|
603
|
-
- **Domain-wide Delegation must be configured on the service account**. If \`${listCalendarsToolName}\` returns permission errors in the \`errors\` field, ask the user to verify the Domain-wide Delegation setup with their Google Workspace administrator
|
|
604
|
-
- Write only 1 sentence between tool calls, then immediately call the next tool. Skip unnecessary explanations and proceed efficiently`
|
|
605
|
-
},
|
|
606
|
-
dataOverviewInstructions: {
|
|
607
|
-
en: `1. Call google-calendar-service-account_request with GET /calendars/{calendarId} to get the default calendar's metadata
|
|
608
|
-
2. Call google-calendar-service-account_request with GET /users/me/calendarList to list all accessible calendars
|
|
609
|
-
3. Call google-calendar-service-account_request with GET /calendars/{calendarId}/events with query params timeMin (RFC3339) and maxResults=10 to sample upcoming events`,
|
|
610
|
-
ja: `1. google-calendar-service-account_request \u3067 GET /calendars/{calendarId} \u3092\u547C\u3073\u51FA\u3057\u3001\u30C7\u30D5\u30A9\u30EB\u30C8\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u53D6\u5F97
|
|
611
|
-
2. google-calendar-service-account_request \u3067 GET /users/me/calendarList \u3092\u547C\u3073\u51FA\u3057\u3001\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u306A\u5168\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u4E00\u89A7\u3092\u53D6\u5F97
|
|
612
|
-
3. google-calendar-service-account_request \u3067 GET /calendars/{calendarId}/events \u3092\u30AF\u30A8\u30EA\u30D1\u30E9\u30E1\u30FC\u30BF timeMin\uFF08RFC3339\u5F62\u5F0F\uFF09\u3068 maxResults=10 \u3067\u547C\u3073\u51FA\u3057\u3001\u76F4\u8FD1\u306E\u30A4\u30D9\u30F3\u30C8\u3092\u30B5\u30F3\u30D7\u30EA\u30F3\u30B0`
|
|
613
397
|
}
|
|
614
398
|
});
|
|
615
399
|
|
|
616
|
-
// ../connectors/src/connectors/google-calendar/tools/request.ts
|
|
400
|
+
// ../connectors/src/connectors/google-calendar/tools/request-with-delegation.ts
|
|
617
401
|
import { z as z2 } from "zod";
|
|
618
402
|
var BASE_URL3 = "https://www.googleapis.com/calendar/v3";
|
|
619
403
|
var REQUEST_TIMEOUT_MS2 = 6e4;
|
|
404
|
+
function decodeServiceAccount2(keyJsonBase64) {
|
|
405
|
+
const decoded = Buffer.from(keyJsonBase64, "base64").toString("utf-8");
|
|
406
|
+
return JSON.parse(decoded);
|
|
407
|
+
}
|
|
620
408
|
var inputSchema2 = z2.object({
|
|
621
409
|
toolUseIntent: z2.string().optional().describe(
|
|
622
410
|
"Brief description of what you intend to accomplish with this tool call"
|
|
@@ -624,13 +412,18 @@ var inputSchema2 = z2.object({
|
|
|
624
412
|
connectionId: z2.string().describe("ID of the Google Calendar connection to use"),
|
|
625
413
|
method: z2.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).describe("HTTP method"),
|
|
626
414
|
path: z2.string().describe(
|
|
627
|
-
"API path appended to https://www.googleapis.com/calendar/v3 (e.g., '/calendars/
|
|
415
|
+
"API path appended to https://www.googleapis.com/calendar/v3 (e.g., '/users/me/calendarList', '/calendars/alice@example.com/events'). Write the calendar ID directly into the path \u2014 there is no placeholder substitution."
|
|
416
|
+
),
|
|
417
|
+
subject: z2.string().describe(
|
|
418
|
+
"Email of the Workspace user to impersonate via Domain-wide Delegation. The token will be issued as this user."
|
|
419
|
+
),
|
|
420
|
+
scopes: z2.array(z2.string()).describe(
|
|
421
|
+
"OAuth scopes the token must include. This connector currently supports read-only operations only \u2014 pass one of ['https://www.googleapis.com/auth/calendar.readonly'] (calendars + events read), ['https://www.googleapis.com/auth/calendar.events.readonly'] (events read only), or ['https://www.googleapis.com/auth/calendar.freebusy'] (busy/free queries only). Per-endpoint scope reference: https://developers.google.com/calendar/api/auth"
|
|
628
422
|
),
|
|
629
|
-
queryParams: z2.record(z2.string(), z2.string()).optional().describe(
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
)
|
|
423
|
+
queryParams: z2.record(z2.string(), z2.string()).optional().describe(
|
|
424
|
+
"Query parameters to append to the URL (e.g., { timeMin: '2025-01-01T00:00:00Z', maxResults: '10' })"
|
|
425
|
+
),
|
|
426
|
+
body: z2.record(z2.string(), z2.unknown()).optional().describe("JSON request body for POST/PUT/PATCH")
|
|
634
427
|
});
|
|
635
428
|
var outputSchema2 = z2.discriminatedUnion("success", [
|
|
636
429
|
z2.object({
|
|
@@ -640,17 +433,16 @@ var outputSchema2 = z2.discriminatedUnion("success", [
|
|
|
640
433
|
}),
|
|
641
434
|
z2.object({
|
|
642
435
|
success: z2.literal(false),
|
|
643
|
-
error: z2.string()
|
|
436
|
+
error: z2.string(),
|
|
437
|
+
serviceAccountEmail: z2.string().optional()
|
|
644
438
|
})
|
|
645
439
|
]);
|
|
646
|
-
var
|
|
647
|
-
name: "
|
|
648
|
-
description: `
|
|
649
|
-
Authentication is handled automatically using a service account.
|
|
650
|
-
{calendarId} in the path is automatically replaced with the connection's default calendar ID.`,
|
|
440
|
+
var requestWithDelegationTool = new ConnectorTool({
|
|
441
|
+
name: "request_with_delegation",
|
|
442
|
+
description: "Call the Google Calendar API on behalf of the specified Workspace user via Domain-wide Delegation. Read-only operations only. Pass `subject` as the target user email and `scopes` as a read-only Calendar scope (e.g., ['https://www.googleapis.com/auth/calendar.readonly']). Use this tool when the project knowledge records the calendar with `(delegation, subject: <email>, ...)`. Requires DwD to be authorized for the service account in the Workspace admin console.",
|
|
651
443
|
inputSchema: inputSchema2,
|
|
652
444
|
outputSchema: outputSchema2,
|
|
653
|
-
async execute({ connectionId, method, path: path2, queryParams, body
|
|
445
|
+
async execute({ connectionId, method, path: path2, subject, scopes, queryParams, body }, connections) {
|
|
654
446
|
const connection2 = connections.find((c) => c.id === connectionId);
|
|
655
447
|
if (!connection2) {
|
|
656
448
|
return {
|
|
@@ -658,41 +450,40 @@ Authentication is handled automatically using a service account.
|
|
|
658
450
|
error: `Connection ${connectionId} not found`
|
|
659
451
|
};
|
|
660
452
|
}
|
|
453
|
+
const keyJsonBase64 = parameters.serviceAccountKeyJsonBase64.getValue(connection2);
|
|
454
|
+
let serviceAccount;
|
|
455
|
+
try {
|
|
456
|
+
serviceAccount = decodeServiceAccount2(keyJsonBase64);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
459
|
+
return {
|
|
460
|
+
success: false,
|
|
461
|
+
error: `Failed to decode service account key: ${msg}`
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const serviceAccountEmail = serviceAccount.client_email;
|
|
661
465
|
console.log(
|
|
662
|
-
`[connector-request] google-calendar/${connection2.name}: ${method} ${path2}`
|
|
466
|
+
`[connector-request] google-calendar/${connection2.name}: ${method} ${path2} subject=${subject}`
|
|
663
467
|
);
|
|
664
468
|
try {
|
|
665
469
|
const { GoogleAuth } = await import("google-auth-library");
|
|
666
|
-
const keyJsonBase64 = parameters.serviceAccountKeyJsonBase64.getValue(connection2);
|
|
667
|
-
const impersonateEmail = impersonateEmailParameter.tryGetValue(connection2);
|
|
668
|
-
const calendarId = calendarIdParameter.tryGetValue(connection2) ?? "primary";
|
|
669
|
-
const resolvedSubject = subject ?? impersonateEmail;
|
|
670
|
-
if (!resolvedSubject) {
|
|
671
|
-
return {
|
|
672
|
-
success: false,
|
|
673
|
-
error: `Missing required parameter: ${impersonateEmailParameter.slug}. Configure the user email for this connection.`
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
const credentials = JSON.parse(
|
|
677
|
-
Buffer.from(keyJsonBase64, "base64").toString("utf-8")
|
|
678
|
-
);
|
|
679
470
|
const auth = new GoogleAuth({
|
|
680
|
-
credentials
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
clientOptions: { subject
|
|
471
|
+
credentials: {
|
|
472
|
+
client_email: serviceAccount.client_email,
|
|
473
|
+
private_key: serviceAccount.private_key
|
|
474
|
+
},
|
|
475
|
+
scopes,
|
|
476
|
+
clientOptions: { subject }
|
|
686
477
|
});
|
|
687
478
|
const token = await auth.getAccessToken();
|
|
688
479
|
if (!token) {
|
|
689
480
|
return {
|
|
690
481
|
success: false,
|
|
691
|
-
error: "Failed to obtain access token"
|
|
482
|
+
error: "Failed to obtain access token",
|
|
483
|
+
serviceAccountEmail
|
|
692
484
|
};
|
|
693
485
|
}
|
|
694
|
-
|
|
695
|
-
let url = `${BASE_URL3}${resolvedPath.startsWith("/") ? "" : "/"}${resolvedPath}`;
|
|
486
|
+
let url = `${BASE_URL3}${path2.startsWith("/") ? "" : "/"}${path2}`;
|
|
696
487
|
if (queryParams) {
|
|
697
488
|
const searchParams = new URLSearchParams(queryParams);
|
|
698
489
|
url += `?${searchParams.toString()}`;
|
|
@@ -700,28 +491,24 @@ Authentication is handled automatically using a service account.
|
|
|
700
491
|
const controller = new AbortController();
|
|
701
492
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS2);
|
|
702
493
|
try {
|
|
494
|
+
const hasBody = body != null && ["POST", "PUT", "PATCH"].includes(method);
|
|
703
495
|
const response = await fetch(url, {
|
|
704
496
|
method,
|
|
705
497
|
headers: {
|
|
706
498
|
Authorization: `Bearer ${token}`,
|
|
707
499
|
"Content-Type": "application/json"
|
|
708
500
|
},
|
|
709
|
-
body:
|
|
501
|
+
body: hasBody ? JSON.stringify(body) : void 0,
|
|
710
502
|
signal: controller.signal
|
|
711
503
|
});
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
success: true,
|
|
715
|
-
status: 204,
|
|
716
|
-
data: { message: "Deleted successfully" }
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
const data = await response.json();
|
|
504
|
+
const data = await response.json().catch(() => ({}));
|
|
720
505
|
if (!response.ok) {
|
|
721
506
|
const errorObj = data?.error;
|
|
507
|
+
const errorMessage = errorObj?.message ?? (typeof data?.message === "string" ? data.message : `HTTP ${response.status} ${response.statusText}`);
|
|
722
508
|
return {
|
|
723
509
|
success: false,
|
|
724
|
-
error:
|
|
510
|
+
error: errorMessage,
|
|
511
|
+
serviceAccountEmail
|
|
725
512
|
};
|
|
726
513
|
}
|
|
727
514
|
return { success: true, status: response.status, data };
|
|
@@ -730,13 +517,121 @@ Authentication is handled automatically using a service account.
|
|
|
730
517
|
}
|
|
731
518
|
} catch (err) {
|
|
732
519
|
const msg = err instanceof Error ? err.message : String(err);
|
|
733
|
-
return {
|
|
520
|
+
return {
|
|
521
|
+
success: false,
|
|
522
|
+
error: msg,
|
|
523
|
+
serviceAccountEmail
|
|
524
|
+
};
|
|
734
525
|
}
|
|
735
526
|
}
|
|
736
527
|
});
|
|
737
528
|
|
|
529
|
+
// ../connectors/src/connectors/google-calendar/setup.ts
|
|
530
|
+
var requestToolName = `google-calendar-service-account_${requestTool.name}`;
|
|
531
|
+
var requestWithDelegationToolName = `google-calendar-service-account_${requestWithDelegationTool.name}`;
|
|
532
|
+
var READONLY_SCOPES = '["https://www.googleapis.com/auth/calendar.readonly"]';
|
|
533
|
+
var googleCalendarOnboarding = new ConnectorOnboarding({
|
|
534
|
+
connectionSetupInstructions: {
|
|
535
|
+
ja: `Google Calendar \u30B3\u30CD\u30AF\u30B7\u30E7\u30F3\u306E\u30BB\u30C3\u30C8\u30A2\u30C3\u30D7\u3092\u884C\u3044\u307E\u3059\u3002\u30A2\u30AF\u30BB\u30B9\u3057\u305F\u3044\u30AB\u30EC\u30F3\u30C0\u30FC\u3092\u30E6\u30FC\u30B6\u30FC\u304B\u3089\u805E\u304D\u3001\u5229\u7528\u53EF\u80FD\u306A\u3082\u306E\u3092\u767A\u898B\u3057\u3066 Project Knowledge \u306B\u8A18\u9332\u3057\u307E\u3059\u3002
|
|
536
|
+
|
|
537
|
+
1. \`askUserQuestion\` \u3067\u5BFE\u8C61\u30E6\u30FC\u30B6\u30FC\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u805E\u304F:
|
|
538
|
+
- \`type\`: \`"freeText"\`
|
|
539
|
+
- \`question\`: \u300C\u30A2\u30AF\u30BB\u30B9\u3057\u305F\u3044\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u6240\u6709\u8005\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u8907\u6570\u53EF\u3001\u30AB\u30F3\u30DE\u533A\u5207\u308A\uFF09\u300D
|
|
540
|
+
- \`placeholder\`: \`"alice@example.com, bob@example.com"\`
|
|
541
|
+
|
|
542
|
+
2. \u30E6\u30FC\u30B6\u30FC\u304B\u3089\u53D7\u3051\u53D6\u3063\u305F\u6587\u5B57\u5217\u304B\u3089\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u62BD\u51FA\u3059\u308B\u3002\u4E26\u884C\u3057\u3066\u4E21\u65B9\u306E\u30C4\u30FC\u30EB\u3067\u30AB\u30EC\u30F3\u30C0\u30FC\u3092\u767A\u898B\u3059\u308B:
|
|
543
|
+
a. \`${requestToolName}\` \u3092\u4EE5\u4E0B\u306E\u5F15\u6570\u3067\u547C\u3073\u3001Service Account \u81EA\u8EAB\u306B\u5171\u6709\u3055\u308C\u3066\u3044\u308B\u30AB\u30EC\u30F3\u30C0\u30FC\u3092\u53D6\u5F97\u3059\u308B\u3002\`success: false\` \u306A\u3089\u305D\u306E\u65E8\u8A18\u9332\u3057\u3066\u6B21\u3078:
|
|
544
|
+
- \`method\`: \`"GET"\`
|
|
545
|
+
- \`path\`: \`"/users/me/calendarList"\`
|
|
546
|
+
- \`scopes\`: \`${READONLY_SCOPES}\`
|
|
547
|
+
b. \u5404\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9 \`<email>\` \u306B\u3064\u3044\u3066 \`${requestWithDelegationToolName}\` \u3092\u4EE5\u4E0B\u306E\u5F15\u6570\u3067\u547C\u3073\u3001\u305D\u306E\u30E6\u30FC\u30B6\u30FC\u304C\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u306A\u30AB\u30EC\u30F3\u30C0\u30FC\u3092\u53D6\u5F97\u3059\u308B:
|
|
548
|
+
- \`method\`: \`"GET"\`
|
|
549
|
+
- \`path\`: \`"/users/me/calendarList"\`
|
|
550
|
+
- \`subject\`: \`<email>\`
|
|
551
|
+
- \`scopes\`: \`${READONLY_SCOPES}\`
|
|
552
|
+
|
|
553
|
+
3. \u30B9\u30C6\u30C3\u30D7 2 \u306E\u767A\u898B\u7D50\u679C (a \u3068 b \u306E\u5168\u30AB\u30EC\u30F3\u30C0\u30FC) \u3092\u7D71\u5408\u3057\u3001\`askUserQuestion\` \u3067\u30AB\u30EC\u30F3\u30C0\u30FC\u9078\u629E\u3092\u6C42\u3081\u308B:
|
|
554
|
+
- 1 \u4EF6\u3082\u898B\u3064\u304B\u3089\u306A\u304B\u3063\u305F\u5834\u5408: \u5931\u6557\u3057\u305F\u30C4\u30FC\u30EB\u306E\u30A8\u30E9\u30FC\u30EC\u30B9\u30DD\u30F3\u30B9\u304B\u3089 \`serviceAccountEmail\` \u3092\u53D6\u308A\u51FA\u3057\u3001\`askUserQuestion\` \u3067\u6B21\u306E\u9078\u629E\u80A2\u3092\u63D0\u793A\u3059\u308B\u3002\`question\` \u306B\u306F\u6B21\u306E\u6848\u5185\u6587\u3092\u542B\u3081\u308B: \u300C\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u306A\u30AB\u30EC\u30F3\u30C0\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u30B5\u30FC\u30D3\u30B9\u30A2\u30AB\u30A6\u30F3\u30C8 \`<serviceAccountEmail>\` \u3067\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u884C\u3063\u3066\u304B\u3089\u7D9A\u884C\u3057\u3066\u304F\u3060\u3055\u3044: (1) Workspace \u7BA1\u7406\u8005\u306B Domain-wide Delegation \u306E\u8A2D\u5B9A\u3092\u4F9D\u983C\u3059\u308B\uFF08[\u8A2D\u5B9A\u30AC\u30A4\u30C9](https://support.google.com/a/answer/162106)\uFF09\u3001(2) \u5BFE\u8C61\u30AB\u30EC\u30F3\u30C0\u30FC\u3092 \`<serviceAccountEmail>\` \u306B\u5171\u6709\u8A2D\u5B9A\u3067\u62DB\u5F85\u3059\u308B\uFF08[\u5171\u6709\u624B\u9806](https://support.google.com/calendar/answer/37082)\uFF09\u300D\u3002
|
|
555
|
+
- \`options\`: \`[{ label: "\u6E96\u5099\u3067\u304D\u305F\u306E\u3067\u30EA\u30C8\u30E9\u30A4", value: "retry" }, { label: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u76F4\u3059", value: "restart" }]\`
|
|
556
|
+
- \u300C\u30EA\u30C8\u30E9\u30A4\u300D: \u76F4\u524D\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u30EA\u30B9\u30C8\u3067\u30B9\u30C6\u30C3\u30D7 2 \u3092\u518D\u5B9F\u884C
|
|
557
|
+
- \u300C\u5165\u529B\u3057\u76F4\u3059\u300D: \u30B9\u30C6\u30C3\u30D7 1 \u304B\u3089\u518D\u5B9F\u884C
|
|
558
|
+
- \u5931\u6557\u304C\u3042\u3063\u305F\u5834\u5408: \u305D\u306E\u65E8\u3092 1 \u6587\u3067\u4F1D\u3048\u3066\u304B\u3089\u6B21\u306B\u9032\u3080
|
|
559
|
+
- \u30AB\u30EC\u30F3\u30C0\u30FC\u9078\u629E:
|
|
560
|
+
- \`type\`: \`"multiSelect"\`
|
|
561
|
+
- \`question\`: \u300C\u4F7F\u7528\u3059\u308B\u30AB\u30EC\u30F3\u30C0\u30FC\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u8907\u6570\u53EF\uFF09\u300D
|
|
562
|
+
- \`options\`: \u5404\u30AB\u30EC\u30F3\u30C0\u30FC\u306B\u3064\u3044\u3066 \`label\`: \`"<\u30AB\u30EC\u30F3\u30C0\u30FC\u540D> (\u30A2\u30AF\u30BB\u30B9\u7D4C\u8DEF)"\`\u3001\`value\`: \`"<calendarId>"\` \u306E\u5F62\u5F0F\u3067\u69CB\u7BC9\u3002\u30A2\u30AF\u30BB\u30B9\u7D4C\u8DEF\u306F a \u3067\u898B\u3064\u304B\u3063\u305F\u3082\u306E\u306F \`"\u5171\u6709"\`\u3001b \u3067\u898B\u3064\u304B\u3063\u305F\u3082\u306E\u306F \`"DwD: <\u305D\u306E\u3068\u304D\u306E subject>"\` \u306E\u3088\u3046\u306B\u4EBA\u9593\u304C\u5224\u5225\u3067\u304D\u308B\u6587\u5B57\u5217\u306B\u3059\u308B
|
|
563
|
+
|
|
564
|
+
4. \u30E6\u30FC\u30B6\u30FC\u304C\u9078\u629E\u3057\u305F calendarId \u96C6\u5408\u3092\u3001\u30B9\u30C6\u30C3\u30D7 2 \u306E\u30C7\u30A3\u30B9\u30AB\u30D0\u30EA\u7D50\u679C\u3068\u7A81\u304D\u5408\u308F\u305B\u3066\u7D4C\u8DEF\u3068 subject \u3092\u7279\u5B9A\u3059\u308B\u3002\`finalizeSetup\` \u3092\u547C\u3073\u3001\`projectKnowledge\` \u306E \`#### \u30B9\u30B3\u30FC\u30D7\` \u7BC0\u306B\u5404\u30AB\u30EC\u30F3\u30C0\u30FC\u3092 1 \u884C\u305A\u3064\u5217\u6319\u3059\u308B:
|
|
565
|
+
- DwD \u7D4C\u7531\u3067\u30A2\u30AF\u30BB\u30B9\u3059\u308B\u5834\u5408: \`- calendar: <calendarId> (delegation, subject: <subject>, name: "<\u30AB\u30EC\u30F3\u30C0\u30FC\u540D>")\`
|
|
566
|
+
- Service Account \u81EA\u8EAB\u306B\u5171\u6709\u3055\u308C\u3066\u3044\u308B\u5834\u5408: \`- calendar: <calendarId> (service-account, name: "<\u30AB\u30EC\u30F3\u30C0\u30FC\u540D>")\`
|
|
567
|
+
|
|
568
|
+
#### \u5236\u7D04
|
|
569
|
+
- \u4E0A\u8A18\u4EE5\u5916\u306E API \u547C\u3073\u51FA\u3057\u3092 setup \u4E2D\u306B\u884C\u308F\u306A\u3044
|
|
570
|
+
- \u30C4\u30FC\u30EB\u547C\u3073\u51FA\u3057\u306E\u9593\u306F 1 \u6587\u3060\u3051\u66F8\u3044\u3066\u5373\u6B21\u306E\u30C4\u30FC\u30EB\u547C\u3073\u51FA\u3057`,
|
|
571
|
+
en: `Set up the Google Calendar connection. Ask the user which calendars they want to access, discover the available ones, and record them in Project Knowledge.
|
|
572
|
+
|
|
573
|
+
1. Call \`askUserQuestion\` to collect target user emails:
|
|
574
|
+
- \`type\`: \`"freeText"\`
|
|
575
|
+
- \`question\`: "Enter the email addresses of the calendar owners you want to access (comma-separated for multiple)"
|
|
576
|
+
- \`placeholder\`: \`"alice@example.com, bob@example.com"\`
|
|
577
|
+
|
|
578
|
+
2. Extract individual emails from the response. Discover calendars using both tools in parallel:
|
|
579
|
+
a. Call \`${requestToolName}\` to list calendars shared directly with the service account. If \`success: false\`, record the failure and continue:
|
|
580
|
+
- \`method\`: \`"GET"\`
|
|
581
|
+
- \`path\`: \`"/users/me/calendarList"\`
|
|
582
|
+
- \`scopes\`: \`${READONLY_SCOPES}\`
|
|
583
|
+
b. For each email \`<email>\`, call \`${requestWithDelegationToolName}\` to list calendars accessible by that user via Domain-wide Delegation:
|
|
584
|
+
- \`method\`: \`"GET"\`
|
|
585
|
+
- \`path\`: \`"/users/me/calendarList"\`
|
|
586
|
+
- \`subject\`: \`<email>\`
|
|
587
|
+
- \`scopes\`: \`${READONLY_SCOPES}\`
|
|
588
|
+
|
|
589
|
+
3. Aggregate the discovery results from step 2 and call \`askUserQuestion\` for calendar selection:
|
|
590
|
+
- If no calendars were found: take \`serviceAccountEmail\` from any failed tool response and call \`askUserQuestion\`. Include this guidance in \`question\`: "No accessible calendars found. With service account \`<serviceAccountEmail>\`, please do one of the following before continuing: (1) Ask your Workspace admin to authorize Domain-wide Delegation ([setup guide](https://support.google.com/a/answer/162106)), or (2) Share the target calendars with \`<serviceAccountEmail>\` ([sharing guide](https://support.google.com/calendar/answer/37082))".
|
|
591
|
+
- \`options\`: \`[{ label: "Ready \u2014 retry", value: "retry" }, { label: "Re-enter the email addresses", value: "restart" }]\`
|
|
592
|
+
- On "retry" \u2192 re-run step 2 with the previously entered email list
|
|
593
|
+
- On "Re-enter" \u2192 re-run step 1
|
|
594
|
+
- If there were partial failures: briefly mention them and proceed.
|
|
595
|
+
- Calendar selection:
|
|
596
|
+
- \`type\`: \`"multiSelect"\`
|
|
597
|
+
- \`question\`: "Select the calendars to use (multiple allowed)"
|
|
598
|
+
- \`options\`: For each calendar, \`label\`: \`"<calendar name> (access path)"\`, \`value\`: \`"<calendarId>"\`. Make the access path human-readable: \`"shared"\` for calendars found via 2a, \`"DwD: <the subject used>"\` for calendars found via 2b
|
|
599
|
+
|
|
600
|
+
4. Cross-reference the selected calendarIds with the step 2 discovery results to recover each calendar's access path and subject. Call \`finalizeSetup\`. Under \`#### \u30B9\u30B3\u30FC\u30D7\` in \`projectKnowledge\`, list each calendar on its own line:
|
|
601
|
+
- Accessed via Domain-wide Delegation: \`- calendar: <calendarId> (delegation, subject: <subject>, name: "<calendar name>")\`
|
|
602
|
+
- Shared directly with the service account: \`- calendar: <calendarId> (service-account, name: "<calendar name>")\`
|
|
603
|
+
|
|
604
|
+
#### Constraints
|
|
605
|
+
- Do not call any other API endpoints during setup
|
|
606
|
+
- Write at most 1 sentence between tool calls`
|
|
607
|
+
},
|
|
608
|
+
dataOverviewInstructions: {
|
|
609
|
+
en: `For each calendar recorded under \`#### \u30B9\u30B3\u30FC\u30D7\`, fetch metadata and a small sample of upcoming events. The annotation on each line tells you which tool to use:
|
|
610
|
+
- \`(delegation, subject: <email>, ...)\` \u2192 \`${requestWithDelegationToolName}\` with \`subject: <email>\`
|
|
611
|
+
- \`(service-account, ...)\` \u2192 \`${requestToolName}\`
|
|
612
|
+
|
|
613
|
+
Pass \`scopes: ${READONLY_SCOPES}\` for every call.
|
|
614
|
+
|
|
615
|
+
For each calendar:
|
|
616
|
+
1. \`method=GET\`, \`path=/calendars/<id>\` to fetch metadata.
|
|
617
|
+
2. \`method=GET\`, \`path=/calendars/<id>/events\`, \`queryParams={ timeMin: <RFC3339 now>, maxResults: "10", singleEvents: "true", orderBy: "startTime" }\`.`,
|
|
618
|
+
ja: `\`#### \u30B9\u30B3\u30FC\u30D7\` \u306E\u5404\u30AB\u30EC\u30F3\u30C0\u30FC\u306B\u3064\u3044\u3066\u3001\u30E1\u30BF\u30C7\u30FC\u30BF\u3068\u76F4\u8FD1\u306E\u30A4\u30D9\u30F3\u30C8\u3092\u5C11\u91CF\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u5404\u884C\u306E\u6CE8\u91C8\u3067\u4F7F\u7528\u3059\u308B\u30C4\u30FC\u30EB\u3092\u9078\u3073\u307E\u3059:
|
|
619
|
+
- \`(delegation, subject: <email>, ...)\` \u2192 \`${requestWithDelegationToolName}\` \u3092 \`subject: <email>\` \u4ED8\u304D\u3067\u547C\u3076
|
|
620
|
+
- \`(service-account, ...)\` \u2192 \`${requestToolName}\` \u3092\u547C\u3076
|
|
621
|
+
|
|
622
|
+
\`scopes\` \u306F\u6BCE\u56DE \`${READONLY_SCOPES}\` \u3092\u6E21\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
623
|
+
|
|
624
|
+
\u5404\u30AB\u30EC\u30F3\u30C0\u30FC\u306B\u3064\u3044\u3066:
|
|
625
|
+
1. \`method=GET\`\u3001\`path=/calendars/<id>\` \u3067\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u53D6\u5F97
|
|
626
|
+
2. \`method=GET\`\u3001\`path=/calendars/<id>/events\`\u3001\`queryParams={ timeMin: <RFC3339 \u306E\u73FE\u5728\u6642\u523B>, maxResults: "10", singleEvents: "true", orderBy: "startTime" }\``
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
738
630
|
// ../connectors/src/connectors/google-calendar/index.ts
|
|
739
|
-
var tools = {
|
|
631
|
+
var tools = {
|
|
632
|
+
request: requestTool,
|
|
633
|
+
request_with_delegation: requestWithDelegationTool
|
|
634
|
+
};
|
|
740
635
|
var googleCalendarConnector = new ConnectorPlugin({
|
|
741
636
|
slug: "google-calendar",
|
|
742
637
|
authType: AUTH_TYPES.SERVICE_ACCOUNT,
|
|
@@ -749,58 +644,45 @@ var googleCalendarConnector = new ConnectorPlugin({
|
|
|
749
644
|
systemPrompt: {
|
|
750
645
|
en: `### Tools
|
|
751
646
|
|
|
752
|
-
|
|
647
|
+
This connector exposes two request tools that correspond to the two ways a Service Account can authenticate against the Google Calendar API:
|
|
753
648
|
|
|
754
|
-
|
|
649
|
+
- \`google-calendar-service-account_request_with_delegation\`: Call the Calendar API on behalf of the specified Workspace user via Domain-wide Delegation. Pass \`subject\` as the target user email. Requires DwD to be authorized for the service account in the Workspace admin console.
|
|
650
|
+
- \`google-calendar-service-account_request\`: Call the Calendar API as the service account itself (no delegation). Only calendars explicitly shared with the service account email are accessible.
|
|
755
651
|
|
|
756
|
-
|
|
652
|
+
Both tools require a \`scopes\` argument.
|
|
757
653
|
|
|
758
|
-
|
|
759
|
-
- \`client.listCalendars()\` \u2014 list all accessible calendars
|
|
760
|
-
- \`client.listEvents(options?, calendarId?)\` \u2014 list events with optional filters
|
|
761
|
-
- \`client.getEvent(eventId, calendarId?)\` \u2014 get a single event by ID
|
|
762
|
-
- \`client.request(path, init?)\` \u2014 low-level authenticated fetch
|
|
654
|
+
### OAuth Scopes (pass as \`scopes\` argument)
|
|
763
655
|
|
|
764
|
-
|
|
656
|
+
This connector is currently read-only. Pass one of:
|
|
765
657
|
|
|
766
|
-
|
|
658
|
+
- \`https://www.googleapis.com/auth/calendar.readonly\` \u2014 read-only on calendars and events
|
|
659
|
+
- \`https://www.googleapis.com/auth/calendar.events.readonly\` \u2014 read-only on events (no calendar metadata)
|
|
660
|
+
- \`https://www.googleapis.com/auth/calendar.freebusy\` \u2014 busy/free time queries only
|
|
767
661
|
|
|
768
|
-
|
|
769
|
-
const calendar = connection("<connectionId>", { subject: "other-user@example.com" });
|
|
770
|
-
\`\`\`
|
|
662
|
+
For \`request_with_delegation\`, the Workspace admin must have authorized the requested scope for the service account in the Domain-wide Delegation settings, otherwise token issuance will fail with \`unauthorized_client\`.
|
|
771
663
|
|
|
772
|
-
|
|
773
|
-
import type { Context } from "hono";
|
|
774
|
-
import { connection } from "@squadbase/vite-server/connectors/google-calendar";
|
|
664
|
+
Per-endpoint scope reference: https://developers.google.com/calendar/api/auth
|
|
775
665
|
|
|
776
|
-
|
|
666
|
+
### Choosing the right tool
|
|
777
667
|
|
|
778
|
-
|
|
779
|
-
const now = new Date().toISOString();
|
|
780
|
-
const { items } = await calendar.listEvents({
|
|
781
|
-
timeMin: now,
|
|
782
|
-
maxResults: 10,
|
|
783
|
-
singleEvents: true,
|
|
784
|
-
orderBy: "startTime",
|
|
785
|
-
});
|
|
668
|
+
Read \`#### \u30B9\u30B3\u30FC\u30D7\` in the project knowledge for this connection. Each calendar appears as a \`- calendar: <id> (...)\` line whose annotation tells you which tool to use:
|
|
786
669
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
\`\`\`
|
|
670
|
+
- \`(delegation, subject: <email>, name: "...")\` \u2192 use \`request_with_delegation\` and pass \`subject: <email>\`
|
|
671
|
+
- \`(service-account, name: "...")\` \u2192 use \`request\` (no \`subject\`)
|
|
672
|
+
|
|
673
|
+
### Path conventions
|
|
674
|
+
|
|
675
|
+
Write the calendar ID directly into the path \u2014 there is no placeholder substitution. Examples:
|
|
676
|
+
|
|
677
|
+
- \`/users/me/calendarList\` \u2014 list calendars accessible to the authenticated identity
|
|
678
|
+
- \`/calendars/alice@example.com/events\` \u2014 events on alice's primary calendar
|
|
679
|
+
- \`/calendars/c_abc123@group.calendar.google.com/events\` \u2014 events on a secondary calendar
|
|
798
680
|
|
|
799
681
|
### Google Calendar API v3 Reference
|
|
800
682
|
|
|
801
683
|
#### Available Endpoints
|
|
802
684
|
- GET \`/calendars/{calendarId}\` \u2014 Get calendar metadata
|
|
803
|
-
- GET \`/users/me/calendarList\` \u2014 List all calendars accessible by the authenticated
|
|
685
|
+
- GET \`/users/me/calendarList\` \u2014 List all calendars accessible by the authenticated identity
|
|
804
686
|
- GET \`/calendars/{calendarId}/events\` \u2014 List events on a calendar
|
|
805
687
|
- GET \`/calendars/{calendarId}/events/{eventId}\` \u2014 Get a single event
|
|
806
688
|
|
|
@@ -812,66 +694,93 @@ export default async function handler(c: Context) {
|
|
|
812
694
|
- \`orderBy=startTime\` \u2014 Order by start time (requires singleEvents=true)
|
|
813
695
|
- \`q\` \u2014 Free text search terms
|
|
814
696
|
|
|
815
|
-
#### Tips
|
|
816
|
-
- Use \`{calendarId}\` placeholder in paths \u2014 it is automatically replaced with the configured default calendar ID
|
|
817
|
-
- Set \`singleEvents=true\` to expand recurring events into individual instances
|
|
818
|
-
- When using \`orderBy=startTime\`, you must also set \`singleEvents=true\`
|
|
819
|
-
- Use RFC3339 format for time parameters (e.g., "2024-01-15T09:00:00Z" or "2024-01-15T09:00:00+09:00")
|
|
820
|
-
- The default calendar ID is "primary" if not configured`,
|
|
821
|
-
ja: `### \u30C4\u30FC\u30EB
|
|
822
|
-
|
|
823
|
-
- \`google-calendar-service-account_request\`: Google Calendar API\u3092\u547C\u3073\u51FA\u3059\u552F\u4E00\u306E\u624B\u6BB5\u3067\u3059\u3002\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u4E00\u89A7\u53D6\u5F97\u3001\u30A4\u30D9\u30F3\u30C8\u306E\u53D6\u5F97\u3001\u30AB\u30EC\u30F3\u30C0\u30FC\u30C7\u30FC\u30BF\u306E\u7BA1\u7406\u306B\u4F7F\u7528\u3057\u307E\u3059\u3002\u30B5\u30FC\u30D3\u30B9\u30A2\u30AB\u30A6\u30F3\u30C8\uFF0BDomain-wide Delegation\u3067\u8A8D\u8A3C\u304C\u81EA\u52D5\u7684\u306B\u51E6\u7406\u3055\u308C\u3001\u30B3\u30CD\u30AF\u30B7\u30E7\u30F3\u306B\u8A2D\u5B9A\u3055\u308C\u305F\u30E6\u30FC\u30B6\u30FC\uFF08\`impersonate-email\`\u30D1\u30E9\u30E1\u30FC\u30BF\uFF09\u3068\u3057\u3066\u52D5\u4F5C\u3057\u307E\u3059\u3002\u30D1\u30B9\u5185\u306E{calendarId}\u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u30FC\u306F\u8A2D\u5B9A\u6E08\u307F\u306E\u30C7\u30D5\u30A9\u30EB\u30C8\u30AB\u30EC\u30F3\u30C0\u30FCID\u3067\u81EA\u52D5\u7684\u306B\u7F6E\u63DB\u3055\u308C\u307E\u3059\u3002\u7279\u5B9A\u30EA\u30AF\u30A8\u30B9\u30C8\u3067\u8A2D\u5B9A\u30E6\u30FC\u30B6\u30FC\u3092\u4E0A\u66F8\u304D\u3057\u305F\u3044\u5834\u5408\u306E\u307F\u3001\u30AA\u30D7\u30B7\u30E7\u30F3\u306E\`subject\`\u30D1\u30E9\u30E1\u30FC\u30BF\u3092\u6E21\u3057\u3066\u304F\u3060\u3055\u3044\u3002
|
|
824
|
-
|
|
825
697
|
### Business Logic
|
|
826
698
|
|
|
827
|
-
|
|
699
|
+
The business logic type for this connector is "typescript". Use the connector SDK in your handler. Do NOT read credentials from environment variables.
|
|
828
700
|
|
|
829
|
-
SDK
|
|
830
|
-
- \`client.listCalendars()\` \u2014 \u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u306A\u5168\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u4E00\u89A7\u53D6\u5F97
|
|
831
|
-
- \`client.listEvents(options?, calendarId?)\` \u2014 \u30D5\u30A3\u30EB\u30BF\u30FC\u4ED8\u304D\u30A4\u30D9\u30F3\u30C8\u4E00\u89A7\u53D6\u5F97
|
|
832
|
-
- \`client.getEvent(eventId, calendarId?)\` \u2014 ID\u306B\u3088\u308B\u5358\u4E00\u30A4\u30D9\u30F3\u30C8\u53D6\u5F97
|
|
833
|
-
- \`client.request(path, init?)\` \u2014 \u4F4E\u30EC\u30D9\u30EB\u306E\u8A8D\u8A3C\u4ED8\u304Dfetch
|
|
701
|
+
SDK methods (client created via \`connection(connectionId)\`):
|
|
834
702
|
|
|
835
|
-
|
|
703
|
+
- \`client.requestWithDelegation(path, { subject, scopes, init? })\` \u2014 call the API as the impersonated Workspace user via Domain-wide Delegation
|
|
704
|
+
- \`client.request(path, { scopes, init? })\` \u2014 call the API as the service account itself (only calendars shared with the SA email are accessible)
|
|
836
705
|
|
|
837
|
-
|
|
706
|
+
Both methods take \`scopes\` \u2014 pass the minimum scope(s) for the endpoint. Both return a standard \`Response\`. Read the body with \`.json()\`. Same path conventions as the tools.
|
|
838
707
|
|
|
839
|
-
|
|
840
|
-
const calendar = connection("<connectionId>", { subject: "other-user@example.com" });
|
|
841
|
-
\`\`\`
|
|
708
|
+
#### Example
|
|
842
709
|
|
|
843
710
|
\`\`\`ts
|
|
844
711
|
import type { Context } from "hono";
|
|
845
712
|
import { connection } from "@squadbase/vite-server/connectors/google-calendar";
|
|
846
713
|
|
|
847
714
|
const calendar = connection("<connectionId>");
|
|
715
|
+
const READ = ["https://www.googleapis.com/auth/calendar.readonly"];
|
|
848
716
|
|
|
849
717
|
export default async function handler(c: Context) {
|
|
850
718
|
const now = new Date().toISOString();
|
|
851
|
-
const
|
|
719
|
+
const qs = new URLSearchParams({
|
|
852
720
|
timeMin: now,
|
|
853
|
-
maxResults: 10,
|
|
854
|
-
singleEvents: true,
|
|
721
|
+
maxResults: "10",
|
|
722
|
+
singleEvents: "true",
|
|
855
723
|
orderBy: "startTime",
|
|
856
724
|
});
|
|
857
725
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
726
|
+
// Project knowledge says: alice@example.com is reachable via delegation
|
|
727
|
+
const aliceRes = await calendar.requestWithDelegation(
|
|
728
|
+
\`/calendars/alice@example.com/events?\${qs}\`,
|
|
729
|
+
{ subject: "alice@example.com", scopes: READ },
|
|
730
|
+
);
|
|
731
|
+
const alice = await aliceRes.json();
|
|
732
|
+
|
|
733
|
+
// Project knowledge says: team@example.com is shared with the SA
|
|
734
|
+
const teamRes = await calendar.request(
|
|
735
|
+
\`/calendars/team@example.com/events?\${qs}\`,
|
|
736
|
+
{ scopes: READ },
|
|
866
737
|
);
|
|
738
|
+
const team = await teamRes.json();
|
|
739
|
+
|
|
740
|
+
return c.json({ alice: alice.items, team: team.items });
|
|
867
741
|
}
|
|
868
|
-
|
|
742
|
+
\`\`\``,
|
|
743
|
+
ja: `### \u30C4\u30FC\u30EB
|
|
744
|
+
|
|
745
|
+
\u3053\u306E\u30B3\u30CD\u30AF\u30BF\u30FC\u306F\u3001Service Account \u304C Google Calendar API \u306B\u8A8D\u8A3C\u3059\u308B 2 \u3064\u306E\u65B9\u6CD5\u306B\u5BFE\u5FDC\u3059\u308B 2 \u3064\u306E request \u30C4\u30FC\u30EB\u3092\u516C\u958B\u3057\u307E\u3059:
|
|
746
|
+
|
|
747
|
+
- \`google-calendar-service-account_request_with_delegation\`: \u6307\u5B9A\u3055\u308C\u305F Workspace \u30E6\u30FC\u30B6\u30FC\u306B\u4EE3\u308F\u3063\u3066 Domain-wide Delegation \u7D4C\u7531\u3067 Calendar API \u3092\u547C\u3073\u51FA\u3057\u307E\u3059\u3002\u4EE3\u7406\u5BFE\u8C61\u306E\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092 \`subject\` \u3068\u3057\u3066\u6E21\u3057\u3066\u304F\u3060\u3055\u3044\u3002Workspace \u7BA1\u7406\u30B3\u30F3\u30BD\u30FC\u30EB\u3067 Service Account \u306E DwD \u304C\u627F\u8A8D\u3055\u308C\u3066\u3044\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002
|
|
748
|
+
- \`google-calendar-service-account_request\`: Service Account \u81EA\u8EAB\u3068\u3057\u3066 Calendar API \u3092\u547C\u3073\u51FA\u3057\u307E\u3059\uFF08DwD \u306A\u3057\uFF09\u3002Service Account \u306E\u30E1\u30A2\u30C9\u306B\u660E\u793A\u7684\u306B\u5171\u6709\u3055\u308C\u3066\u3044\u308B\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u307F\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u3067\u3059\u3002
|
|
749
|
+
|
|
750
|
+
\u4E21\u30C4\u30FC\u30EB\u3068\u3082 \`scopes\` \u5F15\u6570\u304C\u5FC5\u9808\u3067\u3059\u3002
|
|
751
|
+
|
|
752
|
+
### OAuth \u30B9\u30B3\u30FC\u30D7 (\`scopes\` \u5F15\u6570\u3067\u6E21\u3059)
|
|
753
|
+
|
|
754
|
+
\u3053\u306E\u30B3\u30CD\u30AF\u30BF\u30FC\u306F\u73FE\u72B6\u8AAD\u307F\u53D6\u308A\u5C02\u7528\u3067\u3059\u3002\u6B21\u306E\u3044\u305A\u308C\u304B\u3092\u6E21\u3057\u3066\u304F\u3060\u3055\u3044:
|
|
755
|
+
|
|
756
|
+
- \`https://www.googleapis.com/auth/calendar.readonly\` \u2014 \u30AB\u30EC\u30F3\u30C0\u30FC\u3068\u30A4\u30D9\u30F3\u30C8\u306E\u8AAD\u307F\u53D6\u308A
|
|
757
|
+
- \`https://www.googleapis.com/auth/calendar.events.readonly\` \u2014 \u30A4\u30D9\u30F3\u30C8\u306E\u307F\u8AAD\u307F\u53D6\u308A\uFF08\u30AB\u30EC\u30F3\u30C0\u30FC\u30E1\u30BF\u30C7\u30FC\u30BF\u4E0D\u53EF\uFF09
|
|
758
|
+
- \`https://www.googleapis.com/auth/calendar.freebusy\` \u2014 \u7A7A\u304D\u72B6\u6CC1\u30AF\u30A8\u30EA\u306E\u307F
|
|
759
|
+
|
|
760
|
+
\`request_with_delegation\` \u306E\u5834\u5408\u3001\u8981\u6C42\u3059\u308B scope \u306F Workspace \u7BA1\u7406\u8005\u304C Domain-wide Delegation \u8A2D\u5B9A\u3067\u5F53\u8A72 Service Account \u306B\u5BFE\u3057\u3066\u627F\u8A8D\u3057\u3066\u3044\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002\u627F\u8A8D\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u30C8\u30FC\u30AF\u30F3\u767A\u884C\u304C \`unauthorized_client\` \u3067\u5931\u6557\u3057\u307E\u3059\u3002
|
|
761
|
+
|
|
762
|
+
\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u5225\u306E\u6B63\u78BA\u306A scope \u30EA\u30D5\u30A1\u30EC\u30F3\u30B9: https://developers.google.com/calendar/api/auth
|
|
763
|
+
|
|
764
|
+
### \u9069\u5207\u306A\u30C4\u30FC\u30EB\u306E\u9078\u3073\u65B9
|
|
765
|
+
|
|
766
|
+
\u3053\u306E\u30B3\u30CD\u30AF\u30B7\u30E7\u30F3\u306E Project Knowledge \u306E \`#### \u30B9\u30B3\u30FC\u30D7\` \u3092\u8AAD\u3093\u3067\u304F\u3060\u3055\u3044\u3002\u5404\u30AB\u30EC\u30F3\u30C0\u30FC\u306F \`- calendar: <id> (...)\` \u5F62\u5F0F\u306E\u884C\u3068\u3057\u3066\u8A18\u9332\u3055\u308C\u3066\u304A\u308A\u3001\u6CE8\u91C8\u304C\u3069\u3061\u3089\u306E\u30C4\u30FC\u30EB\u3092\u4F7F\u3046\u3079\u304D\u304B\u3092\u793A\u3057\u307E\u3059:
|
|
767
|
+
|
|
768
|
+
- \`(delegation, subject: <email>, name: "...")\` \u2192 \`request_with_delegation\` \u3092\u4F7F\u3044\u3001\`subject: <email>\` \u3092\u6E21\u3059
|
|
769
|
+
- \`(service-account, name: "...")\` \u2192 \`request\` \u3092\u4F7F\u3046\uFF08\`subject\` \u4E0D\u8981\uFF09
|
|
770
|
+
|
|
771
|
+
### \u30D1\u30B9\u306E\u66F8\u304D\u65B9
|
|
772
|
+
|
|
773
|
+
calendar ID \u3092\u30D1\u30B9\u306B\u76F4\u63A5\u66F8\u3044\u3066\u304F\u3060\u3055\u3044\u3002\u30D7\u30EC\u30FC\u30B9\u30DB\u30EB\u30C0\u30FC\u306E\u7F6E\u63DB\u306F\u3042\u308A\u307E\u305B\u3093\u3002\u4F8B:
|
|
774
|
+
|
|
775
|
+
- \`/users/me/calendarList\` \u2014 \u8A8D\u8A3C\u3055\u308C\u305F\u8B58\u5225\u5B50\u304C\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u306A\u30AB\u30EC\u30F3\u30C0\u30FC\u4E00\u89A7
|
|
776
|
+
- \`/calendars/alice@example.com/events\` \u2014 alice \u306E\u30D7\u30E9\u30A4\u30DE\u30EA\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u30A4\u30D9\u30F3\u30C8
|
|
777
|
+
- \`/calendars/c_abc123@group.calendar.google.com/events\` \u2014 \u4E8C\u6B21\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u30A4\u30D9\u30F3\u30C8
|
|
869
778
|
|
|
870
779
|
### Google Calendar API v3 \u30EA\u30D5\u30A1\u30EC\u30F3\u30B9
|
|
871
780
|
|
|
872
781
|
#### \u5229\u7528\u53EF\u80FD\u306A\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8
|
|
873
782
|
- GET \`/calendars/{calendarId}\` \u2014 \u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u53D6\u5F97
|
|
874
|
-
- GET \`/users/me/calendarList\` \u2014 \u8A8D\u8A3C\
|
|
783
|
+
- GET \`/users/me/calendarList\` \u2014 \u8A8D\u8A3C\u3055\u308C\u305F\u8B58\u5225\u5B50\u304C\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\u306A\u5168\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u4E00\u89A7
|
|
875
784
|
- GET \`/calendars/{calendarId}/events\` \u2014 \u30AB\u30EC\u30F3\u30C0\u30FC\u4E0A\u306E\u30A4\u30D9\u30F3\u30C8\u4E00\u89A7
|
|
876
785
|
- GET \`/calendars/{calendarId}/events/{eventId}\` \u2014 \u5358\u4E00\u30A4\u30D9\u30F3\u30C8\u306E\u53D6\u5F97
|
|
877
786
|
|
|
@@ -883,12 +792,52 @@ export default async function handler(c: Context) {
|
|
|
883
792
|
- \`orderBy=startTime\` \u2014 \u958B\u59CB\u6642\u9593\u9806\u306B\u4E26\u3079\u66FF\u3048\uFF08singleEvents=true\u304C\u5FC5\u8981\uFF09
|
|
884
793
|
- \`q\` \u2014 \u30D5\u30EA\u30FC\u30C6\u30AD\u30B9\u30C8\u691C\u7D22\u8A9E
|
|
885
794
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
795
|
+
### Business Logic
|
|
796
|
+
|
|
797
|
+
\u3053\u306E\u30B3\u30CD\u30AF\u30BF\u306E\u30D3\u30B8\u30CD\u30B9\u30ED\u30B8\u30C3\u30AF\u30BF\u30A4\u30D7\u306F "typescript" \u3067\u3059\u3002\u30CF\u30F3\u30C9\u30E9\u5185\u3067\u306F\u30B3\u30CD\u30AF\u30BFSDK\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u74B0\u5883\u5909\u6570\u304B\u3089\u8A8D\u8A3C\u60C5\u5831\u3092\u8AAD\u307F\u53D6\u3089\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002
|
|
798
|
+
|
|
799
|
+
SDK\u30E1\u30BD\u30C3\u30C9 (\`connection(connectionId)\` \u3067\u4F5C\u6210\u3057\u305F\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8):
|
|
800
|
+
|
|
801
|
+
- \`client.requestWithDelegation(path, { subject, scopes, init? })\` \u2014 DwD \u3067 Workspace \u30E6\u30FC\u30B6\u30FC\u306B\u306A\u308A\u3059\u307E\u3057\u3066 API \u3092\u547C\u3076
|
|
802
|
+
- \`client.request(path, { scopes, init? })\` \u2014 SA \u81EA\u8EAB\u3068\u3057\u3066 API \u3092\u547C\u3076\uFF08SA \u306B\u5171\u6709\u3055\u308C\u305F\u30AB\u30EC\u30F3\u30C0\u30FC\u306E\u307F\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD\uFF09
|
|
803
|
+
|
|
804
|
+
\u4E21\u30E1\u30BD\u30C3\u30C9\u3068\u3082 \`scopes\` \u3092\u53D7\u3051\u53D6\u308A\u307E\u3059\u3002\u30A8\u30F3\u30C9\u30DD\u30A4\u30F3\u30C8\u306B\u5FC5\u8981\u306A\u6700\u5C0F scope \u3092\u6E21\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u6A19\u6E96\u306E \`Response\` \u3092\u8FD4\u3059\u306E\u3067 \`response.json()\` \u3067\u30DC\u30C7\u30A3\u3092\u53D6\u5F97\u3057\u307E\u3059\u3002\u30D1\u30B9\u306E\u66F8\u304D\u65B9\u306F\u30C4\u30FC\u30EB\u3068\u540C\u3058\u3002
|
|
805
|
+
|
|
806
|
+
#### Example
|
|
807
|
+
|
|
808
|
+
\`\`\`ts
|
|
809
|
+
import type { Context } from "hono";
|
|
810
|
+
import { connection } from "@squadbase/vite-server/connectors/google-calendar";
|
|
811
|
+
|
|
812
|
+
const calendar = connection("<connectionId>");
|
|
813
|
+
const READ = ["https://www.googleapis.com/auth/calendar.readonly"];
|
|
814
|
+
|
|
815
|
+
export default async function handler(c: Context) {
|
|
816
|
+
const now = new Date().toISOString();
|
|
817
|
+
const qs = new URLSearchParams({
|
|
818
|
+
timeMin: now,
|
|
819
|
+
maxResults: "10",
|
|
820
|
+
singleEvents: "true",
|
|
821
|
+
orderBy: "startTime",
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Project Knowledge: alice@example.com \u306F delegation \u7D4C\u8DEF\u3067\u30A2\u30AF\u30BB\u30B9\u53EF\u80FD
|
|
825
|
+
const aliceRes = await calendar.requestWithDelegation(
|
|
826
|
+
\`/calendars/alice@example.com/events?\${qs}\`,
|
|
827
|
+
{ subject: "alice@example.com", scopes: READ },
|
|
828
|
+
);
|
|
829
|
+
const alice = await aliceRes.json();
|
|
830
|
+
|
|
831
|
+
// Project Knowledge: team@example.com \u306F SA \u306B\u5171\u6709\u3055\u308C\u3066\u3044\u308B
|
|
832
|
+
const teamRes = await calendar.request(
|
|
833
|
+
\`/calendars/team@example.com/events?\${qs}\`,
|
|
834
|
+
{ scopes: READ },
|
|
835
|
+
);
|
|
836
|
+
const team = await teamRes.json();
|
|
837
|
+
|
|
838
|
+
return c.json({ alice: alice.items, team: team.items });
|
|
839
|
+
}
|
|
840
|
+
\`\`\``
|
|
892
841
|
},
|
|
893
842
|
tools
|
|
894
843
|
});
|