slotlock 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +13 -6
- package/agent-server.js +1 -5
- package/agent-store-backend.d.ts +5 -6
- package/agent-store-backend.js +41 -67
- package/api-keys.js +1 -1
- package/application-api.d.ts +3 -3
- package/application-api.js +16 -24
- package/assistant.d.ts +18 -27
- package/assistant.js +36 -53
- 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 +17 -2
- package/config.js +44 -6
- package/dashboard.js +13 -0
- package/ddl.d.ts +20 -27
- package/ddl.js +166 -108
- package/engine.d.ts +6 -9
- package/engine.js +5 -9
- package/event-feed.d.ts +23 -31
- package/event-feed.js +30 -45
- 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/interop.js +1 -4
- package/maintenance.d.ts +14 -10
- package/maintenance.js +43 -26
- 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.js +0 -1
- package/node-server.d.ts +9 -9
- package/node-server.js +25 -34
- package/package-version.js +2 -4
- package/package.json +1 -1
- package/rate-limit.d.ts +6 -1
- package/rate-limit.js +24 -8
- package/rest-api.d.ts +13 -14
- package/rest-api.js +11 -12
- package/rules.d.ts +6 -7
- package/rules.js +30 -42
- package/self-host.d.ts +16 -2
- package/self-host.js +42 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +109 -161
- package/store.js +253 -371
- package/sync.d.ts +44 -59
- package/sync.js +325 -469
- package/timezone.d.ts +25 -33
- package/timezone.js +37 -52
- package/types.d.ts +35 -56
- 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/self-host.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createSlotlockAgentServer, slotlockAgentOperationScope, } from './agent
|
|
|
11
11
|
import { createSlotlockStoreAgentBackend, createSlotlockStoreApplicationBackend, createSlotlockStoreChangeSource, } from './agent-store-backend.js';
|
|
12
12
|
import { SLOTLOCK_API_KEY_DEFAULT_SCOPES, SLOTLOCK_API_KEY_MAX_LIFETIME_DAYS, SLOTLOCK_API_KEY_SCOPES, createSlotlockApiKeyAuthenticator, createSlotlockApiKeyStore, slotlockBearerCredential, } from './api-keys.js';
|
|
13
13
|
import { createSlotlockDashboard, createSlotlockDashboardResources, createSlotlockAcceptanceRecords, createSlotlockDashboardState, } from './dashboard.js';
|
|
14
|
-
import { SECRET_HINT, SlotlockConfigError, formatSlotlockRate, optional, readAvailability, readDashboardConfig, readDashboardOrganizations, readLimits, readSecret, readServerUrl, readTrustedProxyHeader, slotlockKeySubscriptions, } from './config.js';
|
|
14
|
+
import { SECRET_HINT, SlotlockConfigError, formatSlotlockRate, optional, readAvailability, readDashboardConfig, readDashboardOrganizations, readDashboardUsers, readLimits, readSecret, readServerUrl, readTrustedProxyHeader, readTrustedProxySecret, slotlockKeySubscriptions, } from './config.js';
|
|
15
15
|
import { parseBookingRules } from './booking-rules.js';
|
|
16
16
|
import { SLOTLOCK_APPLICATION_SCOPE } from './application-api.js';
|
|
17
17
|
import { SLOTLOCK_DASHBOARD_FUNCTIONS, SLOTLOCK_TENANT_TABLES } from './ddl.js';
|
|
@@ -40,6 +40,7 @@ export const SLOTLOCK_ENVIRONMENT_VARIABLES = Object.freeze([
|
|
|
40
40
|
'SLOTLOCK_SHARED_LIMITS',
|
|
41
41
|
'SLOTLOCK_LIMITS',
|
|
42
42
|
'SLOTLOCK_TRUSTED_PROXY_HEADER',
|
|
43
|
+
'SLOTLOCK_TRUSTED_PROXY_SECRET',
|
|
43
44
|
'SLOTLOCK_GITHUB_CLIENT_ID',
|
|
44
45
|
'SLOTLOCK_GITHUB_CLIENT_SECRET',
|
|
45
46
|
'SLOTLOCK_SESSION_SECRET',
|
|
@@ -64,7 +65,7 @@ const DAY_MS = 86_400_000;
|
|
|
64
65
|
* every GitHub user gives each a tenant, so the cap keeps one from filling the database.
|
|
65
66
|
*/
|
|
66
67
|
const SERVE_MAX_TENANT_RESOURCES = 10_000;
|
|
67
|
-
/** How often `serve` maintains each tenant
|
|
68
|
+
/** How often `serve` maintains each tenant with data (README, Operations: at least daily). */
|
|
68
69
|
const SERVE_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1_000;
|
|
69
70
|
/** Passes `slotlock maintain` runs at most, each as big as `serve`'s, until nothing is left. */
|
|
70
71
|
const MAINTAIN_MAX_PASSES = 100;
|
|
@@ -238,6 +239,9 @@ export function readSlotlockServeConfig(env) {
|
|
|
238
239
|
const trustedProxyHeader = readTrustedProxyHeader(env);
|
|
239
240
|
if (trustedProxyHeader !== undefined)
|
|
240
241
|
config.trustedProxyHeader = trustedProxyHeader;
|
|
242
|
+
const trustedProxySecret = readTrustedProxySecret(env, trustedProxyHeader);
|
|
243
|
+
if (trustedProxySecret !== undefined)
|
|
244
|
+
config.trustedProxySecret = trustedProxySecret;
|
|
241
245
|
if (authToken !== undefined)
|
|
242
246
|
config.authToken = authToken;
|
|
243
247
|
const metricsToken = readSecret(env, 'SLOTLOCK_METRICS_TOKEN');
|
|
@@ -260,6 +264,9 @@ export function readSlotlockServeConfig(env) {
|
|
|
260
264
|
const organizations = readDashboardOrganizations(env);
|
|
261
265
|
if (organizations !== undefined)
|
|
262
266
|
config.organizations = organizations;
|
|
267
|
+
const allowedUsers = dashboard?.allowedUsers ?? readDashboardUsers(env);
|
|
268
|
+
if (allowedUsers !== undefined)
|
|
269
|
+
config.allowedUsers = allowedUsers;
|
|
263
270
|
if (confirmWrites.length > 0 && confirmationSecret !== undefined) {
|
|
264
271
|
config.confirmationSecret = confirmationSecret;
|
|
265
272
|
}
|
|
@@ -313,6 +320,7 @@ function secretsIn(env) {
|
|
|
313
320
|
'SLOTLOCK_SESSION_SECRET',
|
|
314
321
|
'SLOTLOCK_ASSISTANT_SECRET',
|
|
315
322
|
'SLOTLOCK_METRICS_TOKEN',
|
|
323
|
+
'SLOTLOCK_TRUSTED_PROXY_SECRET',
|
|
316
324
|
]) {
|
|
317
325
|
const value = env[name]?.trim();
|
|
318
326
|
if (value)
|
|
@@ -499,16 +507,16 @@ function serveMetrics(target, metrics, path, token) {
|
|
|
499
507
|
};
|
|
500
508
|
}
|
|
501
509
|
/**
|
|
502
|
-
* Whether the dashboard's allowlists let a tenant act
|
|
503
|
-
* SLOTLOCK_DASHBOARD_ORGS lists that organization
|
|
504
|
-
* tenant
|
|
505
|
-
*
|
|
510
|
+
* Whether the dashboard's allowlists let a tenant act, wherever the dashboard runs: a
|
|
511
|
+
* `github-org:<id>` tenant only while SLOTLOCK_DASHBOARD_ORGS lists that organization; a
|
|
512
|
+
* `github:<id>` tenant only while SLOTLOCK_DASHBOARD_USERS is `*` or lists that id; every tenant when
|
|
513
|
+
* SLOTLOCK_DASHBOARD_USERS is not set, and every other tenant.
|
|
506
514
|
*/
|
|
507
|
-
function
|
|
515
|
+
export function slotlockServeAllowsTenant(config, tenantRef) {
|
|
508
516
|
const orgId = /^github-org:(\d+)$/.exec(tenantRef)?.[1];
|
|
509
517
|
if (orgId !== undefined)
|
|
510
518
|
return config.organizations?.includes(orgId) === true;
|
|
511
|
-
const users = config.
|
|
519
|
+
const users = config.allowedUsers;
|
|
512
520
|
if (users === undefined || users === '*')
|
|
513
521
|
return true;
|
|
514
522
|
const githubId = /^github:(\d+)$/.exec(tenantRef)?.[1];
|
|
@@ -550,6 +558,16 @@ export function createSlotlockMaintenanceStore(sql, caps, options = {}) {
|
|
|
550
558
|
maxHeldHoursPerHolder: caps.holder_held_hours,
|
|
551
559
|
});
|
|
552
560
|
}
|
|
561
|
+
/** Every tenant a paged SECURITY DEFINER list (1,000 names a page, after the last) returns. */
|
|
562
|
+
async function pageTenants(page) {
|
|
563
|
+
const tenants = [];
|
|
564
|
+
for (;;) {
|
|
565
|
+
const rows = await page(tenants.at(-1) ?? null);
|
|
566
|
+
tenants.push(...rows.map(({ tenant }) => tenant));
|
|
567
|
+
if (rows.length < 1_000)
|
|
568
|
+
return tenants;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
553
571
|
/** Serve MCP and A2A over HTTP from the Postgres store, as one token-authenticated tenant. */
|
|
554
572
|
export async function startSlotlockServer(config, log) {
|
|
555
573
|
const sql = connect(config.databaseUrl, POOL_SIZE);
|
|
@@ -666,13 +684,16 @@ export async function startSlotlockServer(config, log) {
|
|
|
666
684
|
...(config.trustedProxyHeader !== undefined
|
|
667
685
|
? { trustedProxyHeader: config.trustedProxyHeader }
|
|
668
686
|
: {}),
|
|
687
|
+
...(config.trustedProxySecret !== undefined
|
|
688
|
+
? { trustedProxySecret: config.trustedProxySecret }
|
|
689
|
+
: {}),
|
|
669
690
|
basePath,
|
|
670
691
|
signInPaths: config.dashboard ? ['/dashboard/sign-in', '/dashboard/callback'] : [],
|
|
671
692
|
// The token is compared in constant time first; only then is a key looked up. A person the
|
|
672
693
|
// dashboard no longer allows loses their keys with their session.
|
|
673
694
|
authenticate: async (request) => {
|
|
674
695
|
const principal = (await authenticateToken?.(request)) ?? (await authenticateKey(request));
|
|
675
|
-
if (!principal || !
|
|
696
|
+
if (!principal || !slotlockServeAllowsTenant(config, principal.tenantRef))
|
|
676
697
|
return null;
|
|
677
698
|
maintenance?.touch(principal.tenantRef);
|
|
678
699
|
return principal;
|
|
@@ -811,6 +832,10 @@ export async function startSlotlockServer(config, log) {
|
|
|
811
832
|
store: createSlotlockMaintenanceStore(sql, caps, retention),
|
|
812
833
|
sql,
|
|
813
834
|
tenants: [config.tenantRef],
|
|
835
|
+
// Every tenant with data, from the database: one whose keys have not called since this server
|
|
836
|
+
// started is maintained too. Names only; each pass runs in its tenant's own context.
|
|
837
|
+
discover: () => pageTenants((after) => sql `
|
|
838
|
+
SELECT slotlock.maintenance_tenants(${after}::text) AS tenant`),
|
|
814
839
|
intervalMs: SERVE_MAINTENANCE_INTERVAL_MS,
|
|
815
840
|
// Counts only, never the tenant.
|
|
816
841
|
onPass: (result) => {
|
|
@@ -824,16 +849,8 @@ export async function startSlotlockServer(config, log) {
|
|
|
824
849
|
const send = createSlotlockWebhookSender({ allowPrivateNetworks: config.webhooks === 'private' });
|
|
825
850
|
webhooks = startSlotlockWebhookDelivery({
|
|
826
851
|
// The tenants with a webhook, from the database: a restarted server finds them all.
|
|
827
|
-
tenants:
|
|
828
|
-
|
|
829
|
-
for (;;) {
|
|
830
|
-
const page = await sql `
|
|
831
|
-
SELECT slotlock.webhook_tenants(${tenants.at(-1) ?? null}::text) AS tenant`;
|
|
832
|
-
tenants.push(...page.map(({ tenant }) => tenant));
|
|
833
|
-
if (page.length < 1_000)
|
|
834
|
-
return tenants;
|
|
835
|
-
}
|
|
836
|
-
},
|
|
852
|
+
tenants: () => pageTenants((after) => sql `
|
|
853
|
+
SELECT slotlock.webhook_tenants(${after}::text) AS tenant`),
|
|
837
854
|
deliver: (tenantRef) => deliverSlotlockWebhooks({
|
|
838
855
|
tenantRef,
|
|
839
856
|
send,
|
|
@@ -873,6 +890,7 @@ export async function startSlotlockServer(config, log) {
|
|
|
873
890
|
key_subscriptions: slotlockKeySubscriptions(caps),
|
|
874
891
|
},
|
|
875
892
|
client_address: config.trustedProxyHeader ?? 'socket',
|
|
893
|
+
client_address_proof: config.trustedProxySecret !== undefined,
|
|
876
894
|
});
|
|
877
895
|
return {
|
|
878
896
|
address,
|
|
@@ -1226,7 +1244,7 @@ Commands:
|
|
|
1226
1244
|
migrate Apply the schema and row-level security, then grant the serving
|
|
1227
1245
|
role when SLOTLOCK_MIGRATE_DATABASE_URL names another role
|
|
1228
1246
|
serve [--migrate] Serve MCP, A2A and REST over HTTP (--migrate runs migrate first);
|
|
1229
|
-
maintains
|
|
1247
|
+
maintains every tenant with data every hour
|
|
1230
1248
|
maintain Run maintenance for the tenant now: extend recurring events to the
|
|
1231
1249
|
rolling horizon and prune past the retention window (for cron)
|
|
1232
1250
|
resource add <ref> [--timezone <IANA zone>]
|
|
@@ -1321,6 +1339,9 @@ Environment:
|
|
|
1321
1339
|
SLOTLOCK_TRUSTED_PROXY_HEADER The request field a proxy in front sets to the client address,
|
|
1322
1340
|
such as CF-Connecting-IP (default: the connection's address);
|
|
1323
1341
|
set it only when every request comes through that proxy
|
|
1342
|
+
SLOTLOCK_TRUSTED_PROXY_SECRET A secret the proxy adds to every request in x-slotlock-proxy-proof,
|
|
1343
|
+
32+ random characters: without it there, the field above is
|
|
1344
|
+
ignored and the connection's address counts
|
|
1324
1345
|
SLOTLOCK_GITHUB_CLIENT_ID Serve the dashboard at $SLOTLOCK_PUBLIC_URL/dashboard: the Client ID
|
|
1325
1346
|
of a GitHub OAuth app with callback .../dashboard/callback
|
|
1326
1347
|
SLOTLOCK_GITHUB_CLIENT_SECRET That app's client secret (dashboard)
|
|
@@ -1332,7 +1353,7 @@ Environment:
|
|
|
1332
1353
|
resolves to, a loopback, private, link-local, CGNAT, unique-local
|
|
1333
1354
|
or metadata address; on: allow them, for a self-hosted model
|
|
1334
1355
|
(Ollama, vLLM) on this machine or network
|
|
1335
|
-
SLOTLOCK_DASHBOARD_USERS * or comma-separated GitHub user ids that may sign in
|
|
1356
|
+
SLOTLOCK_DASHBOARD_USERS * or comma-separated GitHub user ids that may sign in and whose keys work
|
|
1336
1357
|
SLOTLOCK_DASHBOARD_ORGS comma-separated GitHub organization ids: each is one shared tenant
|
|
1337
1358
|
SLOTLOCK_DASHBOARD_NAME What the dashboard calls this deployment (default Slotlock)
|
|
1338
1359
|
HOST Listen address (default 127.0.0.1)
|
package/slotlock.manifest.json
CHANGED