@agentshouse/kit 0.1.0-alpha.26 → 0.1.0-alpha.28

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.
@@ -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 textList(value, declaration) {
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", "selection", "declaration"],
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: name,
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
- selection:
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", "scopes"], declaration);
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}]`, preset),
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
- services.forEach((service, position) => {
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.map((instance, position) =>
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);
@@ -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 googleServiceScopes(service) {
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 gateway.status(instance, context)) return "skipped";
90
- return (await gateway.login(instance, context)) ? "ready" : "needs_attention";
8
+ if (await customAuthorizationStatus(instance, context)) return "skipped";
9
+ return (await authorizeCustom(instance, context)) ? "ready" : "needs_attention";
91
10
  }
package/src/contract.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CLAUDE_PASTED_CODE, CODEX_DEVICE_CODE, GROK_DEVICE_CODE } from "./agent-logins.js";
2
2
 
3
- export const KIT_CONTRACT_VERSION = "1";
3
+ export const KIT_CONTRACT_VERSION = "2";
4
4
  export const KIT_CAPTURE_BATCH_MAX = 100;
5
5
  export const COMMAND_INPUT_MAX_BYTES = 4 * 1024;
6
6
  export const AUTHORIZATION_OUTPUT_MAX_BYTES = 4 * 1024;
@@ -21,10 +21,11 @@ export const COMMAND_ADAPTER_SESSION_TIMEOUT_MS = 30_000;
21
21
  export const WORKER_EXIT_GRACE_MS = 5_000;
22
22
  export const WORKER_EXIT_POLL_MS = 100;
23
23
  export const WORKER_SUPERVISION_POLL_MS = 2_000;
24
- export const WORKER_COMMAND_POLL_MS = 2_000;
25
- export const WORKER_HEARTBEAT_MS = 30_000;
26
24
  export const WORKER_COMMAND_BATCH_MAX = 10;
27
25
  export const WORKER_RUN_TOKEN_MARGIN_MS = 60_000;
26
+ export const KIT_STREAM_RECONNECT_MS = 1_000;
27
+ export const KIT_STREAM_RECONNECT_MAX_MS = 30_000;
28
+ export const KIT_STREAM_REJOIN_MS = 2_000;
28
29
  export const TELEGRAM_IMAGE_UPLOAD_CEILING_BYTES = 10 * 1024 * 1024;
29
30
  export const CONVERSATION_FILE_TRANSFER_TIMEOUT_MS = 30_000;
30
31
  export const WORKER_INHERITED_ENVIRONMENT = ["PATH", "HOME"];
@@ -259,6 +260,10 @@ export const KIT_LOGIN_REPORT_PATH = "/kit/logins/report";
259
260
  export const GUIDED_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
260
261
  export const GUIDED_LOGIN_OUTPUT_MAX_BYTES = 64 * 1024;
261
262
 
263
+ export const KIT_SOURCES_DESIRED_PATH = "/kit/sources/desired";
264
+ export const KIT_SOURCES_REPORT_PATH = "/kit/sources/report";
265
+ export const SOURCE_CAPTURE_INTERVAL_MS = 15 * 60 * 1000;
266
+
262
267
  export const EXECUTION_AUTHORITIES = ["allow", "refuse"];
263
268
 
264
269
  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
- return {
188
- state: stall.disposition === "retry" ? "retrying" : "failed",
189
- category: stall.category ?? FAILURE_CATEGORIES[stall.code] ?? "internal",
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
  }
@@ -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, calendars) {
9
- if (value === null) return { calendar: 0, pageToken: null, syncTokens: {} };
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.syncTokens !== "object" ||
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, calendar) {
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/${calendar}`,
67
+ container: `google/calendar/${CALENDAR}`,
75
68
  };
76
69
  }
77
70
 
78
- function pageUrl(calendar, cursor, limit, backfillFloor) {
79
- const url = new URL(
80
- `/calendar/v3/calendars/${encodeURIComponent(calendar)}/events`,
81
- CALENDAR_ORIGIN,
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
- const syncToken = cursor.syncTokens[calendar];
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
- const calendars = options.selection.calendars;
97
- let cursor = cursorOf(options.cursor, calendars);
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
- ...common,
111
+ ...shared,
111
112
  goneCode: "google_calendar_cursor_expired",
112
- url: pageUrl(calendar, cursor, options.limit, options.backfillFloor),
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
- const syncTokens = { ...cursor.syncTokens };
121
- delete syncTokens[calendar];
122
- cursor = { calendar: cursor.calendar, pageToken: null, syncTokens };
121
+ cursor = { pageToken: null, syncToken: null };
123
122
  answer = await googleJson(
124
- { ...common, url: pageUrl(calendar, cursor, options.limit, options.backfillFloor) },
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 = { calendar: cursor.calendar, pageToken: answer.nextPageToken, syncTokens: cursor.syncTokens };
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
- const syncTokens = { ...cursor.syncTokens, [calendar]: answer.nextSyncToken };
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, calendar));
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 || cursor.calendar + 1 < calendars.length,
169
+ more: answer.nextPageToken !== undefined,
176
170
  };
177
171
  }