slotlock 0.5.1 → 0.5.3

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/event-feed.d.ts CHANGED
@@ -1,8 +1,30 @@
1
1
  import type { Sql, TransactionSql } from 'postgres';
2
2
  type SlotlockSql = Sql | TransactionSql;
3
3
  /** What a change can be, as the feed and a webhook's `event_types` name it. */
4
- export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "coverage.stale", "waitlist.offered", "waitlist.expired", "webhook.disabled"];
4
+ export declare const SLOTLOCK_CHANGE_TYPES: readonly ["hold.created", "hold.updated", "hold.confirmed", "hold.released", "hold.expired", "reservation.created", "reservation.updated", "reservation.cancelled", "reservation.checked_in", "reservation.no_show", "event.created", "event.updated", "event.cancelled", "event.timezone_shifted", "event.timezone_conflict", "coverage.stale", "waitlist.offered", "waitlist.expired", "webhook.disabled", "resource.created", "resource.updated", "resource.archived", "resource.restored", "resource.deleted", "coverage.recorded", "coverage.deleted", "webhook.created", "webhook.enabled", "webhook.deleted", "key.created", "key.rotated", "key.revoked"];
5
5
  export type SlotlockChangeType = (typeof SLOTLOCK_CHANGE_TYPES)[number];
6
+ /**
7
+ * Where a change came from, as the history and the feed record it (withTenant's `origin`): an agent
8
+ * protocol (`mcp`, `a2a`), the REST API's agent routes (`rest`) or its application operations
9
+ * (`application`), the dashboard, a member's dashboard assistant, maintenance, or code calling the
10
+ * store itself (`library`). Never an address or anything else about a person.
11
+ */
12
+ export declare const SLOTLOCK_CHANGE_ORIGINS: readonly ["mcp", "a2a", "rest", "application", "dashboard", "dashboard-assistant", "maintenance", "library"];
13
+ export type SlotlockChangeOrigin = (typeof SLOTLOCK_CHANGE_ORIGINS)[number];
14
+ /** Whether `value` is one of SLOTLOCK_CHANGE_ORIGINS. */
15
+ export declare function isSlotlockChangeOrigin(value: unknown): value is SlotlockChangeOrigin;
16
+ /**
17
+ * The SQL that names a coverage source on the feed (`coverage.recorded`, `.deleted`, `.stale`): a
18
+ * digest of the tenant and the source, stable for both, so the source itself (an iCalendar URL with
19
+ * a token, an address) never reaches a webhook or the history. `tenant` and `source` are SQL
20
+ * expressions; slotlockCoverageSourceId works out the same value.
21
+ */
22
+ export declare function slotlockCoverageSourceIdSql(tenant: string, source: string): string;
23
+ /**
24
+ * How the feed names a tenant's coverage source (`subject` and `data.source_id` of `coverage.*`
25
+ * changes; `source_id` of slotlock_get_coverage): `src_` and 24 hex digits.
26
+ */
27
+ export declare function slotlockCoverageSourceId(tenantRef: string, source: string): string;
6
28
  /** How long the feed keeps a change, and so how far back a cursor may reach. */
7
29
  export declare const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
8
30
  /** Webhook endpoints one tenant may have. */
@@ -20,6 +42,10 @@ export interface SlotlockChange {
20
42
  occurredAt: Date;
21
43
  /** Who made it (withTenant's `actor`: an API key, an agent), or null when unrecorded. */
22
44
  actor: string | null;
45
+ /** Where it came from (withTenant's `origin`), or null when unrecorded (before 0.6, say). */
46
+ origin: SlotlockChangeOrigin | null;
47
+ /** The API key that made it (withTenant's `keyId`), or null when no key did. */
48
+ keyId: string | null;
23
49
  /** Pass as `after` to read what follows this change. */
24
50
  position: string;
25
51
  }
@@ -150,9 +176,10 @@ export interface SlotlockEventFeed {
150
176
  };
151
177
  }): Promise<void>;
152
178
  /**
153
- * History: the changes to one resource (`resourceId`) or one record (`subject`: a booking's
154
- * reference, an event's or hold's id), newest first, `limit` (1-100) at a time, each with who
155
- * made it and, for a change of an existing record, what it was before. Pass the last one's
179
+ * History: the changes to one resource (`resourceId`: its bookings and events, and its own
180
+ * configuration) or one record (`subject`: a booking's reference, an event's, hold's, webhook's or
181
+ * API key's id), newest first, `limit` (1-100) at a time, each with who made it (`actor`, `keyId`),
182
+ * where from (`origin`) and, for a change of an existing record, what it was before. Pass the last one's
156
183
  * `id` as `before` for the page after. Kept as long as the feed keeps changes.
157
184
  */
158
185
  listHistory(params: {
@@ -175,4 +202,10 @@ export declare function isWebhookUrl(value: unknown): value is string;
175
202
  * `retentionDays` (default SLOTLOCK_CHANGE_RETENTION_DAYS), and a cursor reaches back as far.
176
203
  */
177
204
  export declare function slotlockEventFeedMethods(sql: SlotlockSql, retentionDays?: number): SlotlockEventFeed;
205
+ /**
206
+ * A position at or before every change that has not ended yet, so a reader that starts from it and
207
+ * follows `next` misses none still to come (it may read again some that ended just before). Live
208
+ * updates start here (agent-store-backend.ts, createSlotlockStoreChangeSource).
209
+ */
210
+ export declare function slotlockChangeFeedHead(sql: SlotlockSql): Promise<string>;
178
211
  export {};
package/event-feed.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // endpoints it is delivered to. Rows are appended by triggers in the same transaction as the change
3
3
  // (ddl.ts, SLOTLOCK_OUTBOX_DDL). These are the store's methods for reading them and for keeping each
4
4
  // endpoint's delivery state; webhooks.ts signs and sends.
5
- import { randomBytes } from 'node:crypto';
5
+ import { createHash, randomBytes } from 'node:crypto';
6
6
  /** A transaction on a client, or a savepoint inside the caller's (a tenant's, say). */
7
7
  async function inTransaction(sql, callback) {
8
8
  return ('begin' in sql ? sql.begin(callback) : sql.savepoint(callback));
@@ -28,7 +28,58 @@ export const SLOTLOCK_CHANGE_TYPES = Object.freeze([
28
28
  'waitlist.offered',
29
29
  'waitlist.expired',
30
30
  'webhook.disabled',
31
+ // Configuration: what a resource is and how it books, the sources it is read from, where the
32
+ // feed is delivered and the keys that reach the tenant. Ids and states, never a secret or a URL.
33
+ 'resource.created',
34
+ 'resource.updated',
35
+ 'resource.archived',
36
+ 'resource.restored',
37
+ 'resource.deleted',
38
+ 'coverage.recorded',
39
+ 'coverage.deleted',
40
+ 'webhook.created',
41
+ 'webhook.enabled',
42
+ 'webhook.deleted',
43
+ 'key.created',
44
+ 'key.rotated',
45
+ 'key.revoked',
31
46
  ]);
47
+ /**
48
+ * Where a change came from, as the history and the feed record it (withTenant's `origin`): an agent
49
+ * protocol (`mcp`, `a2a`), the REST API's agent routes (`rest`) or its application operations
50
+ * (`application`), the dashboard, a member's dashboard assistant, maintenance, or code calling the
51
+ * store itself (`library`). Never an address or anything else about a person.
52
+ */
53
+ export const SLOTLOCK_CHANGE_ORIGINS = Object.freeze([
54
+ 'mcp',
55
+ 'a2a',
56
+ 'rest',
57
+ 'application',
58
+ 'dashboard',
59
+ 'dashboard-assistant',
60
+ 'maintenance',
61
+ 'library',
62
+ ]);
63
+ /** Whether `value` is one of SLOTLOCK_CHANGE_ORIGINS. */
64
+ export function isSlotlockChangeOrigin(value) {
65
+ return typeof value === 'string' && SLOTLOCK_CHANGE_ORIGINS.includes(value);
66
+ }
67
+ /**
68
+ * The SQL that names a coverage source on the feed (`coverage.recorded`, `.deleted`, `.stale`): a
69
+ * digest of the tenant and the source, stable for both, so the source itself (an iCalendar URL with
70
+ * a token, an address) never reaches a webhook or the history. `tenant` and `source` are SQL
71
+ * expressions; slotlockCoverageSourceId works out the same value.
72
+ */
73
+ export function slotlockCoverageSourceIdSql(tenant, source) {
74
+ return `'src_' || left(encode(sha256(convert_to(${tenant} || E'\\n' || ${source}, 'UTF8')), 'hex'), 24)`;
75
+ }
76
+ /**
77
+ * How the feed names a tenant's coverage source (`subject` and `data.source_id` of `coverage.*`
78
+ * changes; `source_id` of slotlock_get_coverage): `src_` and 24 hex digits.
79
+ */
80
+ export function slotlockCoverageSourceId(tenantRef, source) {
81
+ return `src_${createHash('sha256').update(`${tenantRef}\n${source}`, 'utf8').digest('hex').slice(0, 24)}`;
82
+ }
32
83
  /** How long the feed keeps a change, and so how far back a cursor may reach. */
33
84
  export const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
34
85
  /** Webhook endpoints one tenant may have. */
@@ -58,6 +109,9 @@ function toChange(row) {
58
109
  data: row.data,
59
110
  occurredAt: row.occurred_at,
60
111
  actor: row.actor,
112
+ // One a later release added, read by this one: unknown here, so unrecorded, not a failed page.
113
+ origin: isSlotlockChangeOrigin(row.origin) ? row.origin : null,
114
+ keyId: row.key_id,
61
115
  position: encodePosition({ txid: row.txid, id: row.id, at: row.occurred_at.getTime() }),
62
116
  };
63
117
  }
@@ -127,7 +181,7 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
127
181
  SELECT mark.txid::text AS pruned_txid, mark.id::text AS pruned_id,
128
182
  pg_snapshot_xmin(pg_current_snapshot())::text AS horizon,
129
183
  page.id::text, page.txid::text, page.type, page.resource_id, page.subject,
130
- page.data, page.occurred_at, page.actor
184
+ page.data, page.occurred_at, page.actor, page.origin, page.key_id::text
131
185
  FROM (SELECT 1) here
132
186
  LEFT JOIN slotlock.outbox_pruned mark ON mark.tenant_ref = ${params.tenantRef}
133
187
  LEFT JOIN LATERAL (
@@ -179,7 +233,8 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
179
233
  });
180
234
  }
181
235
  const rows = await sql `
182
- SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor
236
+ SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor, origin,
237
+ key_id::text
183
238
  FROM slotlock.outbox
184
239
  WHERE tenant_ref = ${params.tenantRef}
185
240
  ORDER BY txid DESC, id DESC
@@ -194,7 +249,6 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
194
249
  const types = params.eventTypes === undefined ? null : [...new Set(params.eventTypes)];
195
250
  if (types !== null &&
196
251
  (types.length === 0 ||
197
- types.length > 20 ||
198
252
  !types.every((type) => SLOTLOCK_CHANGE_TYPES.includes(type)))) {
199
253
  return { ok: false, code: 'invalid_event_types' };
200
254
  }
@@ -300,7 +354,8 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
300
354
  const claims = [];
301
355
  for (const endpoint of endpoints) {
302
356
  const rows = await sql `
303
- SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor
357
+ SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor, origin,
358
+ key_id::text
304
359
  FROM slotlock.outbox
305
360
  WHERE tenant_ref = ${params.tenantRef}
306
361
  AND txid < pg_snapshot_xmin(pg_current_snapshot())
@@ -393,7 +448,8 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
393
448
  if (before === undefined)
394
449
  return [];
395
450
  const rows = await sql `
396
- SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor
451
+ SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor, origin,
452
+ key_id::text
397
453
  FROM slotlock.outbox
398
454
  WHERE tenant_ref = ${params.tenantRef}
399
455
  AND (${params.resourceId ?? null}::uuid IS NULL OR resource_id = ${params.resourceId ?? null}::uuid)
@@ -445,3 +501,15 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
445
501
  },
446
502
  };
447
503
  }
504
+ /**
505
+ * A position at or before every change that has not ended yet, so a reader that starts from it and
506
+ * follows `next` misses none still to come (it may read again some that ended just before). Live
507
+ * updates start here (agent-store-backend.ts, createSlotlockStoreChangeSource).
508
+ */
509
+ export async function slotlockChangeFeedHead(sql) {
510
+ const [row] = await sql `
511
+ SELECT pg_snapshot_xmin(pg_current_snapshot())::text AS horizon`;
512
+ if (!row)
513
+ throw new Error('slotlock: change feed head returned no row');
514
+ return encodePosition({ txid: row.horizon, id: '0', at: Date.now() });
515
+ }
package/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /// <reference types="node" preserve="true" />
2
2
  export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
3
- export { type SlotlockBookingRule, type SlotlockBookingRules, bookingRuleBroken, bookingSearch, nextStepStart, parseBookingRules, } from './booking-rules.js';
4
- export { type CreateWebhookEndpointResult, type ListChangesResult, SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, type SlotlockChange, type SlotlockChangeType, type SlotlockEventFeed, type SlotlockWebhookClaim, type SlotlockWebhookEndpoint, } from './event-feed.js';
5
- export { type SlotlockCloudEvent, type SlotlockWebhookPassResult, type SlotlockWebhookResponse, type SlotlockWebhookScheduler, type SlotlockWebhookSender, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
3
+ export { type SlotlockBookingRule, type SlotlockBookingRules, type SlotlockBookingSearch, bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
4
+ export { type CreateWebhookEndpointResult, type ListChangesResult, SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, slotlockChangeFeedHead, type SlotlockChange, type SlotlockChangeOrigin, type SlotlockChangeType, type SlotlockEventFeed, type SlotlockWebhookClaim, type SlotlockWebhookEndpoint, slotlockCoverageSourceId, } from './event-feed.js';
5
+ export { type SlotlockCloudEvent, type SlotlockWebhookPassResult, type SlotlockWebhookResponse, type SlotlockWebhookScheduler, type SlotlockWebhookSender, createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
6
6
  export { type SlotlockMetrics, createSlotlockMetrics } from './metrics.js';
7
7
  export { expandRules } from './rules.js';
8
8
  export { availabilityRuleProblem, parseAvailabilityRules, rulesToWeeklyHours, SLOTLOCK_MAX_AVAILABILITY_RULES, weeklyHoursToRules, } from './weekly-hours.js';
@@ -11,11 +11,12 @@ export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOC
11
11
  export type { EmitVAvailabilityOptions, JSCalendarAlert, JSCalendarEvent, JSCalendarParticipant, JSCalendarRecurrenceRule, ParsedVAvailability, } from './interop.js';
12
12
  export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
13
13
  export type { LocalDateTime, TimeZoneConverter, ZonedCalendarDate, ZonedWallTime, } from './timezone.js';
14
- export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
14
+ export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
15
15
  export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockMcpToolResult, SlotlockTraceContext, } from './agent-server.js';
16
16
  export { createSlotlockNodeServer } from './node-server.js';
17
17
  export type { SlotlockNodeListenOptions, SlotlockNodeServer, SlotlockNodeServerAddress, SlotlockNodeServerCloseResult, SlotlockNodeServerErrorPhase, SlotlockNodeServerFetchTarget, SlotlockNodeServerOptions, } from './node-server.js';
18
- export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
18
+ export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
19
+ export { SLOTLOCK_FEED_BUSY_MS, SLOTLOCK_FEED_IDLE_MS, type SlotlockAgentChange, type SlotlockAgentChangeSource, } from './change-watch.js';
19
20
  export type { SlotlockStoreAgentBackendOptions, SlotlockStoreApplicationBackendOptions, } from './agent-store-backend.js';
20
21
  export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlockApplicationOperation, } from './application-api.js';
21
22
  export type { SlotlockApplicationBackend, SlotlockApplicationInvocationContext, SlotlockApplicationOperation, SlotlockApplicationOperationDefinition, } from './application-api.js';
@@ -23,12 +24,14 @@ export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenan
23
24
  export type { SlotlockMaintenanceResult, SlotlockMaintenanceScheduler, SlotlockMaintenanceSchedulerOptions, } from './maintenance.js';
24
25
  export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
25
26
  export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
26
- export type { SlotlockDashboard, SlotlockDashboardOptions, SlotlockDashboardResources, SlotlockDashboardState, } from './dashboard.js';
27
- export type { CreateSlotlockApiKeyInput, SlotlockApiKey, SlotlockApiKeyAuthenticatorOptions, SlotlockApiKeyIdentity, SlotlockApiKeyScope, SlotlockApiKeyStore, } from './api-keys.js';
27
+ export type { SlotlockDashboard, SlotlockDashboardAssistantOptions, SlotlockDashboardOptions, SlotlockDashboardResources, SlotlockDashboardState, } from './dashboard.js';
28
+ export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
29
+ export type { SlotlockAssistantStore } from './assistant.js';
30
+ export type { CreateSlotlockApiKeyInput, SlotlockApiKey, SlotlockApiKeyAuthenticatorOptions, SlotlockApiKeyChange, SlotlockApiKeyIdentity, SlotlockApiKeyScope, SlotlockApiKeyStore, } from './api-keys.js';
28
31
  export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
29
32
  export type { ICalendarWorkerPool, ICalendarWorkerPoolOptions } from './ical-worker.js';
30
33
  export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emitITipCalendar, expandCalendarEventOccurrences, normalizeCalendarEventContent, normalizeExternalCalendarChange, parseICalendarChanges, parseICalendarFeed, parseTrustedICalendarEvents, recurrenceRuleProblem, storedCalendarEventProblem, } from './sync.js';
31
34
  export type { ExternalCalendarChange, ExternalCalendarChangeInput, ExternalCalendarStatus, ICalendarEvent, ICalendarExpansionWindow, ICalendarFeedRead, ICalendarSeriesRefusal, ITipMethod, } from './sync.js';
32
35
  export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
33
36
  export { assertSlotlockSchemaCurrent, calendarEventRollingHorizon, createSlotlockStore, readSlotlockSchemaVersion, SLOTLOCK_AGENT_OWNER_COMMAND_QUOTA, SLOTLOCK_AGENT_OWNER_EVENT_QUOTA, SLOTLOCK_CALENDAR_HORIZON_DAYS, SLOTLOCK_EVENT_COMMAND_RETENTION_DAYS, SLOTLOCK_INTERNAL_EVENT_OWNER_REF, SLOTLOCK_LOCAL_RECURRENCE_COVERAGE_SOURCE, SLOTLOCK_MATERIALIZATION_WINDOW_DAYS, SLOTLOCK_MAX_EVENT_DURATION_DAYS, SLOTLOCK_TENANT_WRITE_METHODS, type SlotlockHoldTenantOptions, type SlotlockMigrationOptions, type SlotlockSql, type SlotlockStore, type SlotlockStoreOptions, type SlotlockTenantLimits, type SlotlockTenantStore, } from './store.js';
34
- export type { AcquireHoldResult, BookingRetentionResult, CalendarAttendee, CalendarAttendeeRole, CalendarCoverageAssessment, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventOccurrence, CalendarEventRetentionResult, CalendarEventStatus, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CalendarOrganizer, CalendarParticipationStatus, CalendarRecurrence, CalendarRecurrenceException, CalendarReminder, CalendarTransparency, ConfirmHoldResult, CancelCalendarEventResult, CancelExternalReservationResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockAgendaEntry, SlotlockHold, SlotlockCalendarEvent, SlotlockReservation, SlotlockResource, SlotlockTenantErasure, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, TrustedICalendarEvent, WeeklyAvailabilityRule, } from './types.js';
37
+ export type { AcquireHoldResult, BookingRetentionResult, CalendarAttendee, CalendarAttendeeRole, CalendarCoverageAssessment, CalendarCoverageCommand, CalendarCoverageResult, CalendarEventContent, CalendarEventOccurrence, CalendarEventRetentionResult, CalendarEventStatus, CalendarFreeBusy, CalendarHorizonRollCursor, CalendarHorizonRollResult, CalendarOrganizer, CalendarParticipationStatus, CalendarRecurrence, CalendarRecurrenceException, CalendarReminder, CalendarTransparency, ConfirmHoldResult, CancelCalendarEventResult, CancelExternalReservationResult, CreateReservationResult, DeleteResourceResult, ExternalReservationCommand, ExternalReservationResult, Interval, SlotlockAgendaEntry, SlotlockHold, SlotlockCalendarEvent, SlotlockReservation, SlotlockResource, SlotlockTenantErasure, SlotlockBookingRuleDefaults, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, TrustedICalendarEvent, WeeklyAvailabilityRule, } from './types.js';
package/index.js CHANGED
@@ -4,21 +4,23 @@
4
4
  // TypeScript 6 default) still loads @types/node.
5
5
  /// <reference types="node" preserve="true" />
6
6
  export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
7
- export { bookingRuleBroken, bookingSearch, nextStepStart, parseBookingRules, } from './booking-rules.js';
8
- export { SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, } from './event-feed.js';
9
- export { createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
7
+ export { bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
8
+ export { SLOTLOCK_CHANGE_ORIGINS, SLOTLOCK_CHANGE_RETENTION_DAYS, SLOTLOCK_CHANGE_TYPES, SLOTLOCK_MAX_WEBHOOK_ENDPOINTS, SLOTLOCK_WEBHOOK_FAILING_LIMIT_MS, slotlockChangeFeedHead, slotlockCoverageSourceId, } from './event-feed.js';
9
+ export { createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
10
10
  export { createSlotlockMetrics } from './metrics.js';
11
11
  export { expandRules } from './rules.js';
12
12
  export { availabilityRuleProblem, parseAvailabilityRules, rulesToWeeklyHours, SLOTLOCK_MAX_AVAILABILITY_RULES, weeklyHoursToRules, } from './weekly-hours.js';
13
13
  export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS, toJSCalendarEvent, } from './interop.js';
14
14
  export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
15
- export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
15
+ export { createSlotlockAgentServer, clientSupportsSlotlockMcpApp, invokeSlotlockAgentOperation, isSlotlockAgentOperation, slotlockAgentTools, slotlockApplicationOperations, slotlockOpenApi, slotlockCalendarResourceUri, slotlockMcpToolResult, parseSlotlockA2ATimestamp, resolveSlotlockAgentOperation, slotlockAgentOperationScope, SLOTLOCK_A2A_PROTOCOL_VERSION, SLOTLOCK_A2A_TASK_PAGE_SIZE, SLOTLOCK_A2A_WATCH_SKILL, SLOTLOCK_A2A_TASK_STATES, SLOTLOCK_AGENT_OPERATION_LEGACY_NAMES, SLOTLOCK_AGENT_SERVER_VERSION, SLOTLOCK_MCP_LEGACY_PROTOCOL_VERSION, SLOTLOCK_MCP_PROTOCOL_VERSION, SLOTLOCK_MCP_RATE_LIMITED_ERROR_CODE, SLOTLOCK_MCP_RESOURCE_URI_TEMPLATE, SLOTLOCK_MCP_SUPPORTED_PROTOCOL_VERSIONS, SLOTLOCK_MCP_APP_HTML, SLOTLOCK_MCP_APP_MIME_TYPE, SLOTLOCK_MCP_APP_RESOURCE, SLOTLOCK_MCP_APP_RESOURCE_URI, SLOTLOCK_MCP_APPS_EXTENSION, SlotlockAgentOperationError, } from './agent-server.js';
16
16
  export { createSlotlockNodeServer } from './node-server.js';
17
- export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
17
+ export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
18
+ export { SLOTLOCK_FEED_BUSY_MS, SLOTLOCK_FEED_IDLE_MS, } from './change-watch.js';
18
19
  export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlockApplicationOperation, } from './application-api.js';
19
20
  export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
20
21
  export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, slotlockApiKeyDigest, SLOTLOCK_API_KEY_ACTIVE_LIMIT, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_PREFIX, SLOTLOCK_API_KEY_RETAINED_LIMIT, SLOTLOCK_API_KEY_SCOPES, } from './api-keys.js';
21
22
  export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
23
+ export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
22
24
  export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
23
25
  export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emitITipCalendar, expandCalendarEventOccurrences, normalizeCalendarEventContent, normalizeExternalCalendarChange, parseICalendarChanges, parseICalendarFeed, parseTrustedICalendarEvents, recurrenceRuleProblem, storedCalendarEventProblem, } from './sync.js';
24
26
  export { createSlotlockApplicationRoleGrantsDdl, createSlotlockTenantRlsDdl, SLOTLOCK_API_KEY_FUNCTIONS, SLOTLOCK_API_KEY_TABLES, SLOTLOCK_CORE_DDL, SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_DASHBOARD_TABLES, SLOTLOCK_DEFINER_FUNCTIONS, SLOTLOCK_DEFINER_TABLES, SLOTLOCK_OPERATOR_FUNCTIONS, SLOTLOCK_SCHEMA_FUNCTIONS, SLOTLOCK_SCHEMA_TABLES, SLOTLOCK_SCHEMA_VERSION, SLOTLOCK_TENANT_ADMIN_FUNCTIONS, SLOTLOCK_TENANT_ADMIN_TABLES, SLOTLOCK_TENANT_CONTEXT_SETTING, SLOTLOCK_TENANT_RLS_DDL, SLOTLOCK_TENANT_TABLES, } from './ddl.js';
package/maintenance.d.ts CHANGED
@@ -10,7 +10,8 @@ export interface SlotlockMaintenanceResult {
10
10
  /**
11
11
  * Expired holds and the answers kept for keyed ones, and past the retention window event
12
12
  * idempotency answers, agents' event cancellations, booking references retired with no
13
- * booking and feed changes, deleted. A cancelled booking's tombstone stays.
13
+ * booking and feed changes, deleted, and dashboard assistant conversations past their expiry.
14
+ * A cancelled booking's tombstone stays.
14
15
  */
15
16
  pruned: number;
16
17
  /**
package/maintenance.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ignoreRejection } from './hooks.js';
2
- import { SLOTLOCK_MAX_WAITLIST_PER_TENANT, calendarEventRollingHorizon } from './store.js';
2
+ import { SLOTLOCK_MAX_WAITLIST_PER_TENANT, calendarEventRollingHorizon, } from './store.js';
3
3
  /**
4
4
  * One maintenance pass for a tenant: roll its recurring events to the rolling horizon, then purge
5
5
  * its expired holds with their answers and prune its events' and bookings' bookkeeping past the
@@ -11,6 +11,8 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
11
11
  if (!(Number.isSafeInteger(batches) && batches >= 1 && batches <= 100)) {
12
12
  throw new RangeError('batches must be a whole number from 1 to 100');
13
13
  }
14
+ // What a pass changes (a no-show released, a waitlist offer) is recorded as maintenance's.
15
+ const withTenant = (callback) => store.withTenant(tenantRef, callback, { origin: 'maintenance' });
14
16
  const window = calendarEventRollingHorizon();
15
17
  let extended = 0;
16
18
  let conflicts = 0;
@@ -24,7 +26,7 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
24
26
  for (let batch = 0; batch < batches; batch++) {
25
27
  options.signal?.throwIfAborted();
26
28
  const from = after;
27
- const roll = await store.withTenant(tenantRef, (tenant) => tenant.rollCalendarEventHorizon({ tenantRef, window, ...(from ? { after: from } : {}) }));
29
+ const roll = await withTenant((tenant) => tenant.rollCalendarEventHorizon({ tenantRef, window, ...(from ? { after: from } : {}) }));
28
30
  extended += roll.extended;
29
31
  conflicts += roll.conflicts;
30
32
  refused += roll.refused;
@@ -47,26 +49,29 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
47
49
  for (let batch = 0; batch < batches && (eventsLeft || bookingsLeft || changesLeft); batch++) {
48
50
  options.signal?.throwIfAborted();
49
51
  if (eventsLeft) {
50
- const prune = await store.withTenant(tenantRef, (tenant) => tenant.pruneCalendarEventRetention({ tenantRef }));
52
+ const prune = await withTenant((tenant) => tenant.pruneCalendarEventRetention({ tenantRef }));
51
53
  pruned += prune.commandsDeleted + prune.tombstonesDeleted;
52
54
  eventsLeft = prune.hasMore;
53
55
  }
54
56
  if (bookingsLeft) {
55
- const prune = await store.withTenant(tenantRef, (tenant) => tenant.pruneBookingRetention({ tenantRef }));
57
+ const prune = await withTenant((tenant) => tenant.pruneBookingRetention({ tenantRef }));
56
58
  pruned += prune.expiredHoldsDeleted + prune.holdCommandsDeleted + prune.tombstonesDeleted;
57
59
  bookingsLeft = prune.hasMore;
58
60
  }
59
61
  if (changesLeft) {
60
- const deleted = await store.withTenant(tenantRef, (tenant) => tenant.pruneChanges({ tenantRef, limit: CHANGE_PRUNE_BATCH }));
62
+ const deleted = await withTenant((tenant) => tenant.pruneChanges({ tenantRef, limit: CHANGE_PRUNE_BATCH }));
61
63
  pruned += deleted;
62
64
  changesLeft = deleted === CHANGE_PRUNE_BATCH;
63
65
  }
64
66
  }
65
67
  options.signal?.throwIfAborted();
66
- const zones = await store.withTenant(tenantRef, (tenant) => tenant.followZoneRules({ tenantRef, move: options.followZoneRules === true }));
67
- const staleSources = await store.withTenant(tenantRef, (tenant) => tenant.reportStaleCoverage({ tenantRef }));
68
+ // Conversations last 2 hours after their last message (dashboard.ts): an expired one is
69
+ // unreadable already, and deleted here.
70
+ pruned += await withTenant((tenant) => tenant.pruneAssistantConversations({ tenantRef }));
71
+ const zones = await withTenant((tenant) => tenant.followZoneRules({ tenantRef, move: options.followZoneRules === true }));
72
+ const staleSources = await withTenant((tenant) => tenant.reportStaleCoverage({ tenantRef }));
68
73
  // No-shows first: the time they free is offered to the waitlist in the same pass.
69
- const noShows = await store.withTenant(tenantRef, (tenant) => tenant.releaseNoShows({ tenantRef }));
74
+ const noShows = await withTenant((tenant) => tenant.releaseNoShows({ tenantRef }));
70
75
  // The whole waitlist, in join order, a page at a time and each page its own transaction, so a
71
76
  // later entry whose window is free is offered however many wait before it.
72
77
  let waitlistOffers = 0;
@@ -74,7 +79,7 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
74
79
  for (let pages = 0; pages < WAITLIST_PAGES; pages++) {
75
80
  options.signal?.throwIfAborted();
76
81
  const from = page;
77
- const offers = await store.withTenant(tenantRef, (tenant) => tenant.offerWaitlist({ tenantRef, ...(from ? { after: from } : {}) }));
82
+ const offers = await withTenant((tenant) => tenant.offerWaitlist({ tenantRef, ...(from ? { after: from } : {}) }));
78
83
  waitlistOffers += offers.offered;
79
84
  page = offers.next;
80
85
  if (!page)
@@ -97,6 +102,8 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
97
102
  }
98
103
  /** Pages of 50 that cover a tenant's whole waitlist (SLOTLOCK_MAX_WAITLIST_PER_TENANT). */
99
104
  const WAITLIST_PAGES = Math.ceil(SLOTLOCK_MAX_WAITLIST_PER_TENANT / 50);
105
+ /** How often the scheduler sweeps every tenant's expired assistant conversations, at most. */
106
+ const SWEEP_INTERVAL_MS = 15 * 60_000;
100
107
  /** Feed changes past the retention deleted per batch. */
101
108
  const CHANGE_PRUNE_BATCH = 1_000;
102
109
  /** The advisory lock key for one tenant's maintenance (64-bit, from the tenant reference). */
@@ -208,11 +215,28 @@ export function startSlotlockMaintenance(options) {
208
215
  working = null;
209
216
  }
210
217
  };
218
+ // Expired assistant conversations of every tenant, the ones whose keys never call included (a
219
+ // tenant only the dashboard serves): one bounded sweep at most every quarter hour.
220
+ let lastSweep = 0;
221
+ const sweep = () => {
222
+ if (controller.signal.aborted || Date.now() - lastSweep < Math.min(intervalMs, SWEEP_INTERVAL_MS))
223
+ return;
224
+ lastSweep = Date.now();
225
+ options.store.sweepAssistantConversations().catch((error) => {
226
+ try {
227
+ ignoreRejection(options.onError?.(error));
228
+ }
229
+ catch {
230
+ // Reporting must never stop maintenance.
231
+ }
232
+ });
233
+ };
211
234
  const tick = () => {
212
235
  for (const tenantRef of [...pinned.keys(), ...served.keys()]) {
213
236
  if (due(tenantRef))
214
237
  enqueue(tenantRef);
215
238
  }
239
+ sweep();
216
240
  };
217
241
  const timer = setInterval(tick, Math.min(intervalMs, 60_000));
218
242
  timer.unref();