slotlock 0.2.0 → 0.3.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 +330 -0
- package/CONTRIBUTING.md +35 -8
- package/GOVERNANCE.md +4 -2
- package/MAINTAINERS.md +3 -2
- package/NOTICE +4 -4
- package/README.md +277 -53
- package/SECURITY.md +31 -2
- package/SPEC.md +142 -15
- package/agent-server.d.ts +12 -3
- package/agent-server.js +108 -78
- package/agent-store-backend.d.ts +6 -0
- package/agent-store-backend.js +355 -36
- package/api-keys.d.ts +11 -3
- package/api-keys.js +29 -6
- package/application-api.d.ts +14 -1
- package/application-api.js +274 -12
- package/config.d.ts +93 -0
- package/config.js +192 -0
- package/dashboard.d.ts +6 -0
- package/dashboard.js +17 -1
- package/ddl.d.ts +45 -3
- package/ddl.js +538 -12
- package/engine.js +20 -6
- package/event-schema.d.ts +59 -0
- package/event-schema.js +85 -0
- package/ical-worker-thread.js +4 -2
- package/ical-worker.d.ts +8 -5
- package/ical-worker.js +9 -2
- package/index.d.ts +5 -5
- package/index.js +3 -3
- package/maintenance.d.ts +15 -5
- package/maintenance.js +59 -27
- package/node-server.d.ts +9 -1
- package/node-server.js +7 -2
- package/openapi.json +6024 -0
- package/package.json +3 -2
- package/rate-limit.d.ts +71 -0
- package/rate-limit.js +718 -0
- package/rest-api.js +10 -0
- package/rules.js +6 -3
- package/self-host.d.ts +22 -8
- package/self-host.js +307 -34
- package/slotlock-manifest.schema.json +6 -1
- package/slotlock.manifest.json +7 -2
- package/store.d.ts +334 -24
- package/store.js +2029 -290
- package/sync.d.ts +43 -2
- package/sync.js +1515 -323
- package/timezone.d.ts +58 -3
- package/timezone.js +249 -20
- package/types.d.ts +213 -9
package/rest-api.js
CHANGED
|
@@ -156,6 +156,16 @@ function openApiOperation(route, operation, extra) {
|
|
|
156
156
|
parameters.push({ name, in: 'query', required: required.has(name), schema });
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
// An application write that keeps a retry identity also takes it as the Idempotency-Key header.
|
|
160
|
+
if (extra.tag === 'Application' && fields.idempotency_key !== undefined) {
|
|
161
|
+
parameters.push({
|
|
162
|
+
name: 'Idempotency-Key',
|
|
163
|
+
in: 'header',
|
|
164
|
+
required: false,
|
|
165
|
+
description: 'Stands in for idempotency_key, where HTTP clients put a retry identity; when both are sent they must be equal.',
|
|
166
|
+
schema: { type: 'string', minLength: 1, maxLength: 500 },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
159
169
|
if (parameters.length > 0)
|
|
160
170
|
result.parameters = parameters;
|
|
161
171
|
result.responses = {
|
package/rules.js
CHANGED
|
@@ -159,10 +159,13 @@ export function expandRules(rules, searchWindow, timezone) {
|
|
|
159
159
|
let start;
|
|
160
160
|
let end;
|
|
161
161
|
if (converter) {
|
|
162
|
-
// The stamp's Y/M/D is the LOCAL calendar date; start and end are both wall-clock.
|
|
162
|
+
// The stamp's Y/M/D is the LOCAL calendar date; start and end are both wall-clock. An
|
|
163
|
+
// edge the clocks skip stands where the skipped hour ends: moved on by the gap, as RFC 5545
|
|
164
|
+
// reads a time, [02:30, 03:30) gave nothing although 03:00-03:30 exists, and [02:00,
|
|
165
|
+
// 02:30) gave 03:00-03:30, which it never names.
|
|
163
166
|
const wallDay = Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate());
|
|
164
|
-
start = converter.wallFrameToUtc(wallDay + rule.startMinutes * MINUTE_MS);
|
|
165
|
-
end = converter.wallFrameToUtc(wallDay + (rule.startMinutes + rule.durationMinutes) * MINUTE_MS);
|
|
167
|
+
start = converter.wallFrameToUtc(wallDay + rule.startMinutes * MINUTE_MS, { gap: 'end' });
|
|
168
|
+
end = converter.wallFrameToUtc(wallDay + (rule.startMinutes + rule.durationMinutes) * MINUTE_MS, { gap: 'end' });
|
|
166
169
|
if (end.getTime() <= start.getTime())
|
|
167
170
|
continue;
|
|
168
171
|
}
|
package/self-host.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type SlotlockAgentOperation, type SlotlockAgentPrincipal, type SlotlockAgentWriteOperation } from './agent-server.js';
|
|
2
|
-
import { SlotlockConfigError, type SlotlockEnv, type SlotlockServeDashboardConfig } from './config.js';
|
|
2
|
+
import { SlotlockConfigError, type SlotlockEnv, type SlotlockLimits, type SlotlockServeDashboardConfig } from './config.js';
|
|
3
3
|
import { type SlotlockNodeServerAddress, type SlotlockNodeServerCloseResult } from './node-server.js';
|
|
4
|
+
import { type SlotlockSql, type SlotlockStore } from './store.js';
|
|
4
5
|
import type { WeeklyAvailabilityRule } from './types.js';
|
|
5
6
|
export { SlotlockConfigError, type SlotlockEnv, type SlotlockServeDashboardConfig };
|
|
6
7
|
/** Every variable the commands read. The README, `.env.example` and `server.json` document these. */
|
|
7
|
-
export declare const SLOTLOCK_ENVIRONMENT_VARIABLES: readonly ["DATABASE_URL", "SLOTLOCK_MIGRATE_DATABASE_URL", "SLOTLOCK_AUTH_TOKEN", "SLOTLOCK_CONFIRMATION_SECRET", "SLOTLOCK_CONFIRM_WRITES", "SLOTLOCK_PUBLIC_URL", "SLOTLOCK_TENANT", "SLOTLOCK_AVAILABILITY", "SLOTLOCK_REST_API", "SLOTLOCK_GITHUB_CLIENT_ID", "SLOTLOCK_GITHUB_CLIENT_SECRET", "SLOTLOCK_SESSION_SECRET", "SLOTLOCK_DASHBOARD_USERS", "HOST", "PORT"];
|
|
8
|
+
export declare const SLOTLOCK_ENVIRONMENT_VARIABLES: readonly ["DATABASE_URL", "SLOTLOCK_MIGRATE_DATABASE_URL", "SLOTLOCK_AUTH_TOKEN", "SLOTLOCK_CONFIRMATION_SECRET", "SLOTLOCK_CONFIRM_WRITES", "SLOTLOCK_PUBLIC_URL", "SLOTLOCK_TENANT", "SLOTLOCK_AVAILABILITY", "SLOTLOCK_REST_API", "SLOTLOCK_LIMITS", "SLOTLOCK_TRUSTED_PROXY_HEADER", "SLOTLOCK_GITHUB_CLIENT_ID", "SLOTLOCK_GITHUB_CLIENT_SECRET", "SLOTLOCK_SESSION_SECRET", "SLOTLOCK_DASHBOARD_USERS", "HOST", "PORT"];
|
|
8
9
|
export interface SlotlockDatabaseConfig {
|
|
9
10
|
/** The role `serve` and `resource` connect as. */
|
|
10
11
|
databaseUrl: string;
|
|
@@ -21,11 +22,14 @@ export interface SlotlockServeConfig extends SlotlockDatabaseConfig {
|
|
|
21
22
|
/** Where clients reach the server; Slotlock publishes its endpoints under this URL. */
|
|
22
23
|
publicUrl: string;
|
|
23
24
|
/**
|
|
24
|
-
* A bearer token for SLOTLOCK_TENANT with
|
|
25
|
-
* authenticate whether or not it is set, each as its own tenant and scopes.
|
|
25
|
+
* A bearer token for SLOTLOCK_TENANT with read and write, never admin. Optional: API keys
|
|
26
|
+
* (`slotlock key create`) authenticate whether or not it is set, each as its own tenant and scopes.
|
|
26
27
|
*/
|
|
27
28
|
authToken?: string;
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Writes that wait for a person's confirmation (MCP 2026-07-28) and are refused elsewhere. None
|
|
31
|
+
* unless SLOTLOCK_CONFIRM_WRITES opts in: every authenticated write takes effect at once.
|
|
32
|
+
*/
|
|
29
33
|
confirmWrites: readonly SlotlockAgentWriteOperation[];
|
|
30
34
|
/** Set exactly when `confirmWrites` is not empty. */
|
|
31
35
|
confirmationSecret?: string;
|
|
@@ -38,6 +42,10 @@ export interface SlotlockServeConfig extends SlotlockDatabaseConfig {
|
|
|
38
42
|
* (SLOTLOCK_REST_API: on unless off).
|
|
39
43
|
*/
|
|
40
44
|
restApi: boolean;
|
|
45
|
+
/** Request limits (SLOTLOCK_LIMITS over SLOTLOCK_DEFAULT_LIMITS): the server's only rate limit. */
|
|
46
|
+
limits: SlotlockLimits;
|
|
47
|
+
/** The lower-case request field a trusted proxy sets to the client address; else the socket's. */
|
|
48
|
+
trustedProxyHeader?: string;
|
|
41
49
|
}
|
|
42
50
|
export declare function readSlotlockDatabaseConfig(env: SlotlockEnv): SlotlockDatabaseConfig;
|
|
43
51
|
export declare function readSlotlockServeConfig(env: SlotlockEnv): SlotlockServeConfig;
|
|
@@ -54,8 +62,8 @@ export declare function slotlockHealthcheckUrl(env: SlotlockEnv): string;
|
|
|
54
62
|
export declare function createSlotlockTokenAuthenticator(token: string, principal: SlotlockAgentPrincipal): (request: Request) => Promise<SlotlockAgentPrincipal | null>;
|
|
55
63
|
/**
|
|
56
64
|
* What `serve` lets a principal call: a served operation its scopes cover. The server token's
|
|
57
|
-
* principal carries
|
|
58
|
-
* agent server alone would leave it to this function.
|
|
65
|
+
* principal carries read and write, never admin; a principal without scopes is refused everything
|
|
66
|
+
* here, though the agent server alone would leave it to this function.
|
|
59
67
|
*/
|
|
60
68
|
export declare function authorizeSlotlockServeOperation(args: {
|
|
61
69
|
principal: SlotlockAgentPrincipal;
|
|
@@ -73,6 +81,7 @@ export interface SlotlockCliIo {
|
|
|
73
81
|
}
|
|
74
82
|
export interface SlotlockLogger {
|
|
75
83
|
info(event: string, fields?: Record<string, unknown>): void;
|
|
84
|
+
warn(event: string, fields?: Record<string, unknown>): void;
|
|
76
85
|
error(event: string, fields?: Record<string, unknown>): void;
|
|
77
86
|
}
|
|
78
87
|
/**
|
|
@@ -85,9 +94,14 @@ export interface SlotlockRunningServer {
|
|
|
85
94
|
/** End open subscriptions, drain in-flight requests, then close the database pool. */
|
|
86
95
|
close(): Promise<SlotlockNodeServerCloseResult>;
|
|
87
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The store `serve` answers requests from. Every surface writes through it, so its tenant limits
|
|
99
|
+
* bound what any key can add, and its transaction limits what one tenant holds of the shared pool.
|
|
100
|
+
*/
|
|
101
|
+
export declare function createSlotlockServeStore(sql: SlotlockSql, caps: SlotlockLimits['caps']): SlotlockStore;
|
|
88
102
|
/** Serve MCP and A2A over HTTP from the Postgres store, as one token-authenticated tenant. */
|
|
89
103
|
export declare function startSlotlockServer(config: SlotlockServeConfig, log: SlotlockLogger): Promise<SlotlockRunningServer>;
|
|
90
|
-
export declare const SLOTLOCK_CLI_HELP = "Usage: slotlock <command> [options]\n\nCommands:\n migrate Apply the schema and row-level security, then grant the serving\n role when SLOTLOCK_MIGRATE_DATABASE_URL names another role\n serve [--migrate] Serve MCP, A2A and REST over HTTP (--migrate runs migrate first);\n maintains each tenant it serves every hour\n maintain Run maintenance for the tenant now: extend recurring events to the\n rolling horizon and prune past the retention window (for cron)\n resource add <ref> [--timezone <IANA zone>]\n Create a calendar resource (a car, room, person or machine)\n resource list Print the tenant's resources, one JSON object per line\n hours show [--resource <ref>]\n Print each resource's bookable hours (null: the server's)\n hours set <json> (--resource <ref> | --all)\n Give one resource, or every one, weekly hours of its own; [] closes\n hours clear (--resource <ref> | --all)\n Hand one resource, or every one, back to the server's hours\n key create <name> [--scope read,write,admin] [--expires-in-days <n>]\n Create an API key for the tenant (default scope read,write, no\n expiry; admin adds the application API) and print it once; only its\n SHA-256 is stored\n key list Print the tenant's API keys, one JSON object per line\n key rotate <id> Give an active key a new secret and print it once; the old one stops\n working at once, and the key keeps its id and what it booked\n key revoke <id> Revoke one of the tenant's API keys at once\n healthcheck Exit 0 when the local server reports ready\n\nOptions:\n -h, --help Show this help\n -v, --version Print the version\n\nEnvironment:\n DATABASE_URL postgres:// URL of the role Slotlock serves as (required); serve\n refuses a superuser or a role with BYPASSRLS\n SLOTLOCK_MIGRATE_DATABASE_URL postgres:// URL of the schema owner, for migrate (optional)\n SLOTLOCK_AUTH_TOKEN Optional bearer token for SLOTLOCK_TENANT with
|
|
104
|
+
export declare const SLOTLOCK_CLI_HELP = "Usage: slotlock <command> [options]\n\nCommands:\n migrate Apply the schema and row-level security, then grant the serving\n role when SLOTLOCK_MIGRATE_DATABASE_URL names another role\n serve [--migrate] Serve MCP, A2A and REST over HTTP (--migrate runs migrate first);\n maintains each tenant it serves every hour\n maintain Run maintenance for the tenant now: extend recurring events to the\n rolling horizon and prune past the retention window (for cron)\n resource add <ref> [--timezone <IANA zone>]\n Create a calendar resource (a car, room, person or machine)\n resource list Print the tenant's resources, one JSON object per line\n resource archive <ref> Take a resource out of use: no new bookings, holds or events, and\n no longer listed; what is on it stays\n resource restore <ref> Put an archived resource back in use\n resource delete <ref> Delete a resource and its history; refused while a booking, hold\n or event on it has not ended\n hours show [--resource <ref>]\n Print each resource's bookable hours (null: the server's)\n hours set <json> (--resource <ref> | --all)\n Give one resource, or every one, weekly hours of its own; [] closes\n hours clear (--resource <ref> | --all)\n Hand one resource, or every one, back to the server's hours\n key create <name> [--scope read,write,admin] [--expires-in-days <n>]\n Create an API key for the tenant (default scope read,write, no\n expiry; admin adds the application API) and print it once; only its\n SHA-256 is stored\n key list Print the tenant's API keys, one JSON object per line\n key rotate <id> Give an active key a new secret and print it once; the old one stops\n working at once, and the key keeps its id and what it booked\n key revoke <id> Revoke one of the tenant's API keys at once\n tenant status Print whether the tenant is suspended\n tenant suspend [--reason <text>]\n Suspend the tenant: it may read, and writes nothing; its keys get\n no new secret (as SLOTLOCK_MIGRATE_DATABASE_URL's role when set:\n only the schema owner suspends)\n tenant unsuspend Lift the tenant's suspension (as the schema owner, likewise)\n tenant erase --confirm <tenant>\n Delete everything the tenant has, its API keys included, for good;\n --confirm repeats SLOTLOCK_TENANT\n healthcheck Exit 0 when the local server reports ready\n\nOptions:\n -h, --help Show this help\n -v, --version Print the version\n\nEnvironment:\n DATABASE_URL postgres:// URL of the role Slotlock serves as (required); serve\n refuses a superuser or a role with BYPASSRLS\n SLOTLOCK_MIGRATE_DATABASE_URL postgres:// URL of the schema owner, for migrate (optional)\n SLOTLOCK_AUTH_TOKEN Optional bearer token for SLOTLOCK_TENANT with read and write\n access (never admin), 32+ random characters (serve); API keys\n work with or without it\n SLOTLOCK_CONFIRMATION_SECRET Signs pending write confirmations, 32+ random characters (serve;\n required when SLOTLOCK_CONFIRM_WRITES is not none)\n SLOTLOCK_CONFIRM_WRITES none (default), all, or a comma-separated list of write tools\n that wait for a person (MCP 2026-07-28 form elicitation; refused\n with confirmation_required on REST, A2A and older MCP)\n SLOTLOCK_PUBLIC_URL URL clients use (default http://localhost:$PORT)\n SLOTLOCK_TENANT Tenant of the token, resource and key commands (default \"default\")\n SLOTLOCK_AVAILABILITY JSON array of weekly bookable-hours rules (default: none)\n SLOTLOCK_REST_API on (default): also serve the tools as a REST API under /v1, and\n the application API for admin keys, described at /openapi.json;\n off: MCP and A2A only\n SLOTLOCK_LIMITS Request limits, comma-separated name=value over the defaults:\n rates as <count>/<s|m|h>[:<burst>] or off for address (50/s:100,\n every request per client, before any key lookup), key_read,\n tenant_read (20/s:40), key_write, tenant_write (5/s:20),\n global_read (500/s:1000), global_write (50/s:100), app_read\n (20/s:40) and app_write (5/s:20) per tenant for the\n application API, sign_in (20/m:20); counts for\n tenant_series_writes and series_writes\n (1: writes that may expand a recurring series at once),\n maintenance_runs (1: application API maintenance runs at once),\n subscriptions (256), tenant_subscriptions (16),\n key_subscriptions (4, and fewer than tenant_subscriptions),\n key_lookups (2), key_lookup_queue (256),\n tenant_transactions (2: one tenant's database transactions at\n once, of the pool's 10); what one tenant may store:\n tenant_holds (10000), tenant_reservations, tenant_events and\n tenant_coverage (100000)\n SLOTLOCK_TRUSTED_PROXY_HEADER The request field a proxy in front sets to the client address,\n such as CF-Connecting-IP (default: the connection's address);\n set it only when every request comes through that proxy\n SLOTLOCK_GITHUB_CLIENT_ID Serve the dashboard at $SLOTLOCK_PUBLIC_URL/dashboard: the Client ID\n of a GitHub OAuth app with callback .../dashboard/callback\n SLOTLOCK_GITHUB_CLIENT_SECRET That app's client secret (dashboard)\n SLOTLOCK_SESSION_SECRET Signs dashboard sessions, 32+ random characters (dashboard)\n SLOTLOCK_DASHBOARD_USERS * or comma-separated GitHub user ids that may sign in (dashboard)\n HOST Listen address (default 127.0.0.1)\n PORT Listen port (default 8080)\n\nGenerate each secret with: openssl rand -hex 32\n";
|
|
91
105
|
/**
|
|
92
106
|
* Run one `slotlock` command and resolve to its exit code: 0 success, 1 failure, 2 a usage or
|
|
93
107
|
* configuration error (nothing was started). `serve` resolves once `io.signal` aborts and it has
|