slotlock 0.6.2 → 0.7.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/CHANGELOG.md +492 -0
- package/CONTRIBUTING.md +12 -5
- package/README.md +50 -1544
- package/SECURITY.md +5 -2
- package/SPEC.md +1 -1
- package/agent-server.d.ts +160 -8
- package/agent-server.js +560 -49
- package/agent-store-backend.d.ts +39 -7
- package/agent-store-backend.js +1439 -104
- package/api-keys.d.ts +69 -3
- package/api-keys.js +218 -29
- package/application-api.d.ts +59 -6
- package/application-api.js +842 -39
- package/approvals.d.ts +88 -0
- package/approvals.js +293 -0
- package/assistant.d.ts +58 -27
- package/assistant.js +151 -53
- package/billing.d.ts +114 -0
- package/billing.js +258 -0
- package/booking-rules.d.ts +30 -41
- package/booking-rules.js +17 -25
- package/change-watch.d.ts +1 -0
- package/change-watch.js +1 -1
- package/cli.js +3 -4
- package/config.d.ts +42 -6
- package/config.js +114 -7
- package/dashboard-icon.d.ts +1 -0
- package/dashboard-icon.js +5 -0
- package/dashboard.d.ts +203 -7
- package/dashboard.js +1558 -64
- package/ddl.d.ts +67 -35
- package/ddl.js +2732 -162
- package/engine.d.ts +6 -9
- package/engine.js +5 -9
- package/event-feed.d.ts +53 -35
- package/event-feed.js +201 -64
- package/event-schema.js +6 -9
- package/hooks.d.ts +2 -4
- package/hooks.js +2 -4
- package/ical-worker-thread.js +3 -4
- package/ical-worker.d.ts +8 -10
- package/ical-worker.js +7 -9
- package/index.d.ts +15 -4
- package/index.js +8 -2
- package/interop.js +1 -4
- package/maintenance.d.ts +24 -10
- package/maintenance.js +59 -28
- package/mcp-app.d.ts +7 -9
- package/mcp-app.js +7 -9
- package/mcp-modern.d.ts +14 -16
- package/mcp-modern.js +23 -27
- package/metrics.d.ts +41 -0
- package/metrics.js +22 -1
- package/node-server.d.ts +9 -9
- package/node-server.js +25 -34
- package/openapi.json +14675 -5957
- package/otel.d.ts +86 -0
- package/otel.js +273 -0
- package/overage.d.ts +33 -0
- package/overage.js +115 -0
- package/package-version.js +2 -4
- package/package.json +1 -1
- package/partner-billing.d.ts +81 -0
- package/partner-billing.js +333 -0
- package/partner-names.d.ts +8 -0
- package/partner-names.js +10 -0
- package/partners.d.ts +147 -0
- package/partners.js +375 -0
- package/plans.d.ts +92 -0
- package/plans.js +115 -0
- package/quotas.d.ts +108 -0
- package/quotas.js +258 -0
- package/rate-limit.d.ts +27 -2
- package/rate-limit.js +106 -23
- package/resource-profile.d.ts +135 -0
- package/resource-profile.js +263 -0
- package/rest-api.d.ts +15 -14
- package/rest-api.js +79 -15
- package/rules.d.ts +6 -7
- package/rules.js +30 -42
- package/sandbox.d.ts +70 -0
- package/sandbox.js +295 -0
- package/self-host.d.ts +46 -4
- package/self-host.js +990 -41
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +482 -165
- package/store.js +1460 -484
- package/sync.d.ts +44 -59
- package/sync.js +325 -469
- package/teams.d.ts +25 -0
- package/teams.js +63 -0
- package/timezone.d.ts +25 -33
- package/timezone.js +50 -53
- package/types.d.ts +130 -57
- package/usage.d.ts +106 -0
- package/usage.js +238 -0
- package/webhooks.d.ts +22 -25
- package/webhooks.js +35 -40
- package/weekly-hours.d.ts +3 -0
- package/weekly-hours.js +1 -1
package/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" preserve="true" />
|
|
2
2
|
export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
|
|
3
3
|
export { type SlotlockBookingRule, type SlotlockBookingRules, type SlotlockBookingSearch, bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
|
|
4
|
+
export { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_FIELDS, SLOTLOCK_RESOURCE_PROFILE_LIMITS, parseSlotlockLocation, parseSlotlockResourceGroup, parseSlotlockResourceProfile, type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceAttributes, type SlotlockResourceKind, type SlotlockResourceProfileChange, type SlotlockResourceProfileInput, } from './resource-profile.js';
|
|
4
5
|
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
6
|
export { type SlotlockCloudEvent, type SlotlockWebhookPassResult, type SlotlockWebhookResponse, type SlotlockWebhookScheduler, type SlotlockWebhookSender, createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
|
|
6
7
|
export { type SlotlockMetrics, createSlotlockMetrics } from './metrics.js';
|
|
@@ -11,8 +12,8 @@ export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOC
|
|
|
11
12
|
export type { EmitVAvailabilityOptions, JSCalendarAlert, JSCalendarEvent, JSCalendarParticipant, JSCalendarRecurrenceRule, ParsedVAvailability, } from './interop.js';
|
|
12
13
|
export { createTimeZoneConverter, zonedCalendarDate, zonedDateTimeToUtc, zonedWallTime, } from './timezone.js';
|
|
13
14
|
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_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';
|
|
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_MAX_ALTERNATIVES, 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
|
+
export type { SlotlockAgentCalendarBackend, SlotlockAgentConfirmationOptions, SlotlockAgentErrorContext, SlotlockAgentInvocationContext, SlotlockAgentOperation, SlotlockAgentOperationDispatchOptions, SlotlockAgentOperationDispatchResult, SlotlockAgentPrincipal, SlotlockAgentScope, SlotlockAgentServerEvent, SlotlockAgentServerOAuthOptions, SlotlockAgentServerOptions, SlotlockAgentSubscriptionOptions, SlotlockAgentWriteOperation, SlotlockAlternative, SlotlockArgumentIssue, SlotlockMcpToolResult, SlotlockTraceContext, } from './agent-server.js';
|
|
16
17
|
export { createSlotlockNodeServer } from './node-server.js';
|
|
17
18
|
export type { SlotlockNodeListenOptions, SlotlockNodeServer, SlotlockNodeServerAddress, SlotlockNodeServerCloseResult, SlotlockNodeServerErrorPhase, SlotlockNodeServerFetchTarget, SlotlockNodeServerOptions, } from './node-server.js';
|
|
18
19
|
export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
|
|
@@ -22,11 +23,21 @@ export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlock
|
|
|
22
23
|
export type { SlotlockApplicationBackend, SlotlockApplicationInvocationContext, SlotlockApplicationOperation, SlotlockApplicationOperationDefinition, } from './application-api.js';
|
|
23
24
|
export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
|
|
24
25
|
export type { SlotlockMaintenanceResult, SlotlockMaintenanceScheduler, SlotlockMaintenanceSchedulerOptions, } from './maintenance.js';
|
|
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';
|
|
26
|
+
export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, isSlotlockTestTenant, slotlockLiveTenant, SLOTLOCK_TEST_API_KEY_PREFIX, SLOTLOCK_TEST_TENANT_SUFFIX, 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';
|
|
26
27
|
export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
|
|
27
28
|
export type { SlotlockDashboard, SlotlockDashboardAssistantOptions, SlotlockDashboardOptions, SlotlockDashboardResources, SlotlockDashboardState, } from './dashboard.js';
|
|
28
29
|
export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
|
|
29
30
|
export type { SlotlockAssistantStore } from './assistant.js';
|
|
31
|
+
export { createSlotlockPartnerKeyAuthenticator, createSlotlockPartnerStore, parseSlotlockPartnerContract, slotlockPartnerTenantRef, SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_ID, SLOTLOCK_PARTNER_SCOPE, } from './partners.js';
|
|
32
|
+
export { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from './teams.js';
|
|
33
|
+
export { reportSlotlockOverage, SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT } from './overage.js';
|
|
34
|
+
export type { SlotlockOverageReport } from './overage.js';
|
|
35
|
+
export { createSlotlockOtlpExporter, readSlotlockOtlpConfig, slotlockCallSpan } from './otel.js';
|
|
36
|
+
export type { SlotlockOtlpConfig, SlotlockTraceExporter } from './otel.js';
|
|
37
|
+
export type { SlotlockDashboardTeams } from './teams.js';
|
|
38
|
+
export { closeSlotlockPartnerMonth, priceSlotlockPartnerMonth, slotlockClientLabel, slotlockPartnerMonthChecksum, } from './partner-billing.js';
|
|
39
|
+
export type { SlotlockPartnerAccountMonth, SlotlockPartnerInvoiceLine, SlotlockPartnerMonthClose, } from './partner-billing.js';
|
|
40
|
+
export type { SlotlockPartnerAccount, SlotlockPartnerContract, SlotlockPartnerPlan, SlotlockPartnerPricing, SlotlockPartnerStore, } from './partners.js';
|
|
30
41
|
export type { CreateSlotlockApiKeyInput, SlotlockApiKey, SlotlockApiKeyAuthenticatorOptions, SlotlockApiKeyChange, SlotlockApiKeyIdentity, SlotlockApiKeyScope, SlotlockApiKeyStore, } from './api-keys.js';
|
|
31
42
|
export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
|
|
32
43
|
export type { ICalendarWorkerPool, ICalendarWorkerPoolOptions } from './ical-worker.js';
|
|
@@ -34,4 +45,4 @@ export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emit
|
|
|
34
45
|
export type { ExternalCalendarChange, ExternalCalendarChangeInput, ExternalCalendarStatus, ICalendarEvent, ICalendarExpansionWindow, ICalendarFeedRead, ICalendarSeriesRefusal, ITipMethod, } from './sync.js';
|
|
35
46
|
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';
|
|
36
47
|
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';
|
|
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';
|
|
48
|
+
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, SlotlockBookingDescription, SlotlockReservation, SlotlockResource, SlotlockTenantErasure, SlotlockBookingRuleDefaults, PutCalendarEventCommand, PutCalendarEventResult, ReleaseHoldResult, TrustedICalendarEvent, WeeklyAvailabilityRule, } from './types.js';
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
/// <reference types="node" preserve="true" />
|
|
6
6
|
export { findNextAvailable, mergeIntervals, subtractBusy } from './engine.js';
|
|
7
7
|
export { bookingRuleBroken, bookingSearch, bookingSearchBlocked, bookingSearchReach, nextStepStart, parseBookingRuleDefaults, parseBookingRules, } from './booking-rules.js';
|
|
8
|
+
export { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_FIELDS, SLOTLOCK_RESOURCE_PROFILE_LIMITS, parseSlotlockLocation, parseSlotlockResourceGroup, parseSlotlockResourceProfile, } from './resource-profile.js';
|
|
8
9
|
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
10
|
export { createSlotlockGuardedFetch, createSlotlockWebhookSender, deliverSlotlockWebhooks, isPrivateAddress, signSlotlockWebhook, slotlockCloudEvent, slotlockWebhookRetryDelayMs, startSlotlockWebhookDelivery, } from './webhooks.js';
|
|
10
11
|
export { createSlotlockMetrics } from './metrics.js';
|
|
@@ -12,15 +13,20 @@ export { expandRules } from './rules.js';
|
|
|
12
13
|
export { availabilityRuleProblem, parseAvailabilityRules, rulesToWeeklyHours, SLOTLOCK_MAX_AVAILABILITY_RULES, weeklyHoursToRules, } from './weekly-hours.js';
|
|
13
14
|
export { emitVAvailability, parseVAvailability, recurrenceRuleFromRRule, SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS, toJSCalendarEvent, } from './interop.js';
|
|
14
15
|
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_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
|
+
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_MAX_ALTERNATIVES, 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
17
|
export { createSlotlockNodeServer } from './node-server.js';
|
|
17
18
|
export { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, projectSlotlockCalendarEvent, } from './agent-store-backend.js';
|
|
18
19
|
export { SLOTLOCK_FEED_BUSY_MS, SLOTLOCK_FEED_IDLE_MS, } from './change-watch.js';
|
|
19
20
|
export { SLOTLOCK_APPLICATION_OPERATIONS, SLOTLOCK_APPLICATION_SCOPE, isSlotlockApplicationOperation, } from './application-api.js';
|
|
20
21
|
export { maintainSlotlockTenant, startSlotlockMaintenance, withSlotlockMaintenanceLock, } from './maintenance.js';
|
|
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';
|
|
22
|
+
export { createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, generateSlotlockApiKey, isSlotlockApiKey, isSlotlockTestTenant, slotlockLiveTenant, SLOTLOCK_TEST_API_KEY_PREFIX, SLOTLOCK_TEST_TENANT_SUFFIX, 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';
|
|
22
23
|
export { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, createSlotlockMemoryDashboardState, SLOTLOCK_DASHBOARD_MAX_RESOURCES, } from './dashboard.js';
|
|
23
24
|
export { createSlotlockAssistantStore, createSlotlockMemoryAssistantStore, SLOTLOCK_ASSISTANT_TOOL_USE, } from './assistant.js';
|
|
25
|
+
export { createSlotlockPartnerKeyAuthenticator, createSlotlockPartnerStore, parseSlotlockPartnerContract, slotlockPartnerTenantRef, SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_ID, SLOTLOCK_PARTNER_SCOPE, } from './partners.js';
|
|
26
|
+
export { createSlotlockDashboardTeams, createSlotlockOrganizationCheck } from './teams.js';
|
|
27
|
+
export { reportSlotlockOverage, SLOTLOCK_DEFAULT_OVERAGE_STOP_PERCENT } from './overage.js';
|
|
28
|
+
export { createSlotlockOtlpExporter, readSlotlockOtlpConfig, slotlockCallSpan } from './otel.js';
|
|
29
|
+
export { closeSlotlockPartnerMonth, priceSlotlockPartnerMonth, slotlockClientLabel, slotlockPartnerMonthChecksum, } from './partner-billing.js';
|
|
24
30
|
export { ICalendarWorkerBusyError, ICalendarWorkerPoolClosedError, createICalendarWorkerPool, } from './ical-worker.js';
|
|
25
31
|
export { CalendarContractError, CalendarExpansionLimitError, emitICalendar, emitITipCalendar, expandCalendarEventOccurrences, normalizeCalendarEventContent, normalizeExternalCalendarChange, parseICalendarChanges, parseICalendarFeed, parseTrustedICalendarEvents, recurrenceRuleProblem, storedCalendarEventProblem, } from './sync.js';
|
|
26
32
|
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/interop.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// directly.
|
|
5
5
|
import ICAL from 'ical.js';
|
|
6
6
|
import { zonedDateTimeFields } from './timezone.js';
|
|
7
|
-
import { parseAvailabilityRules } from './weekly-hours.js';
|
|
7
|
+
import { invalidAvailability, parseAvailabilityRules } from './weekly-hours.js';
|
|
8
8
|
/** The longest VAVAILABILITY text `parseVAvailability` reads. */
|
|
9
9
|
export const SLOTLOCK_MAX_VAVAILABILITY_CHARACTERS = 65_536;
|
|
10
10
|
const PRODID = '-//Pylota//Slotlock//EN';
|
|
@@ -13,9 +13,6 @@ const DAYS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
|
|
13
13
|
/** 2024-01-07 is a Sunday: the reference week runs Sunday 7 to Saturday 13 January. */
|
|
14
14
|
const REFERENCE_SUNDAY = Date.UTC(2024, 0, 7);
|
|
15
15
|
const VAVAILABILITY_DTSTAMP = '20240101T000000Z';
|
|
16
|
-
function invalidAvailability(message) {
|
|
17
|
-
return Object.assign(new Error(message), { code: 'invalid_availability' });
|
|
18
|
-
}
|
|
19
16
|
const pad = (value, width = 2) => String(value).padStart(width, '0');
|
|
20
17
|
/** RFC 5545 §3.1: lines of at most 75 octets, continued with a leading space. */
|
|
21
18
|
function fold(line) {
|
package/maintenance.d.ts
CHANGED
|
@@ -8,10 +8,9 @@ export interface SlotlockMaintenanceResult {
|
|
|
8
8
|
/** Series over the expansion budget, or whose roll failed otherwise; they keep their old coverage. */
|
|
9
9
|
refused: number;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* A cancelled booking's tombstone stays.
|
|
11
|
+
* Rows deleted: expired holds and their keyed answers; past retention, event idempotency answers,
|
|
12
|
+
* agent event cancellations, stray booking retirements and feed changes; and expired assistant
|
|
13
|
+
* conversations. A cancelled booking's tombstone stays.
|
|
15
14
|
*/
|
|
16
15
|
pruned: number;
|
|
17
16
|
/**
|
|
@@ -25,24 +24,31 @@ export interface SlotlockMaintenanceResult {
|
|
|
25
24
|
noShows: number;
|
|
26
25
|
/** Waitlist entries whose window came free, each now holding it (`waitlist.offered`). */
|
|
27
26
|
waitlistOffers: number;
|
|
27
|
+
/** Approvals no one decided in time, their held time freed (`approval.expired`). */
|
|
28
|
+
approvalsExpired: number;
|
|
28
29
|
/** More was left than one pass does: run another. */
|
|
29
30
|
capped: boolean;
|
|
30
31
|
/**
|
|
31
|
-
* The roll stopped before the series it had
|
|
32
|
-
*
|
|
32
|
+
* The roll stopped before the series it had deferred: others may still extend, so run another
|
|
33
|
+
* pass at once.
|
|
33
34
|
*/
|
|
34
35
|
rollPending: boolean;
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
|
-
* One maintenance pass for a tenant: roll its recurring events to the
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* transaction. Aborting `signal` stops it between batches with the signal's reason.
|
|
38
|
+
* One maintenance pass for a tenant: roll its recurring events to the horizon, then purge expired
|
|
39
|
+
* holds and prune bookkeeping past retention, each in at most `batches` batches (1-100, default
|
|
40
|
+
* 10), each its own transaction. Aborting `signal` stops it between batches.
|
|
41
41
|
*/
|
|
42
42
|
export declare function maintainSlotlockTenant(store: SlotlockStore, tenantRef: string, options?: {
|
|
43
43
|
batches?: number;
|
|
44
44
|
signal?: AbortSignal;
|
|
45
45
|
followZoneRules?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Roll the tenant's committed bookings and holds up into its usage first (usage.ts), before
|
|
48
|
+
* pruning can delete feed changes it has not counted. For a deployment that meters
|
|
49
|
+
* (SLOTLOCK_USAGE=on).
|
|
50
|
+
*/
|
|
51
|
+
usage?: boolean;
|
|
46
52
|
}): Promise<SlotlockMaintenanceResult>;
|
|
47
53
|
/**
|
|
48
54
|
* Run `work` holding the tenant's maintenance lock on a connection of its own, or return
|
|
@@ -63,12 +69,20 @@ export interface SlotlockMaintenanceSchedulerOptions {
|
|
|
63
69
|
sql: Sql;
|
|
64
70
|
/** Tenants maintained from the start, whether or not a key of theirs calls. */
|
|
65
71
|
tenants: readonly string[];
|
|
72
|
+
/**
|
|
73
|
+
* Every tenant with data (slotlock.maintenance_tenants), read at the start and once an interval;
|
|
74
|
+
* each not maintained within the interval is queued, so a tenant whose keys never call is
|
|
75
|
+
* maintained too. What it throws goes to onError.
|
|
76
|
+
*/
|
|
77
|
+
discover?: () => Promise<readonly string[]>;
|
|
66
78
|
/** How often each tenant is maintained, at most a day. */
|
|
67
79
|
intervalMs: number;
|
|
68
80
|
/** Tenants remembered; the one served longest ago is forgotten first. Default 10,000. */
|
|
69
81
|
maxTenants?: number;
|
|
70
82
|
/** Batches of each task per pass (maintainSlotlockTenant). */
|
|
71
83
|
batches?: number;
|
|
84
|
+
/** Roll each tenant's bookings and holds up into its usage, first (maintainSlotlockTenant `usage`). */
|
|
85
|
+
usage?: boolean;
|
|
72
86
|
/** A pass that ran: counts only, never the tenant. Not awaited; what it throws is ignored. */
|
|
73
87
|
onPass?: (result: SlotlockMaintenanceResult) => void;
|
|
74
88
|
/**
|
package/maintenance.js
CHANGED
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import { ignoreRejection } from './hooks.js';
|
|
2
2
|
import { SLOTLOCK_MAX_WAITLIST_PER_TENANT, calendarEventRollingHorizon, } from './store.js';
|
|
3
3
|
/**
|
|
4
|
-
* One maintenance pass for a tenant: roll its recurring events to the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* transaction. Aborting `signal` stops it between batches with the signal's reason.
|
|
4
|
+
* One maintenance pass for a tenant: roll its recurring events to the horizon, then purge expired
|
|
5
|
+
* holds and prune bookkeeping past retention, each in at most `batches` batches (1-100, default
|
|
6
|
+
* 10), each its own transaction. Aborting `signal` stops it between batches.
|
|
8
7
|
*/
|
|
9
8
|
export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
10
9
|
const batches = options.batches ?? 10;
|
|
11
10
|
if (!(Number.isSafeInteger(batches) && batches >= 1 && batches <= 100)) {
|
|
12
11
|
throw new RangeError('batches must be a whole number from 1 to 100');
|
|
13
12
|
}
|
|
14
|
-
// What a pass changes (a no-show released, a waitlist offer) is recorded as maintenance's.
|
|
15
13
|
const withTenant = (callback) => store.withTenant(tenantRef, callback, { origin: 'maintenance' });
|
|
14
|
+
if (options.usage) {
|
|
15
|
+
for (let batch = 0; batch < batches; batch++) {
|
|
16
|
+
options.signal?.throwIfAborted();
|
|
17
|
+
const read = await withTenant((tenant) => tenant.rollUpUsage({ tenantRef, limit: USAGE_ROLL_UP_BATCH }));
|
|
18
|
+
if (read < USAGE_ROLL_UP_BATCH)
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
16
22
|
const window = calendarEventRollingHorizon();
|
|
17
23
|
let extended = 0;
|
|
18
24
|
let conflicts = 0;
|
|
19
25
|
let refused = 0;
|
|
20
26
|
let rollCapped = true;
|
|
21
27
|
let after = null;
|
|
22
|
-
// A roll
|
|
23
|
-
// roll's error is thrown after.
|
|
28
|
+
// A failed roll does not stop the pruning; its error is thrown after.
|
|
24
29
|
let rollFailure = null;
|
|
25
30
|
try {
|
|
26
31
|
for (let batch = 0; batch < batches; batch++) {
|
|
@@ -65,15 +70,16 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
options.signal?.throwIfAborted();
|
|
68
|
-
// Conversations
|
|
69
|
-
// unreadable already, and deleted here.
|
|
73
|
+
// Conversations expire 2 hours after their last message (dashboard.ts).
|
|
70
74
|
pruned += await withTenant((tenant) => tenant.pruneAssistantConversations({ tenantRef }));
|
|
71
75
|
const zones = await withTenant((tenant) => tenant.followZoneRules({ tenantRef, move: options.followZoneRules === true }));
|
|
72
76
|
const staleSources = await withTenant((tenant) => tenant.reportStaleCoverage({ tenantRef }));
|
|
73
|
-
//
|
|
77
|
+
// Approvals past their wait, and no-shows, first: the time they free is offered to the waitlist
|
|
78
|
+
// in the same pass.
|
|
79
|
+
const approvals = await withTenant((tenant) => tenant.expireApprovals({ tenantRef, limit: 100 }));
|
|
74
80
|
const noShows = await withTenant((tenant) => tenant.releaseNoShows({ tenantRef }));
|
|
75
|
-
// The whole waitlist
|
|
76
|
-
//
|
|
81
|
+
// The whole waitlist in join order, a page per transaction, so a later free entry is offered
|
|
82
|
+
// however many wait before it.
|
|
77
83
|
let waitlistOffers = 0;
|
|
78
84
|
let page = null;
|
|
79
85
|
for (let pages = 0; pages < WAITLIST_PAGES; pages++) {
|
|
@@ -96,7 +102,8 @@ export async function maintainSlotlockTenant(store, tenantRef, options = {}) {
|
|
|
96
102
|
staleSources,
|
|
97
103
|
noShows,
|
|
98
104
|
waitlistOffers,
|
|
99
|
-
|
|
105
|
+
approvalsExpired: approvals.expired,
|
|
106
|
+
capped: rollCapped || eventsLeft || bookingsLeft || changesLeft || approvals.expired === 100,
|
|
100
107
|
rollPending: rollCapped && after !== null && (after.rollRetryAt ?? null) === null,
|
|
101
108
|
};
|
|
102
109
|
}
|
|
@@ -106,6 +113,8 @@ const WAITLIST_PAGES = Math.ceil(SLOTLOCK_MAX_WAITLIST_PER_TENANT / 50);
|
|
|
106
113
|
const SWEEP_INTERVAL_MS = 15 * 60_000;
|
|
107
114
|
/** Feed changes past the retention deleted per batch. */
|
|
108
115
|
const CHANGE_PRUNE_BATCH = 1_000;
|
|
116
|
+
/** Feed changes rolled up into usage per batch. */
|
|
117
|
+
const USAGE_ROLL_UP_BATCH = 5_000;
|
|
109
118
|
/** The advisory lock key for one tenant's maintenance (64-bit, from the tenant reference). */
|
|
110
119
|
const LOCK_NAMESPACE = 'slotlock.maintenance:';
|
|
111
120
|
/**
|
|
@@ -173,6 +182,7 @@ export function startSlotlockMaintenance(options) {
|
|
|
173
182
|
try {
|
|
174
183
|
const result = await withSlotlockMaintenanceLock(options.sql, tenantRef, () => maintainSlotlockTenant(options.store, tenantRef, {
|
|
175
184
|
...(options.batches !== undefined ? { batches: options.batches } : {}),
|
|
185
|
+
...(options.usage ? { usage: true } : {}),
|
|
176
186
|
signal: controller.signal,
|
|
177
187
|
}));
|
|
178
188
|
recordPass(tenantRef, Date.now());
|
|
@@ -183,12 +193,10 @@ export function startSlotlockMaintenance(options) {
|
|
|
183
193
|
catch {
|
|
184
194
|
// Reporting must never stop maintenance, or make a pass that ran look failed.
|
|
185
195
|
}
|
|
186
|
-
// A capped pass that
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
// reached. A pass that reached the series put back waits for the next interval: they
|
|
191
|
-
// take turns there, and the next pass reaches anything new before them.
|
|
196
|
+
// A capped pass that progressed, or whose roll stopped before its deferred series, goes
|
|
197
|
+
// again at once: deferred series sit behind untried ones, so each stuck one is met once
|
|
198
|
+
// before a healthy one is reached. A pass that reached the deferred series waits for
|
|
199
|
+
// the next interval.
|
|
192
200
|
const progressed = result.extended + result.pruned > 0;
|
|
193
201
|
again = result.capped && (progressed || result.rollPending);
|
|
194
202
|
}
|
|
@@ -215,20 +223,42 @@ export function startSlotlockMaintenance(options) {
|
|
|
215
223
|
working = null;
|
|
216
224
|
}
|
|
217
225
|
};
|
|
218
|
-
|
|
219
|
-
|
|
226
|
+
const report = (error) => {
|
|
227
|
+
try {
|
|
228
|
+
ignoreRejection(options.onError?.(error));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Reporting must never stop maintenance.
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
// Expired conversations of every tenant, including those no key calls (dashboard-only): one
|
|
235
|
+
// bounded sweep at most every quarter hour.
|
|
220
236
|
let lastSweep = 0;
|
|
221
237
|
const sweep = () => {
|
|
222
238
|
if (controller.signal.aborted || Date.now() - lastSweep < Math.min(intervalMs, SWEEP_INTERVAL_MS))
|
|
223
239
|
return;
|
|
224
240
|
lastSweep = Date.now();
|
|
225
|
-
options.store.sweepAssistantConversations().catch(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
241
|
+
options.store.sweepAssistantConversations().catch(report);
|
|
242
|
+
};
|
|
243
|
+
// Every tenant with data, once an interval: those no key has called since the server started too.
|
|
244
|
+
let lastDiscovery = Number.NEGATIVE_INFINITY;
|
|
245
|
+
let discovering = false;
|
|
246
|
+
const discover = () => {
|
|
247
|
+
const list = options.discover;
|
|
248
|
+
if (!list || discovering || controller.signal.aborted)
|
|
249
|
+
return;
|
|
250
|
+
if (Date.now() - lastDiscovery < intervalMs)
|
|
251
|
+
return;
|
|
252
|
+
lastDiscovery = Date.now();
|
|
253
|
+
discovering = true;
|
|
254
|
+
(async () => list())()
|
|
255
|
+
.then((tenants) => {
|
|
256
|
+
for (const tenantRef of tenants)
|
|
257
|
+
if (due(tenantRef))
|
|
258
|
+
enqueue(tenantRef);
|
|
259
|
+
}, report)
|
|
260
|
+
.finally(() => {
|
|
261
|
+
discovering = false;
|
|
232
262
|
});
|
|
233
263
|
};
|
|
234
264
|
const tick = () => {
|
|
@@ -236,6 +266,7 @@ export function startSlotlockMaintenance(options) {
|
|
|
236
266
|
if (due(tenantRef))
|
|
237
267
|
enqueue(tenantRef);
|
|
238
268
|
}
|
|
269
|
+
discover();
|
|
239
270
|
sweep();
|
|
240
271
|
};
|
|
241
272
|
const timer = setInterval(tick, Math.min(intervalMs, 60_000));
|
package/mcp-app.d.ts
CHANGED
|
@@ -5,16 +5,14 @@ export declare const SLOTLOCK_MCP_APP_RESOURCE_URI = "ui://slotlock/calendar";
|
|
|
5
5
|
/** True only when the host declares the stable extension's required HTML MIME capability. */
|
|
6
6
|
export declare function clientSupportsSlotlockMcpApp(capabilities: unknown): boolean;
|
|
7
7
|
/**
|
|
8
|
-
* A static, network-dark calendar view.
|
|
9
|
-
* `ui/notifications/tool-result` channel; the template
|
|
10
|
-
* textContent
|
|
11
|
-
*
|
|
8
|
+
* A static, network-dark calendar view. Data arrives only through the host's
|
|
9
|
+
* `ui/notifications/tool-result` channel; the template holds no tenant data and renders untrusted
|
|
10
|
+
* values with textContent. Reviewed source rather than generated UI, so hosts can inspect and cache
|
|
11
|
+
* it.
|
|
12
12
|
*
|
|
13
|
-
* A certain slot
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* where Slotlock asks a person to confirm the write if it is set to. The view itself never calls a
|
|
17
|
-
* tool, and every write tool is visible to the model only.
|
|
13
|
+
* A certain slot can be picked: the view sends one `ui/message` naming only the resource id and its
|
|
14
|
+
* two instants, so the agent books it through the normal path (with confirmation if configured).
|
|
15
|
+
* The view never calls a tool.
|
|
18
16
|
*/
|
|
19
17
|
export declare const SLOTLOCK_MCP_APP_HTML = "<!doctype html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <title>Slotlock calendar</title>\n <style>\n :root{color-scheme:light dark;--fb-bg:#fbfbf9;--fb-surface:#ffffff;--fb-sunken:#f3f4f0;--fb-text:#171915;--fb-muted:#5d655b;--fb-faint:#8a9187;--fb-line:#e3e5df;--fb-line-strong:#c9cdc4;--fb-inverse:#171915;--fb-on-inverse:#ffffff;--fb-ring:#2e5bff;--fb-ok:#23855b;--fb-warn:#b7791f;--fb-danger:#9b2c2c}\n @media (prefers-color-scheme:dark){:root:not([data-theme=\"light\"]){--fb-bg:#121411;--fb-surface:#1a1d19;--fb-sunken:#22261f;--fb-text:#eef0ea;--fb-muted:#a9b0a5;--fb-faint:#7d847a;--fb-line:#2c302a;--fb-line-strong:#3d4339;--fb-inverse:#eef0ea;--fb-on-inverse:#121411;--fb-ring:#7d9bff;--fb-ok:#5cc495;--fb-warn:#e0a44a;--fb-danger:#f08a8a}}\n :root[data-theme=\"dark\"]{--fb-bg:#121411;--fb-surface:#1a1d19;--fb-sunken:#22261f;--fb-text:#eef0ea;--fb-muted:#a9b0a5;--fb-faint:#7d847a;--fb-line:#2c302a;--fb-line-strong:#3d4339;--fb-inverse:#eef0ea;--fb-on-inverse:#121411;--fb-ring:#7d9bff;--fb-ok:#5cc495;--fb-warn:#e0a44a;--fb-danger:#f08a8a}\n :root{--bg:var(--color-background-primary,var(--fb-bg));--surface:var(--color-background-secondary,var(--fb-surface));--sunken:var(--color-background-tertiary,var(--fb-sunken));--text:var(--color-text-primary,var(--fb-text));--muted:var(--color-text-secondary,var(--fb-muted));--faint:var(--color-text-tertiary,var(--fb-faint));--line:var(--color-border-primary,var(--fb-line));--line-strong:var(--color-border-secondary,var(--fb-line-strong));--inverse:var(--color-background-inverse,var(--fb-inverse));--on-inverse:var(--color-text-inverse,var(--fb-on-inverse));--ring:var(--color-ring-primary,var(--fb-ring));--ok:var(--color-text-success,var(--fb-ok));--danger:var(--color-text-danger,var(--fb-danger));font-family:var(--font-sans,ui-sans-serif,system-ui,-apple-system,\"Segoe UI\",sans-serif);color:var(--text);background:var(--bg)}\n *{box-sizing:border-box}body{margin:0;min-width:0;background:transparent;-webkit-font-smoothing:antialiased}button,input,textarea{font:inherit;color:inherit}p{margin:0}\n button:focus-visible,input:focus-visible,textarea:focus-visible{outline:2px solid var(--ring);outline-offset:2px}\n .shell{display:grid;gap:12px;padding:16px;max-width:760px;margin:0 auto}\n .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n .status{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}.dot{width:7px;height:7px;border-radius:50%;background:var(--fb-warn)}.status[data-ready=\"true\"] .dot{background:var(--ok)}\n /* The picker */\n .picker{display:grid;border:1px solid var(--line);border-radius:16px;background:var(--surface);overflow:hidden}.picker[hidden]{display:none}\n .head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:16px 16px 12px}\n .head h1{margin:0;font-size:var(--font-heading-sm-size,18px);font-weight:var(--font-weight-semibold,600);letter-spacing:-.01em}\n .meta{margin-top:4px;font-size:13px;color:var(--muted);display:flex;flex-wrap:wrap;gap:4px 10px}.meta span+span::before{content:\"\u00B7\";margin-right:10px;color:var(--faint)}\n .clock{display:inline-flex;padding:2px;border-radius:8px;background:var(--sunken);flex:none}.clock button{min-width:40px;padding:4px 8px;border:0;border-radius:6px;background:transparent;font-size:12px;color:var(--muted);cursor:pointer}.clock button[aria-pressed=\"true\"]{background:var(--surface);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.08)}\n .days{display:flex;gap:6px;padding:0 16px 12px;overflow-x:auto;scrollbar-width:none;scroll-snap-type:x proximity}.days::-webkit-scrollbar{display:none}.days[hidden]{display:none}\n .day{flex:none;display:grid;justify-items:center;gap:1px;width:56px;padding:8px 0 7px;border:1px solid var(--line);border-radius:12px;background:transparent;cursor:pointer;scroll-snap-align:start;font-variant-numeric:tabular-nums}\n .day small{font-size:11px;color:var(--muted)}.day strong{font-size:17px;font-weight:var(--font-weight-semibold,600);line-height:1.2}.day i{font-style:normal;font-size:10px;color:var(--faint)}\n .day:hover{border-color:var(--line-strong)}.day[aria-pressed=\"true\"]{background:var(--inverse);border-color:var(--inverse);color:var(--on-inverse)}.day[aria-pressed=\"true\"] small,.day[aria-pressed=\"true\"] i{color:inherit;opacity:.75}\n .times{display:grid;gap:14px;padding:14px 16px 16px;border-top:1px solid var(--line)}.times[hidden],.choice[hidden]{display:none}.foot:has(.choice[hidden]){justify-content:flex-end}\n .part h2{margin:0 0 8px;font-size:11px;font-weight:var(--font-weight-medium,500);letter-spacing:.06em;text-transform:uppercase;color:var(--faint)}\n .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:8px}\n .time{display:grid;justify-items:center;gap:1px;min-height:42px;padding:8px 6px;border:1px solid var(--line-strong);border-radius:10px;background:var(--surface);cursor:pointer;font-variant-numeric:tabular-nums;font-weight:var(--font-weight-medium,500);transition:border-color .12s,background-color .12s}\n .time small{font-size:10px;font-weight:400;color:var(--muted)}.time:hover:not(:disabled){border-color:var(--text)}\n .time[aria-pressed=\"true\"]{background:var(--inverse);border-color:var(--inverse);color:var(--on-inverse)}.time[aria-pressed=\"true\"] small{color:inherit;opacity:.75}.time:disabled{cursor:default}.time:disabled:not([aria-pressed=\"true\"]){opacity:.45}\n .one{display:grid;gap:4px;padding:4px 16px 16px}.one strong{font-size:var(--font-heading-md-size,22px);font-weight:var(--font-weight-semibold,600);letter-spacing:-.01em;font-variant-numeric:tabular-nums}.one span{font-size:14px;color:var(--muted)}.one[hidden]{display:none}\n .foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-top:1px solid var(--line);background:var(--sunken)}\n .choice{min-width:0;font-size:13px;color:var(--muted);overflow-wrap:anywhere}.choice b{display:block;color:var(--text);font-weight:var(--font-weight-semibold,600)}\n .book{flex:none;min-height:40px;padding:9px 16px;border:0;border-radius:10px;background:var(--inverse);color:var(--on-inverse);font-weight:var(--font-weight-semibold,600);cursor:pointer}.book:disabled{cursor:default;opacity:.4}.book[data-sent=\"true\"]{opacity:1;background:transparent;color:var(--ok);padding-inline:4px}\n .fallback{display:grid;gap:6px;padding:12px 16px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}.fallback[hidden]{display:none}.fallback textarea{width:100%;min-height:64px;padding:8px;border:1px solid var(--line-strong);border-radius:8px;background:var(--bg);font:12px/1.5 var(--font-mono,ui-monospace,monospace);resize:vertical}\n /* Every other result */\n .list[hidden]{display:none}.list{display:grid;gap:12px}\n .mast{display:flex;align-items:center;justify-content:space-between;gap:12px}.mast h1{margin:0;font-size:var(--font-heading-sm-size,18px);font-weight:var(--font-weight-semibold,600)}\n .toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.search{flex:1;min-width:min(100%,220px);min-height:38px;padding:8px 12px;border:1px solid var(--line-strong);border-radius:10px;background:var(--surface)}.summary{font-size:13px;color:var(--muted)}\n .panel{min-width:0;border:1px solid var(--line);border-radius:14px;background:var(--surface);overflow:hidden}.items{display:grid;min-width:0}\n .item{display:grid;grid-template-columns:minmax(120px,.85fr) minmax(0,1.5fr) auto;gap:12px;align-items:start;padding:12px 14px;border-bottom:1px solid var(--line)}.item:last-child{border-bottom:0}.item-title{font-weight:var(--font-weight-semibold,600);overflow-wrap:anywhere}.item-detail{min-width:0;color:var(--muted);font-size:13px;line-height:1.45;overflow-wrap:anywhere}.badge{justify-self:end;padding:3px 7px;border:1px solid var(--line);border-radius:6px;font-size:11px;color:var(--muted);white-space:nowrap}.empty{padding:28px 16px;text-align:center;color:var(--muted);font-size:14px}\n details{border-top:1px solid var(--line)}summary{cursor:pointer;padding:10px 14px;color:var(--muted);font-size:12px}pre{margin:0;padding:14px;max-height:260px;overflow:auto;font:12px/1.5 var(--font-mono,ui-monospace,monospace);white-space:pre-wrap;overflow-wrap:anywhere;background:var(--sunken)}\n @media(max-width:520px){.shell{padding:10px}.item{grid-template-columns:1fr}.badge{justify-self:start}.grid{grid-template-columns:repeat(auto-fill,minmax(84px,1fr))}.foot{flex-wrap:wrap}.book{flex:1}}\n @media(prefers-reduced-motion:reduce){.time{transition:none}}\n </style>\n</head>\n<body>\n <main class=\"shell\">\n <section class=\"picker\" id=\"picker\" aria-labelledby=\"pickTitle\" hidden>\n <div class=\"head\">\n <div><h1 id=\"pickTitle\">Pick a time</h1><p class=\"meta\" id=\"pickMeta\"></p></div>\n <div class=\"clock\" role=\"group\" aria-label=\"Clock\"><button type=\"button\" id=\"h12\" aria-pressed=\"false\">12h</button><button type=\"button\" id=\"h24\" aria-pressed=\"false\">24h</button></div>\n </div>\n <div class=\"days\" id=\"days\" role=\"group\" aria-label=\"Day\"></div>\n <div class=\"one\" id=\"one\" hidden></div>\n <div class=\"times\" id=\"times\"></div>\n <div class=\"foot\">\n <p class=\"choice\" id=\"choice\" aria-live=\"polite\">No time picked</p>\n <button type=\"button\" class=\"book\" id=\"book\" disabled>Book this time</button>\n </div>\n <div class=\"fallback\" id=\"fallback\" hidden><p>Your host did not pass the choice on. Send this to your agent:</p><textarea id=\"fallbackText\" readonly></textarea></div>\n </section>\n <section class=\"list\" id=\"list\">\n <header class=\"mast\"><h1>Slotlock calendar</h1><p class=\"status\" id=\"status\" data-ready=\"false\" role=\"status\"><span class=\"dot\" aria-hidden=\"true\"></span><span id=\"statusText\">Connecting to host</span></p></header>\n <div class=\"toolbar\">\n <label class=\"sr\" for=\"filter\">Filter this result</label><input id=\"filter\" class=\"search\" type=\"search\" autocomplete=\"off\" placeholder=\"Filter by resource, event or time\">\n <p class=\"summary\" id=\"summary\" aria-live=\"polite\">Waiting for a calendar result.</p>\n </div>\n <div class=\"panel\">\n <div class=\"items\" id=\"items\"><p class=\"empty\">Ask your agent to list resources, inspect free-busy, find a slot, or list events.</p></div>\n <details><summary>Structured result</summary><pre id=\"raw\">No result yet.</pre></details>\n </div>\n </section>\n </main>\n <script>\n (() => {\n 'use strict';\n const state = { data: null, query: '', requestId: 1, nextId: 2, timeZone: undefined, locale: undefined, hour12: undefined, day: null, chosen: null, sent: false, pending: {}, size: '' };\n const $ = (id) => document.getElementById(id);\n const status = $('status'), statusText = $('statusText'), items = $('items'), summary = $('summary'), raw = $('raw'), filter = $('filter');\n const picker = $('picker'), list = $('list'), days = $('days'), times = $('times'), one = $('one'), choice = $('choice'), book = $('book'), pickMeta = $('pickMeta'), fallback = $('fallback'), fallbackText = $('fallbackText'), h12 = $('h12'), h24 = $('h24');\n const safeText = (value) => value === null || value === undefined ? '\u2014' : String(value);\n const post = (message) => window.parent.postMessage(message, '*');\n const setStatus = (text, ready) => { statusText.textContent = text; status.dataset.ready = ready ? 'true' : 'false'; };\n const clear = (node) => { while (node.firstChild) node.removeChild(node.firstChild); };\n const el = (tag, className, text) => { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; };\n\n // The host's look: its standard theme variables (only these names, as plain values) and its theme.\n const THEME_KEYS = /^--(color-(background|text|border|ring)-[a-z]+|font-(sans|mono)|font-weight-[a-z]+|font-(text|heading)-[a-z]+-size)$/;\n const applyHost = (context) => {\n if (!context || typeof context !== 'object') return;\n if (context.theme === 'light' || context.theme === 'dark') document.documentElement.dataset.theme = context.theme;\n const variables = context.styles && context.styles.variables;\n if (variables && typeof variables === 'object') {\n Object.entries(variables).forEach(([key, value]) => {\n if (THEME_KEYS.test(key) && typeof value === 'string' && value.length <= 200 && !/url\\(|expression|[;{}<>]/i.test(value)) document.documentElement.style.setProperty(key, value);\n });\n }\n if (typeof context.timeZone === 'string') state.timeZone = context.timeZone;\n if (typeof context.locale === 'string') state.locale = context.locale;\n };\n\n // Only what the view has checked goes into the message a person sends: an id of plain\n // characters and two instants it parsed itself, never a title or any other server text.\n const SAFE_ID = /^[A-Za-z0-9._:-]{1,200}$/;\n const instant = (value) => { const time = typeof value === 'string' ? Date.parse(value) : NaN; return Number.isFinite(time) ? new Date(time) : null; };\n const HOLD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n const pickable = (slot) => {\n if (!slot || typeof slot.resource_id !== 'string' || !SAFE_ID.test(slot.resource_id)) return null;\n const start = instant(slot.start); const end = instant(slot.end);\n if (!start || !end || end <= start) return null;\n if (slot.hold_id === undefined) return { resource_id: slot.resource_id, start, end };\n // A held time (slotlock_hold_slots): booking it names the hold, so it needs a plain id.\n const expires = instant(slot.expires_at);\n return typeof slot.hold_id === 'string' && HOLD_ID.test(slot.hold_id) && expires\n ? { resource_id: slot.resource_id, start, end, hold_id: slot.hold_id, expires }\n : null;\n };\n const slotsOf = (data) => {\n if (!data || typeof data !== 'object' || data.error) return [];\n // Held time is free for this agent alone until it expires: certain by being held.\n if (Array.isArray(data.holds)) return data.holds.map(pickable).filter((slot) => slot && slot.hold_id);\n if (data.coverage?.certainty !== 'certain') return [];\n const listed = Array.isArray(data.slots) ? data.slots : 'start' in data ? [data] : [];\n return listed.map(pickable).filter(Boolean);\n };\n\n const fmt = (date, options) => {\n const zone = state.timeZone ? { timeZone: state.timeZone } : {};\n try { return new Intl.DateTimeFormat(state.locale, { ...options, ...zone }).format(date); }\n catch { return new Intl.DateTimeFormat(undefined, options).format(date); }\n };\n const prefers12 = () => { try { const cycle = new Intl.DateTimeFormat(state.locale, { hour: 'numeric' }).resolvedOptions().hourCycle; return cycle === 'h11' || cycle === 'h12'; } catch { return false; } };\n const hour12 = () => state.hour12 === undefined ? prefers12() : state.hour12;\n const dayKey = (date) => fmt(date, { year: 'numeric', month: '2-digit', day: '2-digit' });\n const dayLabel = (date) => fmt(date, { weekday: 'short', day: 'numeric', month: 'short' });\n const timeLabel = (date) => fmt(date, { hour: hour12() ? 'numeric' : '2-digit', minute: '2-digit', hour12: hour12() });\n // The hour as a number in any locale: German formats it as \"10 Uhr\", so read the hour part.\n const hourOf = (date) => {\n const zone = state.timeZone ? { timeZone: state.timeZone } : {};\n let parts;\n try { parts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', hourCycle: 'h23', ...zone }).formatToParts(date); }\n catch { parts = new Intl.DateTimeFormat('en-US', { hour: 'numeric', hourCycle: 'h23' }).formatToParts(date); }\n return Number(parts.find((part) => part.type === 'hour')?.value) % 24;\n };\n const minutes = (slot) => Math.round((slot.end - slot.start) / 60000);\n const lengthLabel = (total) => total < 60 ? total + ' min' : Math.floor(total / 60) + ' h' + (total % 60 ? ' ' + (total % 60) + ' min' : '');\n const sentence = (slot) => 'Book the slot I picked: ' + (slot.hold_id ? 'hold ' + slot.hold_id + ', ' : '') + 'resource ' + slot.resource_id + ', from ' + slot.start.toISOString() + ' to ' + slot.end.toISOString() + ' (' + dayLabel(slot.start) + ', ' + timeLabel(slot.start) + ' to ' + timeLabel(slot.end) + (state.timeZone ? ' ' + state.timeZone : '') + ').';\n\n const renderPicker = (slots) => {\n const resources = [...new Set(slots.map((slot) => slot.resource_id))];\n const lengths = new Set(slots.map(minutes));\n clear(pickMeta);\n if (lengths.size === 1) pickMeta.append(el('span', '', lengthLabel(minutes(slots[0]))));\n if (resources.length > 1) pickMeta.append(el('span', '', resources.length + ' resources'));\n pickMeta.append(el('span', '', state.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'Local time'));\n const held = slots.filter((slot) => slot.expires).map((slot) => slot.expires).sort((a, b) => a - b)[0];\n if (held) pickMeta.append(el('span', 'held', 'Held until ' + timeLabel(held)));\n h12.setAttribute('aria-pressed', String(hour12())); h24.setAttribute('aria-pressed', String(!hour12()));\n const byDay = new Map();\n slots.forEach((slot) => { const key = dayKey(slot.start); if (!byDay.has(key)) byDay.set(key, []); byDay.get(key).push(slot); });\n if (!byDay.has(state.day)) state.day = byDay.keys().next().value;\n const single = slots.length === 1;\n $('pickTitle').textContent = single ? 'Next available' : 'Pick a time';\n clear(days); clear(times); clear(one);\n days.hidden = single || byDay.size < 2;\n one.hidden = !single;\n times.hidden = single;\n if (single) {\n if (!state.chosen) state.chosen = slots[0];\n one.append(el('strong', '', timeLabel(slots[0].start) + '\u2013' + timeLabel(slots[0].end)), el('span', '', fmt(slots[0].start, { weekday: 'long', day: 'numeric', month: 'long' })));\n } else {\n byDay.forEach((daySlots, key) => {\n const first = daySlots[0].start;\n const button = el('button', 'day');\n button.type = 'button';\n button.setAttribute('aria-pressed', String(key === state.day));\n button.setAttribute('aria-label', dayLabel(first) + ', ' + daySlots.length + (daySlots.length === 1 ? ' time' : ' times'));\n button.append(el('small', '', fmt(first, { weekday: 'short' })), el('strong', '', fmt(first, { day: 'numeric' })), el('i', '', fmt(first, { month: 'short' })));\n button.addEventListener('click', () => { state.day = key; render(); });\n days.append(button);\n });\n const parts = [['Morning', (h) => h < 12], ['Afternoon', (h) => h >= 12 && h < 17], ['Evening', (h) => h >= 17]];\n const daySlots = byDay.get(state.day) || [];\n parts.forEach(([name, test]) => {\n const inPart = daySlots.filter((slot) => test(hourOf(slot.start)));\n if (inPart.length === 0) return;\n const part = el('section', 'part'); const grid = el('div', 'grid');\n part.append(el('h2', '', name), grid);\n inPart.forEach((slot) => {\n const button = el('button', 'time', timeLabel(slot.start));\n button.type = 'button';\n const isChosen = state.chosen && state.chosen.resource_id === slot.resource_id && +state.chosen.start === +slot.start;\n button.setAttribute('aria-pressed', String(Boolean(isChosen)));\n button.setAttribute('aria-label', dayLabel(slot.start) + ', ' + timeLabel(slot.start) + ' to ' + timeLabel(slot.end) + (resources.length > 1 ? ', resource ' + (resources.indexOf(slot.resource_id) + 1) : ''));\n if (resources.length > 1) button.append(el('small', '', 'Resource ' + (resources.indexOf(slot.resource_id) + 1)));\n else if (lengths.size > 1) button.append(el('small', '', lengthLabel(minutes(slot))));\n button.disabled = state.sent;\n button.addEventListener('click', () => { state.chosen = slot; fallback.hidden = true; render(); });\n grid.append(button);\n });\n times.append(part);\n });\n }\n clear(choice);\n choice.hidden = single;\n if (state.chosen) {\n choice.append(el('b', '', dayLabel(state.chosen.start) + ', ' + timeLabel(state.chosen.start) + '\u2013' + timeLabel(state.chosen.end)));\n if (resources.length > 1) choice.append(document.createTextNode('Resource ' + (resources.indexOf(state.chosen.resource_id) + 1)));\n } else choice.textContent = 'No time picked';\n book.disabled = !state.chosen || state.sent;\n book.dataset.sent = String(state.sent);\n book.textContent = state.sent ? '\u2713 Sent to your agent' : 'Book this time';\n };\n book.addEventListener('click', () => {\n if (!state.chosen || state.sent) return;\n state.sent = true;\n fallback.hidden = true;\n const text = sentence(state.chosen);\n const id = state.nextId++;\n state.pending[id] = text;\n setStatus('Sent to your agent', true);\n post({ jsonrpc: '2.0', id, method: 'ui/message', params: { role: 'user', content: { type: 'text', text } } });\n render();\n });\n h12.addEventListener('click', () => { state.hour12 = true; render(); });\n h24.addEventListener('click', () => { state.hour12 = false; render(); });\n\n const reportSize = () => {\n const root = document.documentElement;\n const size = { width: Math.ceil(root.scrollWidth), height: Math.ceil(root.scrollHeight) };\n const key = size.width + 'x' + size.height;\n if (key === state.size) return;\n state.size = key;\n post({ jsonrpc: '2.0', method: 'ui/notifications/size-changed', params: size });\n };\n const intervalText = (entry) => [entry.start || entry.starts_at, entry.end || entry.ends_at].filter(Boolean).join(' \u2192 ');\n // A refused call carries no structuredContent (hosts validate it against the tool's\n // outputSchema); its text content is {\"error\":{\"code\":\u2026}}.\n const refusalOf = (content) => {\n const text = Array.isArray(content) ? content.find((part) => part && part.type === 'text')?.text : undefined;\n try {\n const parsed = JSON.parse(text);\n if (parsed && parsed.error && typeof parsed.error === 'object') return { error: parsed.error };\n } catch {}\n return { error: { code: 'request_refused' } };\n };\n const flatten = (data) => {\n if (!data || typeof data !== 'object') return [];\n if (data.error) return [{ title: data.error.code || 'Unable to load', detail: 'The calendar server refused this request.', badge: 'Error' }];\n if (Array.isArray(data.resources)) return data.resources.flatMap((resource) => {\n if (Array.isArray(resource.busy)) return resource.busy.map((busy) => ({ title: resource.resource_id, detail: intervalText(busy), badge: resource.coverage?.certainty || 'busy' }));\n return [{ title: resource.external_ref || resource.id || resource.resource_id, detail: resource.timezone || 'Calendar resource', badge: resource.coverage?.certainty || 'resource' }];\n });\n if (Array.isArray(data.slots)) {\n if (data.coverage?.certainty !== 'certain') return [{ title: 'Availability unproven', detail: 'Required calendar coverage is incomplete.', badge: data.coverage?.certainty || 'uncertain' }];\n return data.slots.length === 0 ? [{ title: 'No free time', detail: 'Nothing of that length is free in the window.', badge: 'certain' }] : data.slots.map((slot) => ({ title: slot.resource_id, detail: intervalText(slot), badge: 'free' }));\n }\n if (Array.isArray(data.holds)) return [{ title: 'Nothing held', detail: 'Every time asked for was taken.', badge: 'refused' }];\n if (Array.isArray(data.events)) return data.events.map((event) => ({ title: event.title || event.id, detail: intervalText(event), badge: event.status || event.transparency || 'event' }));\n if (data.event && typeof data.event === 'object') return [{ title: data.event.title || data.event.id, detail: intervalText(data.event), badge: data.event.status || 'event' }];\n if ('start' in data || 'end' in data) {\n const coverage = data.coverage && typeof data.coverage === 'object' ? data.coverage : null;\n const certainty = coverage?.certainty;\n const completeInterval = typeof data.start === 'string' && typeof data.end === 'string';\n if (certainty !== 'certain' || !completeInterval) {\n const reason = typeof coverage?.reason === 'string' ? coverage.reason.replaceAll('_', ' ') : 'Required calendar coverage is incomplete.';\n return [{ title: 'Availability unproven', detail: reason, badge: certainty || 'uncertain' }];\n }\n return [{ title: data.resource_id || 'Next available', detail: intervalText(data), badge: certainty }];\n }\n return Object.entries(data).slice(0, 100).map(([key, value]) => ({ title: key, detail: typeof value === 'object' ? JSON.stringify(value) : safeText(value), badge: 'value' }));\n };\n const line = (title, detail, badge) => {\n const row = el('article', 'item');\n row.append(el('div', 'item-title', safeText(title)), el('div', 'item-detail', safeText(detail)), el('div', 'badge', safeText(badge)));\n return row;\n };\n const render = () => {\n const slots = slotsOf(state.data);\n picker.hidden = slots.length === 0;\n list.hidden = slots.length > 0;\n if (slots.length > 0) { renderPicker(slots); reportSize(); return; }\n clear(items);\n const rows = flatten(state.data).filter((entry) => (entry.title + ' ' + entry.detail + ' ' + entry.badge).toLowerCase().includes(state.query));\n if (rows.length === 0) items.append(el('p', 'empty', state.data ? 'No rows match this filter.' : 'Waiting for a calendar result.'));\n else rows.forEach((entry) => items.append(line(entry.title, entry.detail, entry.badge)));\n summary.textContent = rows.length === 1 ? '1 calendar row' : rows.length + ' calendar rows';\n raw.textContent = state.data ? JSON.stringify(state.data, null, 2) : 'No result yet.';\n reportSize();\n };\n filter.addEventListener('input', () => { state.query = filter.value.trim().toLowerCase(); render(); });\n window.addEventListener('message', (event) => {\n if (event.source !== window.parent) return;\n const message = event.data;\n if (!message || message.jsonrpc !== '2.0') return;\n if (message.id === state.requestId && message.result) {\n applyHost(message.result.hostContext);\n post({ jsonrpc: '2.0', method: 'ui/notifications/initialized' });\n setStatus('Connected', true);\n render();\n return;\n }\n if (message.id in state.pending) {\n const text = state.pending[message.id];\n delete state.pending[message.id];\n if (message.error) {\n // Refused or unsupported: the person can still send it, and pick again.\n state.sent = false;\n fallbackText.value = text;\n fallback.hidden = false;\n setStatus('Not sent', false);\n render();\n }\n return;\n }\n if (message.method === 'ui/notifications/tool-result') {\n const result = message.params || {};\n state.data = result.structuredContent || (result.isError ? refusalOf(result.content) : null);\n state.chosen = null; state.sent = false; state.day = null;\n fallback.hidden = true;\n setStatus(result.isError ? 'Request refused' : 'Result current', !result.isError);\n render();\n }\n if (message.method === 'ui/notifications/host-context-changed') { applyHost(message.params); render(); }\n });\n if (typeof ResizeObserver === 'function') new ResizeObserver(reportSize).observe(document.documentElement);\n post({ jsonrpc: '2.0', id: state.requestId, method: 'ui/initialize', params: { appInfo: { name: 'slotlock-calendar', version: '1.1.0' }, appCapabilities: { availableDisplayModes: ['inline', 'fullscreen'] }, protocolVersion: '2026-01-26' } });\n })();\n </script>\n</body>\n</html>";
|
|
20
18
|
export declare const SLOTLOCK_MCP_APP_RESOURCE: Readonly<{
|
package/mcp-app.js
CHANGED
|
@@ -16,16 +16,14 @@ export function clientSupportsSlotlockMcpApp(capabilities) {
|
|
|
16
16
|
return Array.isArray(mimeTypes) && mimeTypes.includes(SLOTLOCK_MCP_APP_MIME_TYPE);
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* A static, network-dark calendar view.
|
|
20
|
-
* `ui/notifications/tool-result` channel; the template
|
|
21
|
-
* textContent
|
|
22
|
-
*
|
|
19
|
+
* A static, network-dark calendar view. Data arrives only through the host's
|
|
20
|
+
* `ui/notifications/tool-result` channel; the template holds no tenant data and renders untrusted
|
|
21
|
+
* values with textContent. Reviewed source rather than generated UI, so hosts can inspect and cache
|
|
22
|
+
* it.
|
|
23
23
|
*
|
|
24
|
-
* A certain slot
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* where Slotlock asks a person to confirm the write if it is set to. The view itself never calls a
|
|
28
|
-
* tool, and every write tool is visible to the model only.
|
|
24
|
+
* A certain slot can be picked: the view sends one `ui/message` naming only the resource id and its
|
|
25
|
+
* two instants, so the agent books it through the normal path (with confirmation if configured).
|
|
26
|
+
* The view never calls a tool.
|
|
29
27
|
*/
|
|
30
28
|
export const SLOTLOCK_MCP_APP_HTML = `<!doctype html>
|
|
31
29
|
<html lang="en">
|
package/mcp-modern.d.ts
CHANGED
|
@@ -16,11 +16,10 @@ export interface ModernMcpRefusal {
|
|
|
16
16
|
data?: Record<string, unknown>;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Validate a 2026-07-28 request's `_meta` envelope and the
|
|
20
|
-
* specification's order:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* streamable-http §Server Validation). Every refusal is HTTP 400.
|
|
19
|
+
* Validate a 2026-07-28 request's `_meta` envelope and the headers mirroring its body, in the
|
|
20
|
+
* specification's order: required `_meta` fields (-32602), `MCP-Protocol-Version` against the body
|
|
21
|
+
* (-32020), a supported revision (-32022), then `Mcp-Method` and `Mcp-Name` against the body
|
|
22
|
+
* (-32020). Every refusal is HTTP 400.
|
|
24
23
|
*/
|
|
25
24
|
export declare function checkModernMcpRequest(args: {
|
|
26
25
|
headers: Headers;
|
|
@@ -40,13 +39,13 @@ export declare function sha256Hex(value: string): string;
|
|
|
40
39
|
/** Validate and copy the HMAC keys that seal `requestState`: 1-8 keys of at least 32 bytes each. */
|
|
41
40
|
export declare function stateSecrets(secrets: readonly (string | Uint8Array)[]): Buffer[];
|
|
42
41
|
/**
|
|
43
|
-
* Seal a JSON payload for a client to
|
|
44
|
-
*
|
|
42
|
+
* Seal a JSON payload for a client to echo: `v1.<payload>.<HMAC-SHA256>`, base64url.
|
|
43
|
+
* Integrity-protected, not encrypted: never put anything the client may not see in it.
|
|
45
44
|
*/
|
|
46
45
|
export declare function sealRequestState(key: Buffer, payload: Record<string, unknown>): string;
|
|
47
46
|
/**
|
|
48
|
-
* The payload of a state this server sealed with any of `keys`, and the key
|
|
49
|
-
*
|
|
47
|
+
* The payload of a state this server sealed with any of `keys`, and the verifying key; `null`
|
|
48
|
+
* otherwise. The MAC is checked in constant time before parsing.
|
|
50
49
|
*/
|
|
51
50
|
export declare function openRequestState(keys: readonly Buffer[], state: unknown): {
|
|
52
51
|
payload: Record<string, unknown>;
|
|
@@ -69,9 +68,9 @@ export interface McpEventStream {
|
|
|
69
68
|
close(): void;
|
|
70
69
|
}
|
|
71
70
|
/**
|
|
72
|
-
* An SSE response (streamable-http §Receiving Messages):
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* An SSE response (streamable-http §Receiving Messages): uncached and unbuffered by proxies
|
|
72
|
+
* (`X-Accel-Buffering: no`). `onCancel` runs once if the reader leaves first, which is how an HTTP
|
|
73
|
+
* client cancels.
|
|
75
74
|
*/
|
|
76
75
|
export declare function openMcpEventStream(onCancel: () => void): McpEventStream;
|
|
77
76
|
/** W3C Trace Context (and Baggage) a request carried, validated; handed to the backend as is. */
|
|
@@ -81,9 +80,8 @@ export interface SlotlockTraceContext {
|
|
|
81
80
|
baggage?: string;
|
|
82
81
|
}
|
|
83
82
|
/**
|
|
84
|
-
* Trace context from
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* dropped on its own (W3C Trace Context §3.3, Baggage §3.3).
|
|
83
|
+
* Trace context from `_meta` (MCP reserves `traceparent`, `tracestate` and `baggage` there) or, if
|
|
84
|
+
* absent, the HTTP headers; never a mix. An invalid `traceparent` discards it all; an invalid
|
|
85
|
+
* `tracestate` or `baggage` is dropped alone (W3C Trace Context §3.3, Baggage §3.3).
|
|
88
86
|
*/
|
|
89
87
|
export declare function traceContextFrom(meta: Record<string, unknown> | undefined, headers: Headers): SlotlockTraceContext | undefined;
|