slotlock 0.5.2 → 0.6.0

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,47 @@
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 digest 0.5.3 named a coverage source by on the feed: `src_` and the first 24 hex digits of
18
+ * sha256(tenant, newline, source). Unkeyed, so anyone who knows the tenant could confirm a guessed
19
+ * source against it; 0.6 names a source by a random id instead (`calendar_coverage.source_id`), and
20
+ * the upgrade uses this only to find the changes 0.5.3 wrote and rename them. `tenant` and `source`
21
+ * are SQL expressions.
22
+ */
23
+ export declare function legacyCoverageSourceDigestSql(tenant: string, source: string): string;
24
+ /**
25
+ * 0.5.3 worked out a source's id here: an unkeyed digest of the tenant and the source, which let
26
+ * anyone who saw a change confirm a guessed source. Since 0.6 a source's `source_id` is random and
27
+ * only the database knows it, so this throws (`coverage_source_id_unavailable`) rather than give a
28
+ * digest nothing carries any more.
29
+ *
30
+ * @deprecated Read `sourceId` from `listCoverage` (`source_id` of `slotlock_get_coverage`). It may
31
+ * be removed in 0.7.0.
32
+ */
33
+ export declare function slotlockCoverageSourceId(_tenantRef: string, _source: string): never;
34
+ /**
35
+ * A change's subject and data as the feed serves them. A `coverage.*` change never carries its
36
+ * source (`source`) or the error read from it (`last_error`, which can quote it), and is named by a
37
+ * `source_id` of this release or `src_redacted`: an earlier release wrote both, and the upgrade
38
+ * renames those rows in batches after it commits, so a row it has not reached yet, or one an
39
+ * earlier release's server writes during a rolling upgrade, is served redacted.
40
+ */
41
+ export declare function slotlockServedChange(type: string, subject: string, data: Record<string, unknown>): {
42
+ subject: string;
43
+ data: Record<string, unknown>;
44
+ };
6
45
  /** How long the feed keeps a change, and so how far back a cursor may reach. */
7
46
  export declare const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
8
47
  /** Webhook endpoints one tenant may have. */
@@ -20,6 +59,10 @@ export interface SlotlockChange {
20
59
  occurredAt: Date;
21
60
  /** Who made it (withTenant's `actor`: an API key, an agent), or null when unrecorded. */
22
61
  actor: string | null;
62
+ /** Where it came from (withTenant's `origin`), or null when unrecorded (before 0.6, say). */
63
+ origin: SlotlockChangeOrigin | null;
64
+ /** The API key that made it (withTenant's `keyId`), or null when no key did. */
65
+ keyId: string | null;
23
66
  /** Pass as `after` to read what follows this change. */
24
67
  position: string;
25
68
  }
@@ -150,9 +193,10 @@ export interface SlotlockEventFeed {
150
193
  };
151
194
  }): Promise<void>;
152
195
  /**
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
196
+ * History: the changes to one resource (`resourceId`: its bookings and events, and its own
197
+ * configuration) or one record (`subject`: a booking's reference, an event's, hold's, webhook's or
198
+ * API key's id), newest first, `limit` (1-100) at a time, each with who made it (`actor`, `keyId`),
199
+ * where from (`origin`) and, for a change of an existing record, what it was before. Pass the last one's
156
200
  * `id` as `before` for the page after. Kept as long as the feed keeps changes.
157
201
  */
158
202
  listHistory(params: {
@@ -175,4 +219,10 @@ export declare function isWebhookUrl(value: unknown): value is string;
175
219
  * `retentionDays` (default SLOTLOCK_CHANGE_RETENTION_DAYS), and a cursor reaches back as far.
176
220
  */
177
221
  export declare function slotlockEventFeedMethods(sql: SlotlockSql, retentionDays?: number): SlotlockEventFeed;
222
+ /**
223
+ * A position at or before every change that has not ended yet, so a reader that starts from it and
224
+ * follows `next` misses none still to come (it may read again some that ended just before). Live
225
+ * updates start here (agent-store-backend.ts, createSlotlockStoreChangeSource).
226
+ */
227
+ export declare function slotlockChangeFeedHead(sql: SlotlockSql): Promise<string>;
178
228
  export {};
package/event-feed.js CHANGED
@@ -28,7 +28,86 @@ 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 digest 0.5.3 named a coverage source by on the feed: `src_` and the first 24 hex digits of
69
+ * sha256(tenant, newline, source). Unkeyed, so anyone who knows the tenant could confirm a guessed
70
+ * source against it; 0.6 names a source by a random id instead (`calendar_coverage.source_id`), and
71
+ * the upgrade uses this only to find the changes 0.5.3 wrote and rename them. `tenant` and `source`
72
+ * are SQL expressions.
73
+ */
74
+ export function legacyCoverageSourceDigestSql(tenant, source) {
75
+ return `'src_' || pg_catalog.left(pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(${tenant} || E'\\n' || ${source}, 'UTF8')), 'hex'), 24)`;
76
+ }
77
+ /**
78
+ * 0.5.3 worked out a source's id here: an unkeyed digest of the tenant and the source, which let
79
+ * anyone who saw a change confirm a guessed source. Since 0.6 a source's `source_id` is random and
80
+ * only the database knows it, so this throws (`coverage_source_id_unavailable`) rather than give a
81
+ * digest nothing carries any more.
82
+ *
83
+ * @deprecated Read `sourceId` from `listCoverage` (`source_id` of `slotlock_get_coverage`). It may
84
+ * be removed in 0.7.0.
85
+ */
86
+ export function slotlockCoverageSourceId(_tenantRef, _source) {
87
+ throw Object.assign(new Error("slotlockCoverageSourceId cannot work out a source's id since 0.6: it is random. Read sourceId from listCoverage (source_id of slotlock_get_coverage)"), { code: 'coverage_source_id_unavailable' });
88
+ }
89
+ /** What a coverage source's `source_id` is: `src_` and 32 random hex digits. */
90
+ const SLOTLOCK_COVERAGE_SOURCE_ID = /^src_[0-9a-f]{32}$/;
91
+ /**
92
+ * The `source_id` of a `coverage.*` change whose source could not be named any more: one an
93
+ * earlier release recorded by the source itself (or by its 0.5.3 digest) after the source was
94
+ * deleted, or one not yet renamed when it is read.
95
+ */
96
+ const SLOTLOCK_REDACTED_SOURCE_ID = 'src_redacted';
97
+ /**
98
+ * A change's subject and data as the feed serves them. A `coverage.*` change never carries its
99
+ * source (`source`) or the error read from it (`last_error`, which can quote it), and is named by a
100
+ * `source_id` of this release or `src_redacted`: an earlier release wrote both, and the upgrade
101
+ * renames those rows in batches after it commits, so a row it has not reached yet, or one an
102
+ * earlier release's server writes during a rolling upgrade, is served redacted.
103
+ */
104
+ export function slotlockServedChange(type, subject, data) {
105
+ if (!type.startsWith('coverage.'))
106
+ return { subject, data };
107
+ const { source: _source, last_error: _lastError, ...rest } = data;
108
+ const sourceId = SLOTLOCK_COVERAGE_SOURCE_ID.test(subject) ? subject : SLOTLOCK_REDACTED_SOURCE_ID;
109
+ return { subject: sourceId, data: { ...rest, source_id: sourceId } };
110
+ }
32
111
  /** How long the feed keeps a change, and so how far back a cursor may reach. */
33
112
  export const SLOTLOCK_CHANGE_RETENTION_DAYS = 30;
34
113
  /** Webhook endpoints one tenant may have. */
@@ -54,10 +133,12 @@ function toChange(row) {
54
133
  id: `msg_${row.id}`,
55
134
  type: row.type,
56
135
  resourceId: row.resource_id,
57
- subject: row.subject,
58
- data: row.data,
136
+ ...slotlockServedChange(row.type, row.subject, row.data),
59
137
  occurredAt: row.occurred_at,
60
138
  actor: row.actor,
139
+ // One a later release added, read by this one: unknown here, so unrecorded, not a failed page.
140
+ origin: isSlotlockChangeOrigin(row.origin) ? row.origin : null,
141
+ keyId: row.key_id,
61
142
  position: encodePosition({ txid: row.txid, id: row.id, at: row.occurred_at.getTime() }),
62
143
  };
63
144
  }
@@ -127,7 +208,7 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
127
208
  SELECT mark.txid::text AS pruned_txid, mark.id::text AS pruned_id,
128
209
  pg_snapshot_xmin(pg_current_snapshot())::text AS horizon,
129
210
  page.id::text, page.txid::text, page.type, page.resource_id, page.subject,
130
- page.data, page.occurred_at, page.actor
211
+ page.data, page.occurred_at, page.actor, page.origin, page.key_id::text
131
212
  FROM (SELECT 1) here
132
213
  LEFT JOIN slotlock.outbox_pruned mark ON mark.tenant_ref = ${params.tenantRef}
133
214
  LEFT JOIN LATERAL (
@@ -179,7 +260,8 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
179
260
  });
180
261
  }
181
262
  const rows = await sql `
182
- SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor
263
+ SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor, origin,
264
+ key_id::text
183
265
  FROM slotlock.outbox
184
266
  WHERE tenant_ref = ${params.tenantRef}
185
267
  ORDER BY txid DESC, id DESC
@@ -194,7 +276,6 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
194
276
  const types = params.eventTypes === undefined ? null : [...new Set(params.eventTypes)];
195
277
  if (types !== null &&
196
278
  (types.length === 0 ||
197
- types.length > 20 ||
198
279
  !types.every((type) => SLOTLOCK_CHANGE_TYPES.includes(type)))) {
199
280
  return { ok: false, code: 'invalid_event_types' };
200
281
  }
@@ -300,7 +381,8 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
300
381
  const claims = [];
301
382
  for (const endpoint of endpoints) {
302
383
  const rows = await sql `
303
- SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor
384
+ SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor, origin,
385
+ key_id::text
304
386
  FROM slotlock.outbox
305
387
  WHERE tenant_ref = ${params.tenantRef}
306
388
  AND txid < pg_snapshot_xmin(pg_current_snapshot())
@@ -393,11 +475,16 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
393
475
  if (before === undefined)
394
476
  return [];
395
477
  const rows = await sql `
396
- SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor
478
+ SELECT id::text, txid::text, type, resource_id, subject, data, occurred_at, actor, origin,
479
+ key_id::text
397
480
  FROM slotlock.outbox
398
481
  WHERE tenant_ref = ${params.tenantRef}
399
482
  AND (${params.resourceId ?? null}::uuid IS NULL OR resource_id = ${params.resourceId ?? null}::uuid)
400
- AND (${params.subject ?? null}::text IS NULL OR subject = ${params.subject ?? null})
483
+ AND (${params.subject ?? null}::text IS NULL OR (
484
+ subject = ${params.subject ?? null}
485
+ -- A coverage change matches only by the name it is served under: one not renamed
486
+ -- yet may still hold its source, and a guess must not find it.
487
+ AND (type NOT LIKE 'coverage.%' OR subject ~ '^src_([0-9a-f]{32}|redacted)$')))
401
488
  AND (${before}::bigint IS NULL OR id < ${before}::bigint)
402
489
  ORDER BY id DESC
403
490
  LIMIT ${limit}`;
@@ -445,3 +532,15 @@ export function slotlockEventFeedMethods(sql, retentionDays = SLOTLOCK_CHANGE_RE
445
532
  },
446
533
  };
447
534
  }
535
+ /**
536
+ * A position at or before every change that has not ended yet, so a reader that starts from it and
537
+ * follows `next` misses none still to come (it may read again some that ended just before). Live
538
+ * updates start here (agent-store-backend.ts, createSlotlockStoreChangeSource).
539
+ */
540
+ export async function slotlockChangeFeedHead(sql) {
541
+ const [row] = await sql `
542
+ SELECT pg_snapshot_xmin(pg_current_snapshot())::text AS horizon`;
543
+ if (!row)
544
+ throw new Error('slotlock: change feed head returned no row');
545
+ return encodePosition({ txid: row.horizon, id: '0', at: Date.now() });
546
+ }
package/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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';
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
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';
@@ -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';
15
- export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockMcpToolResult, SlotlockTraceContext, } 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
+ export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockArgumentIssue, 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';
@@ -26,11 +27,11 @@ export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlo
26
27
  export type { SlotlockDashboard, SlotlockDashboardAssistantOptions, SlotlockDashboardOptions, SlotlockDashboardResources, SlotlockDashboardState, } from './dashboard.js';
27
28
  export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
28
29
  export type { SlotlockAssistantStore } from './assistant.js';
29
- export type { CreateSlotlockApiKeyInput, SlotlockApiKey, SlotlockApiKeyAuthenticatorOptions, SlotlockApiKeyIdentity, SlotlockApiKeyScope, SlotlockApiKeyStore, } from './api-keys.js';
30
+ export type { CreateSlotlockApiKeyInput, SlotlockApiKey, SlotlockApiKeyAuthenticatorOptions, SlotlockApiKeyChange, SlotlockApiKeyIdentity, SlotlockApiKeyScope, SlotlockApiKeyStore, } from './api-keys.js';
30
31
  export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
31
32
  export type { ICalendarWorkerPool, ICalendarWorkerPoolOptions } from './ical-worker.js';
32
33
  export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emitITipCalendar, expandCalendarEventOccurrences, normalizeCalendarEventContent, normalizeExternalCalendarChange, parseICalendarChanges, parseICalendarFeed, parseTrustedICalendarEvents, recurrenceRuleProblem, storedCalendarEventProblem, } from './sync.js';
33
34
  export type { ExternalCalendarChange, ExternalCalendarChangeInput, ExternalCalendarStatus, ICalendarEvent, ICalendarExpansionWindow, ICalendarFeedRead, ICalendarSeriesRefusal, ITipMethod, } from './sync.js';
34
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';
35
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';
36
- 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,17 +4,18 @@
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';
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
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';
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,17 +49,17 @@ 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
  }
@@ -65,11 +67,11 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
65
67
  options.signal?.throwIfAborted();
66
68
  // Conversations last 2 hours after their last message (dashboard.ts): an expired one is
67
69
  // unreadable already, and deleted here.
68
- pruned += await store.withTenant(tenantRef, (tenant) => tenant.pruneAssistantConversations({ tenantRef }));
69
- const zones = await store.withTenant(tenantRef, (tenant) => tenant.followZoneRules({ tenantRef, move: options.followZoneRules === true }));
70
- const staleSources = await store.withTenant(tenantRef, (tenant) => tenant.reportStaleCoverage({ tenantRef }));
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 }));
71
73
  // No-shows first: the time they free is offered to the waitlist in the same pass.
72
- const noShows = await store.withTenant(tenantRef, (tenant) => tenant.releaseNoShows({ tenantRef }));
74
+ const noShows = await withTenant((tenant) => tenant.releaseNoShows({ tenantRef }));
73
75
  // The whole waitlist, in join order, a page at a time and each page its own transaction, so a
74
76
  // later entry whose window is free is offered however many wait before it.
75
77
  let waitlistOffers = 0;
@@ -77,7 +79,7 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
77
79
  for (let pages = 0; pages < WAITLIST_PAGES; pages++) {
78
80
  options.signal?.throwIfAborted();
79
81
  const from = page;
80
- 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 } : {}) }));
81
83
  waitlistOffers += offers.offered;
82
84
  page = offers.next;
83
85
  if (!page)