@agentshouse/kit 0.1.0-alpha.26 → 0.1.0-alpha.27
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 +104 -136
- package/bin/kit.js +48 -46
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/configuration.js +9 -209
- package/src/connector-login.js +3 -84
- package/src/contract.js +4 -0
- package/src/delivery.js +4 -7
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +346 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/house.js +43 -5
- package/src/kit-work-stream.js +1 -1
- package/src/setup.js +8 -14
- package/src/source.js +51 -26
- package/src/state.js +19 -1
- package/src/tasks.js +72 -0
- package/src/worker.js +26 -60
- package/src/working-copy.js +1 -15
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
package/src/configuration.js
CHANGED
|
@@ -7,18 +7,10 @@ import {
|
|
|
7
7
|
MIGRATOR_TIMEOUT_MS,
|
|
8
8
|
} from "./contract.js";
|
|
9
9
|
import { declarationReader } from "./declaration.js";
|
|
10
|
-
import { GOOGLE_DRIVE_ID } from "./google/id.js";
|
|
11
10
|
import { KitRefusal } from "./refusal.js";
|
|
12
11
|
|
|
13
|
-
const PRESETS = new Set(["custom", "google"]);
|
|
14
12
|
const AGENT_CLIENTS = new Set(["claude-code"]);
|
|
15
|
-
const GOOGLE_CURATED_SERVICES = new Set(["gmail", "calendar", "drive"]);
|
|
16
13
|
const ENVIRONMENT_VALUE_MAX = 4096;
|
|
17
|
-
const GOOGLE_API_HOST = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.googleapis\.com$/;
|
|
18
|
-
const GOOGLE_API_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$/;
|
|
19
|
-
const GOOGLE_API_SCOPE = /^https:\/\/www\.googleapis\.com\/auth\/[A-Za-z0-9._-]+$/;
|
|
20
|
-
const EXTRACTION_PATH = /^(?:[A-Za-z_][A-Za-z0-9_-]*|\d+)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$/;
|
|
21
|
-
const QUERY_NAME = /^[A-Za-z][A-Za-z0-9_.-]{0,127}$/;
|
|
22
14
|
|
|
23
15
|
const { refuse, plain, closed, text, positiveCount, filled } = declarationReader(
|
|
24
16
|
"kit_configuration_invalid",
|
|
@@ -116,225 +108,33 @@ function backfillOf(value, declaration) {
|
|
|
116
108
|
return { kind: named[0], bound: positiveCount(backfill[named[0]], declaration) };
|
|
117
109
|
}
|
|
118
110
|
|
|
119
|
-
function
|
|
120
|
-
return filled(value, declaration).map((entry, position) =>
|
|
121
|
-
text(entry, `${declaration}[${position}]`, 512),
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function textArray(value, declaration) {
|
|
126
|
-
if (!Array.isArray(value)) refuse(declaration);
|
|
127
|
-
return value.map((entry, position) => text(entry, `${declaration}[${position}]`, 512));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function googleSelectionOf(value, declaration, service) {
|
|
131
|
-
if (service === "gmail") {
|
|
132
|
-
const selection = closed(value, ["labels"], declaration);
|
|
133
|
-
return { labels: textList(selection.labels, `${declaration}.labels`) };
|
|
134
|
-
}
|
|
135
|
-
if (service === "calendar") {
|
|
136
|
-
const selection = closed(value, ["calendars"], declaration);
|
|
137
|
-
return { calendars: textList(selection.calendars, `${declaration}.calendars`) };
|
|
138
|
-
}
|
|
139
|
-
if (service === "drive") {
|
|
140
|
-
const selection = closed(value, ["files", "folders"], declaration);
|
|
141
|
-
const files =
|
|
142
|
-
selection.files === undefined ? [] : textArray(selection.files, `${declaration}.files`);
|
|
143
|
-
const folders =
|
|
144
|
-
selection.folders === undefined ? [] : textArray(selection.folders, `${declaration}.folders`);
|
|
145
|
-
if (files.length + folders.length === 0) refuse(declaration);
|
|
146
|
-
files.forEach((id, position) => {
|
|
147
|
-
if (!GOOGLE_DRIVE_ID.test(id)) refuse(`${declaration}.files[${position}]`);
|
|
148
|
-
});
|
|
149
|
-
folders.forEach((id, position) => {
|
|
150
|
-
if (!GOOGLE_DRIVE_ID.test(id)) refuse(`${declaration}.folders[${position}]`);
|
|
151
|
-
});
|
|
152
|
-
return { files, folders };
|
|
153
|
-
}
|
|
154
|
-
refuse(declaration);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function extractionPath(value, declaration) {
|
|
158
|
-
const path = text(value, declaration, 512);
|
|
159
|
-
if (!EXTRACTION_PATH.test(path)) refuse(declaration);
|
|
160
|
-
return path;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function queryOf(value, declaration) {
|
|
164
|
-
const query = plain(value, declaration);
|
|
165
|
-
const parsed = {};
|
|
166
|
-
for (const [name, entry] of Object.entries(query)) {
|
|
167
|
-
if (!QUERY_NAME.test(name) || typeof entry !== "string" || entry.length > 2048) {
|
|
168
|
-
refuse(`${declaration}.${name}`);
|
|
169
|
-
}
|
|
170
|
-
parsed[name] = entry;
|
|
171
|
-
}
|
|
172
|
-
return parsed;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function declaredServiceOf(value, declaration) {
|
|
176
|
-
const declared = closed(
|
|
177
|
-
value,
|
|
178
|
-
["host", "path", "query", "page_size", "page_token", "items", "fields", "scopes"],
|
|
179
|
-
declaration,
|
|
180
|
-
);
|
|
181
|
-
const host = text(declared.host, `${declaration}.host`, 253);
|
|
182
|
-
if (!GOOGLE_API_HOST.test(host)) refuse(`${declaration}.host`);
|
|
183
|
-
const path = text(declared.path, `${declaration}.path`, 2048);
|
|
184
|
-
if (!GOOGLE_API_PATH.test(path) || path.includes("..") || path.includes("//")) {
|
|
185
|
-
refuse(`${declaration}.path`);
|
|
186
|
-
}
|
|
187
|
-
const tokens = closed(
|
|
188
|
-
declared.page_token,
|
|
189
|
-
["request", "response", "checkpoint_request", "checkpoint_response"],
|
|
190
|
-
`${declaration}.page_token`,
|
|
191
|
-
);
|
|
192
|
-
const pageToken = {
|
|
193
|
-
request: text(tokens.request, `${declaration}.page_token.request`, 128),
|
|
194
|
-
response: extractionPath(tokens.response, `${declaration}.page_token.response`),
|
|
195
|
-
checkpointRequest: text(
|
|
196
|
-
tokens.checkpoint_request,
|
|
197
|
-
`${declaration}.page_token.checkpoint_request`,
|
|
198
|
-
128,
|
|
199
|
-
),
|
|
200
|
-
checkpointResponse: extractionPath(
|
|
201
|
-
tokens.checkpoint_response,
|
|
202
|
-
`${declaration}.page_token.checkpoint_response`,
|
|
203
|
-
),
|
|
204
|
-
};
|
|
205
|
-
if (
|
|
206
|
-
!QUERY_NAME.test(pageToken.request) ||
|
|
207
|
-
!QUERY_NAME.test(pageToken.checkpointRequest) ||
|
|
208
|
-
pageToken.request === pageToken.checkpointRequest
|
|
209
|
-
) {
|
|
210
|
-
refuse(`${declaration}.page_token`);
|
|
211
|
-
}
|
|
212
|
-
const fields = closed(
|
|
213
|
-
declared.fields,
|
|
214
|
-
[
|
|
215
|
-
"native_id",
|
|
216
|
-
"native_version",
|
|
217
|
-
"provider_modified_at",
|
|
218
|
-
"observed_at",
|
|
219
|
-
"body",
|
|
220
|
-
"container",
|
|
221
|
-
"author",
|
|
222
|
-
"url",
|
|
223
|
-
],
|
|
224
|
-
`${declaration}.fields`,
|
|
225
|
-
);
|
|
226
|
-
const pageSize = text(declared.page_size, `${declaration}.page_size`, 128);
|
|
227
|
-
if (!QUERY_NAME.test(pageSize)) refuse(`${declaration}.page_size`);
|
|
228
|
-
const scopes = textList(declared.scopes, `${declaration}.scopes`).map((scope, position) => {
|
|
229
|
-
if (!GOOGLE_API_SCOPE.test(scope)) refuse(`${declaration}.scopes[${position}]`);
|
|
230
|
-
return scope;
|
|
231
|
-
});
|
|
232
|
-
return {
|
|
233
|
-
host,
|
|
234
|
-
path,
|
|
235
|
-
query: queryOf(declared.query, `${declaration}.query`),
|
|
236
|
-
pageSize,
|
|
237
|
-
scopes: [...new Set(scopes)],
|
|
238
|
-
pageToken,
|
|
239
|
-
items: extractionPath(declared.items, `${declaration}.items`),
|
|
240
|
-
fields: {
|
|
241
|
-
nativeId: extractionPath(fields.native_id, `${declaration}.fields.native_id`),
|
|
242
|
-
nativeVersion:
|
|
243
|
-
fields.native_version === undefined
|
|
244
|
-
? null
|
|
245
|
-
: extractionPath(fields.native_version, `${declaration}.fields.native_version`),
|
|
246
|
-
providerModifiedAt:
|
|
247
|
-
fields.provider_modified_at === undefined
|
|
248
|
-
? null
|
|
249
|
-
: extractionPath(
|
|
250
|
-
fields.provider_modified_at,
|
|
251
|
-
`${declaration}.fields.provider_modified_at`,
|
|
252
|
-
),
|
|
253
|
-
observedAt: extractionPath(fields.observed_at, `${declaration}.fields.observed_at`),
|
|
254
|
-
body: textList(fields.body, `${declaration}.fields.body`).map((path, position) =>
|
|
255
|
-
extractionPath(path, `${declaration}.fields.body[${position}]`),
|
|
256
|
-
),
|
|
257
|
-
container: extractionPath(fields.container, `${declaration}.fields.container`),
|
|
258
|
-
author:
|
|
259
|
-
fields.author === undefined
|
|
260
|
-
? null
|
|
261
|
-
: extractionPath(fields.author, `${declaration}.fields.author`),
|
|
262
|
-
url:
|
|
263
|
-
fields.url === undefined ? null : extractionPath(fields.url, `${declaration}.fields.url`),
|
|
264
|
-
},
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function serviceOf(value, declaration, preset) {
|
|
111
|
+
function serviceOf(value, declaration) {
|
|
269
112
|
const service = closed(
|
|
270
113
|
value,
|
|
271
|
-
["service", "source_ref", "rooms", "backfill", "connector"
|
|
114
|
+
["service", "source_ref", "rooms", "backfill", "connector"],
|
|
272
115
|
declaration,
|
|
273
116
|
);
|
|
274
|
-
const name = declaredId(service.service, `${declaration}.service`);
|
|
275
|
-
if (preset === "custom" && service.connector === undefined) refuse(`${declaration}.connector`);
|
|
276
|
-
if (preset === "custom" && service.selection !== undefined) refuse(`${declaration}.selection`);
|
|
277
|
-
if (preset === "custom" && service.declaration !== undefined) refuse(`${declaration}.declaration`);
|
|
278
|
-
if (preset === "google" && service.connector !== undefined) refuse(`${declaration}.connector`);
|
|
279
|
-
if (preset === "google" && GOOGLE_CURATED_SERVICES.has(name) && service.declaration !== undefined) {
|
|
280
|
-
refuse(`${declaration}.declaration`);
|
|
281
|
-
}
|
|
282
|
-
if (preset === "google" && !GOOGLE_CURATED_SERVICES.has(name)) {
|
|
283
|
-
if (service.selection !== undefined) refuse(`${declaration}.selection`);
|
|
284
|
-
if (service.declaration === undefined) refuse(`${declaration}.declaration`);
|
|
285
|
-
}
|
|
286
117
|
return {
|
|
287
|
-
service:
|
|
118
|
+
service: declaredId(service.service, `${declaration}.service`),
|
|
288
119
|
sourceRef: text(service.source_ref, `${declaration}.source_ref`, 512),
|
|
289
120
|
rooms: filled(service.rooms, `${declaration}.rooms`).map((room, position) =>
|
|
290
121
|
text(room, `${declaration}.rooms[${position}]`, 512),
|
|
291
122
|
),
|
|
292
123
|
backfill: backfillOf(service.backfill, `${declaration}.backfill`),
|
|
293
|
-
|
|
294
|
-
preset === "google" && GOOGLE_CURATED_SERVICES.has(name)
|
|
295
|
-
? googleSelectionOf(service.selection, `${declaration}.selection`, name)
|
|
296
|
-
: null,
|
|
297
|
-
connector: preset === "custom" ? connectorOf(service.connector, `${declaration}.connector`) : null,
|
|
298
|
-
declaration:
|
|
299
|
-
preset === "google" && !GOOGLE_CURATED_SERVICES.has(name)
|
|
300
|
-
? declaredServiceOf(service.declaration, `${declaration}.declaration`)
|
|
301
|
-
: null,
|
|
124
|
+
connector: connectorOf(service.connector, `${declaration}.connector`),
|
|
302
125
|
};
|
|
303
126
|
}
|
|
304
127
|
|
|
305
128
|
function instanceOf(value, declaration) {
|
|
306
|
-
const instance = closed(value, ["id", "preset", "services"
|
|
307
|
-
const preset = declaredId(instance.preset, `${declaration}.preset`);
|
|
308
|
-
if (!PRESETS.has(preset)) refuse(`${declaration}.preset`);
|
|
309
|
-
if (preset !== "google" && instance.scopes !== undefined) refuse(`${declaration}.scopes`);
|
|
310
|
-
const scopes =
|
|
311
|
-
instance.scopes === undefined
|
|
312
|
-
? []
|
|
313
|
-
: textList(instance.scopes, `${declaration}.scopes`).map((scope, position) => {
|
|
314
|
-
if (!GOOGLE_API_SCOPE.test(scope)) refuse(`${declaration}.scopes[${position}]`);
|
|
315
|
-
return scope;
|
|
316
|
-
});
|
|
129
|
+
const instance = closed(value, ["id", "preset", "services"], declaration);
|
|
317
130
|
const services = filled(instance.services, `${declaration}.services`).map((service, position) =>
|
|
318
|
-
serviceOf(service, `${declaration}.services[${position}]
|
|
131
|
+
serviceOf(service, `${declaration}.services[${position}]`),
|
|
319
132
|
);
|
|
320
133
|
services.forEach((service, position) => {
|
|
321
134
|
const first = services.findIndex((candidate) => candidate.service === service.service);
|
|
322
135
|
if (first !== position) refuse(`${declaration}.services[${position}].service`);
|
|
323
136
|
});
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
service.declaration !== null &&
|
|
327
|
-
service.declaration.scopes.some((scope) => !scopes.includes(scope))
|
|
328
|
-
) {
|
|
329
|
-
refuse(`${declaration}.services[${position}].declaration.scopes`);
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
return {
|
|
333
|
-
id: declaredId(instance.id, `${declaration}.id`),
|
|
334
|
-
preset,
|
|
335
|
-
services,
|
|
336
|
-
...(preset === "google" ? { scopes: [...new Set(scopes)] } : {}),
|
|
337
|
-
};
|
|
137
|
+
return { id: declaredId(instance.id, `${declaration}.id`), preset: instance.preset, services };
|
|
338
138
|
}
|
|
339
139
|
|
|
340
140
|
function absolutePath(value, declaration) {
|
|
@@ -402,8 +202,8 @@ export function parseConfiguration(document) {
|
|
|
402
202
|
if (typeof skills !== "boolean") refuse("skills");
|
|
403
203
|
if (!Array.isArray(declared.instances)) refuse("instances");
|
|
404
204
|
if (!Array.isArray(declared.migrators)) refuse("migrators");
|
|
405
|
-
const instances = declared.instances.
|
|
406
|
-
instanceOf(instance, `instances[${position}]`),
|
|
205
|
+
const instances = declared.instances.flatMap((instance, position) =>
|
|
206
|
+
instance?.preset === "custom" ? [instanceOf(instance, `instances[${position}]`)] : [],
|
|
407
207
|
);
|
|
408
208
|
instances.forEach((instance, position) => {
|
|
409
209
|
const first = instances.findIndex((candidate) => candidate.id === instance.id);
|
package/src/connector-login.js
CHANGED
|
@@ -1,91 +1,10 @@
|
|
|
1
1
|
import { readConfiguration, sourceInstance } from "./configuration.js";
|
|
2
2
|
import { authorizeCustom, customAuthorizationStatus } from "./custom/authorization.js";
|
|
3
|
-
import { googleAuthorization, authorizeGoogle, GOOGLE_SCOPES } from "./google/oauth.js";
|
|
4
|
-
import { KitRefusal } from "./refusal.js";
|
|
5
3
|
|
|
6
|
-
function
|
|
7
|
-
const curated = GOOGLE_SCOPES[service.service];
|
|
8
|
-
return curated === undefined ? service.declaration.scopes : [curated];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function googleServiceAuthorized(authorization, scopes) {
|
|
12
|
-
return authorization.grants.some((grant) =>
|
|
13
|
-
scopes.every((scope) => grant.scopes.includes(scope)),
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function googleConsent(service, context) {
|
|
18
|
-
return async ({ url }) => {
|
|
19
|
-
const collected = await context.present(
|
|
20
|
-
{
|
|
21
|
-
steps: [
|
|
22
|
-
{ kind: "visit", label: `Sign in to Google and allow access for ${service.service}`, url },
|
|
23
|
-
{
|
|
24
|
-
kind: "collect",
|
|
25
|
-
label: "The address your browser ended on",
|
|
26
|
-
name: "address",
|
|
27
|
-
description:
|
|
28
|
-
"After you allow access, Google sends your browser to a 127.0.0.1 address that does not open. Copy that whole address from the address bar and paste it here.",
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
context.signal,
|
|
33
|
-
);
|
|
34
|
-
return collected.address;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const CONNECTOR_LOGINS = {
|
|
39
|
-
custom: {
|
|
40
|
-
status: customAuthorizationStatus,
|
|
41
|
-
login: authorizeCustom,
|
|
42
|
-
},
|
|
43
|
-
google: {
|
|
44
|
-
status: async (instance, context) => {
|
|
45
|
-
const authorization = await googleAuthorization(context.directory, instance.id);
|
|
46
|
-
if (authorization === null) return false;
|
|
47
|
-
return instance.services.every((service) =>
|
|
48
|
-
googleServiceAuthorized(authorization, googleServiceScopes(service)),
|
|
49
|
-
);
|
|
50
|
-
},
|
|
51
|
-
guided: async (instance, context) => {
|
|
52
|
-
for (const service of instance.services) {
|
|
53
|
-
try {
|
|
54
|
-
await authorizeGoogle(
|
|
55
|
-
{
|
|
56
|
-
directory: context.directory,
|
|
57
|
-
instance: instance.id,
|
|
58
|
-
client: context.dependencies.googleOAuthClient(),
|
|
59
|
-
scopes: googleServiceScopes(service),
|
|
60
|
-
approve: googleConsent(service, context),
|
|
61
|
-
},
|
|
62
|
-
context.dependencies.googleDependencies ?? {},
|
|
63
|
-
);
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (context.signal.aborted) return "cancelled";
|
|
66
|
-
if (error instanceof KitRefusal && error.code === "google_authorization_declined") {
|
|
67
|
-
return "cancelled";
|
|
68
|
-
}
|
|
69
|
-
return "failed";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return "ready";
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export function connectorLogin(instance, dependencies = {}) {
|
|
78
|
-
return { ...CONNECTOR_LOGINS, ...(dependencies.connectorLogins ?? {}) }[instance.preset];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export async function signInConnector(options, dependencies = {}) {
|
|
4
|
+
export async function signInConnector(options) {
|
|
82
5
|
const configuration = await readConfiguration(options.configuration);
|
|
83
6
|
const instance = sourceInstance(configuration, options.instance);
|
|
84
|
-
const gateway = connectorLogin(instance, dependencies);
|
|
85
|
-
if (gateway.login === undefined) {
|
|
86
|
-
throw new KitRefusal("kit_connector_login_guided", `${instance.id} signs in through House setup`);
|
|
87
|
-
}
|
|
88
7
|
const context = { directory: options.directory, signal: options.signal };
|
|
89
|
-
if (await
|
|
90
|
-
return (await
|
|
8
|
+
if (await customAuthorizationStatus(instance, context)) return "skipped";
|
|
9
|
+
return (await authorizeCustom(instance, context)) ? "ready" : "needs_attention";
|
|
91
10
|
}
|
package/src/contract.js
CHANGED
|
@@ -259,6 +259,10 @@ export const KIT_LOGIN_REPORT_PATH = "/kit/logins/report";
|
|
|
259
259
|
export const GUIDED_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
|
|
260
260
|
export const GUIDED_LOGIN_OUTPUT_MAX_BYTES = 64 * 1024;
|
|
261
261
|
|
|
262
|
+
export const KIT_SOURCES_DESIRED_PATH = "/kit/sources/desired";
|
|
263
|
+
export const KIT_SOURCES_REPORT_PATH = "/kit/sources/report";
|
|
264
|
+
export const SOURCE_CAPTURE_INTERVAL_MS = 15 * 60 * 1000;
|
|
265
|
+
|
|
262
266
|
export const EXECUTION_AUTHORITIES = ["allow", "refuse"];
|
|
263
267
|
|
|
264
268
|
export const PERMISSION_OPTION_KINDS = Object.freeze({
|
package/src/delivery.js
CHANGED
|
@@ -54,10 +54,8 @@ const FAILURE_CATEGORIES = {
|
|
|
54
54
|
google_provider_unavailable: "provider",
|
|
55
55
|
google_provider_response_invalid: "provider",
|
|
56
56
|
google_checkpoint_invalid: "provider",
|
|
57
|
-
google_service_unsupported: "config",
|
|
58
|
-
google_oauth_client_unconfigured: "config",
|
|
59
|
-
google_drive_selection_exceeded: "config",
|
|
60
57
|
google_drive_item_missing: "provider",
|
|
58
|
+
api_disabled: "config",
|
|
61
59
|
};
|
|
62
60
|
|
|
63
61
|
export function fileDescriptor(file) {
|
|
@@ -184,8 +182,7 @@ export function stallOf(code, houseRetry = null) {
|
|
|
184
182
|
|
|
185
183
|
export function healthOfRun(stall) {
|
|
186
184
|
if (stall === null) return { state: "ok", category: null };
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
};
|
|
185
|
+
const category = stall.category ?? FAILURE_CATEGORIES[stall.code] ?? "internal";
|
|
186
|
+
if (stall.disposition === "retry") return { state: "retrying", category };
|
|
187
|
+
return { state: "failed", category, cause: stall.code };
|
|
191
188
|
}
|
package/src/google/calendar.js
CHANGED
|
@@ -4,9 +4,10 @@ import { googleJson } from "./gateway.js";
|
|
|
4
4
|
import { GOOGLE_SCOPES } from "./oauth.js";
|
|
5
5
|
|
|
6
6
|
const CALENDAR_ORIGIN = "https://www.googleapis.com";
|
|
7
|
+
const CALENDAR = "primary";
|
|
7
8
|
|
|
8
|
-
function cursorOf(value
|
|
9
|
-
if (value === null) return {
|
|
9
|
+
function cursorOf(value) {
|
|
10
|
+
if (value === null) return { pageToken: null, syncToken: null };
|
|
10
11
|
let cursor;
|
|
11
12
|
try {
|
|
12
13
|
cursor = JSON.parse(value);
|
|
@@ -16,16 +17,8 @@ function cursorOf(value, calendars) {
|
|
|
16
17
|
if (
|
|
17
18
|
typeof cursor !== "object" ||
|
|
18
19
|
cursor === null ||
|
|
19
|
-
!Number.isInteger(cursor.calendar) ||
|
|
20
|
-
cursor.calendar < 0 ||
|
|
21
|
-
cursor.calendar >= calendars.length ||
|
|
22
20
|
(cursor.pageToken !== null && typeof cursor.pageToken !== "string") ||
|
|
23
|
-
typeof cursor.
|
|
24
|
-
cursor.syncTokens === null ||
|
|
25
|
-
Array.isArray(cursor.syncTokens) ||
|
|
26
|
-
Object.entries(cursor.syncTokens).some(
|
|
27
|
-
([calendar, token]) => !calendars.includes(calendar) || typeof token !== "string",
|
|
28
|
-
)
|
|
21
|
+
(cursor.syncToken !== null && typeof cursor.syncToken !== "string")
|
|
29
22
|
) {
|
|
30
23
|
throw new KitRefusal("google_checkpoint_invalid", "calendar");
|
|
31
24
|
}
|
|
@@ -45,7 +38,7 @@ function authorOf(event) {
|
|
|
45
38
|
: event.organizer.email;
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
function normalizedEvent(event
|
|
41
|
+
function normalizedEvent(event) {
|
|
49
42
|
if (
|
|
50
43
|
typeof event?.id !== "string" ||
|
|
51
44
|
typeof event.etag !== "string" ||
|
|
@@ -71,45 +64,53 @@ function normalizedEvent(event, calendar) {
|
|
|
71
64
|
author: authorOf(event),
|
|
72
65
|
url: event.htmlLink ?? null,
|
|
73
66
|
attachments: [],
|
|
74
|
-
container: `google/calendar/${
|
|
67
|
+
container: `google/calendar/${CALENDAR}`,
|
|
75
68
|
};
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
function common(options) {
|
|
72
|
+
return {
|
|
73
|
+
directory: options.directory,
|
|
74
|
+
account: options.account,
|
|
75
|
+
requiredScopes: [GOOGLE_SCOPES.calendar],
|
|
76
|
+
signal: options.signal,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pageUrl(cursor, limit, backfillFloor) {
|
|
81
|
+
const url = new URL(`/calendar/v3/calendars/${CALENDAR}/events`, CALENDAR_ORIGIN);
|
|
83
82
|
url.searchParams.set("maxResults", `${limit}`);
|
|
84
83
|
url.searchParams.set("singleEvents", "true");
|
|
85
84
|
url.searchParams.set("showDeleted", "true");
|
|
86
85
|
if (cursor.pageToken !== null) url.searchParams.set("pageToken", cursor.pageToken);
|
|
87
|
-
|
|
88
|
-
if (syncToken !== undefined) url.searchParams.set("syncToken", syncToken);
|
|
86
|
+
if (cursor.syncToken !== null) url.searchParams.set("syncToken", cursor.syncToken);
|
|
89
87
|
else if (backfillFloor !== null && backfillFloor !== undefined) {
|
|
90
88
|
url.searchParams.set("timeMin", backfillFloor);
|
|
91
89
|
}
|
|
92
90
|
return url.toString();
|
|
93
91
|
}
|
|
94
92
|
|
|
93
|
+
export async function calendarEmail(options, dependencies = {}) {
|
|
94
|
+
const calendar = await googleJson(
|
|
95
|
+
{ ...common(options), url: `${CALENDAR_ORIGIN}/calendar/v3/calendars/${CALENDAR}` },
|
|
96
|
+
dependencies,
|
|
97
|
+
);
|
|
98
|
+
if (typeof calendar.id !== "string") {
|
|
99
|
+
throw new KitRefusal("google_provider_response_invalid", "calendar");
|
|
100
|
+
}
|
|
101
|
+
return calendar.id;
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
export async function calendarPage(options, dependencies = {}) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const calendar = calendars[cursor.calendar];
|
|
99
|
-
const common = {
|
|
100
|
-
directory: options.directory,
|
|
101
|
-
instance: options.instance,
|
|
102
|
-
client: options.client,
|
|
103
|
-
requiredScopes: [GOOGLE_SCOPES.calendar],
|
|
104
|
-
signal: options.signal,
|
|
105
|
-
};
|
|
105
|
+
let cursor = cursorOf(options.cursor);
|
|
106
|
+
const shared = common(options);
|
|
106
107
|
let answer;
|
|
107
108
|
try {
|
|
108
109
|
answer = await googleJson(
|
|
109
110
|
{
|
|
110
|
-
...
|
|
111
|
+
...shared,
|
|
111
112
|
goneCode: "google_calendar_cursor_expired",
|
|
112
|
-
url: pageUrl(
|
|
113
|
+
url: pageUrl(cursor, options.limit, options.backfillFloor),
|
|
113
114
|
},
|
|
114
115
|
dependencies,
|
|
115
116
|
);
|
|
@@ -117,11 +118,9 @@ export async function calendarPage(options, dependencies = {}) {
|
|
|
117
118
|
if (!(error instanceof KitRefusal) || error.code !== "google_calendar_cursor_expired") {
|
|
118
119
|
throw error;
|
|
119
120
|
}
|
|
120
|
-
|
|
121
|
-
delete syncTokens[calendar];
|
|
122
|
-
cursor = { calendar: cursor.calendar, pageToken: null, syncTokens };
|
|
121
|
+
cursor = { pageToken: null, syncToken: null };
|
|
123
122
|
answer = await googleJson(
|
|
124
|
-
{ ...
|
|
123
|
+
{ ...shared, url: pageUrl(cursor, options.limit, options.backfillFloor) },
|
|
125
124
|
dependencies,
|
|
126
125
|
);
|
|
127
126
|
}
|
|
@@ -135,23 +134,18 @@ export async function calendarPage(options, dependencies = {}) {
|
|
|
135
134
|
}
|
|
136
135
|
let next;
|
|
137
136
|
if (answer.nextPageToken !== undefined) {
|
|
138
|
-
next = {
|
|
137
|
+
next = { pageToken: answer.nextPageToken, syncToken: cursor.syncToken };
|
|
139
138
|
} else {
|
|
140
139
|
if (typeof answer.nextSyncToken !== "string") {
|
|
141
140
|
throw new KitRefusal("google_provider_response_invalid", "calendar sync token");
|
|
142
141
|
}
|
|
143
|
-
|
|
144
|
-
next = {
|
|
145
|
-
calendar: cursor.calendar + 1 === calendars.length ? 0 : cursor.calendar + 1,
|
|
146
|
-
pageToken: null,
|
|
147
|
-
syncTokens,
|
|
148
|
-
};
|
|
142
|
+
next = { pageToken: null, syncToken: answer.nextSyncToken };
|
|
149
143
|
}
|
|
150
144
|
const normalized = [];
|
|
151
145
|
const dropped = [];
|
|
152
146
|
for (const event of items) {
|
|
153
147
|
try {
|
|
154
|
-
normalized.push(normalizedEvent(event
|
|
148
|
+
normalized.push(normalizedEvent(event));
|
|
155
149
|
} catch (error) {
|
|
156
150
|
if (
|
|
157
151
|
error instanceof KitRefusal &&
|
|
@@ -172,6 +166,6 @@ export async function calendarPage(options, dependencies = {}) {
|
|
|
172
166
|
items: normalized,
|
|
173
167
|
dropped,
|
|
174
168
|
cursor: JSON.stringify(next),
|
|
175
|
-
more: answer.nextPageToken !== undefined
|
|
169
|
+
more: answer.nextPageToken !== undefined,
|
|
176
170
|
};
|
|
177
171
|
}
|