slotlock 0.6.1 → 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 +34 -0
- package/README.md +13 -6
- package/SECURITY.md +15 -4
- 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 +14 -1
- 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/config.js
CHANGED
|
@@ -97,15 +97,32 @@ export function readAvailability(env) {
|
|
|
97
97
|
* (bidi, zero-width) or line separator character, `<` or `>`.
|
|
98
98
|
*/
|
|
99
99
|
export const SLOTLOCK_DASHBOARD_NAME_PATTERN = /^[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}<>]{1,60}$/u;
|
|
100
|
+
/**
|
|
101
|
+
* Setting any of these means serving the dashboard. SLOTLOCK_DASHBOARD_USERS is not one: alone, it
|
|
102
|
+
* names whose keys a server takes when the dashboard runs elsewhere (readDashboardUsers).
|
|
103
|
+
*/
|
|
100
104
|
const DASHBOARD_VARIABLES = [
|
|
101
105
|
'SLOTLOCK_GITHUB_CLIENT_ID',
|
|
102
106
|
'SLOTLOCK_GITHUB_CLIENT_SECRET',
|
|
103
107
|
'SLOTLOCK_SESSION_SECRET',
|
|
104
|
-
'SLOTLOCK_DASHBOARD_USERS',
|
|
105
108
|
];
|
|
106
109
|
/**
|
|
107
|
-
*
|
|
108
|
-
* dashboard
|
|
110
|
+
* SLOTLOCK_DASHBOARD_USERS: `'*'`, GitHub user ids, or undefined when it is not set. A server whose
|
|
111
|
+
* dashboard runs elsewhere (a Worker) reads it too, so that removing a person there stops their keys.
|
|
112
|
+
*/
|
|
113
|
+
export function readDashboardUsers(env) {
|
|
114
|
+
const users = optional(env, 'SLOTLOCK_DASHBOARD_USERS')?.replaceAll(' ', '');
|
|
115
|
+
if (users === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (users !== '*' && !/^[1-9]\d{0,19}(,[1-9]\d{0,19})*$/.test(users)) {
|
|
118
|
+
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_USERS must be * for every GitHub account, or a comma-separated list of GitHub user ids (gh api users/<login> --jq .id)');
|
|
119
|
+
}
|
|
120
|
+
return users === '*' ? '*' : [...new Set(users.split(','))];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The dashboard's settings, or undefined when none of DASHBOARD_VARIABLES is set. Setting any of
|
|
124
|
+
* them means serving the dashboard, so the rest, and SLOTLOCK_DASHBOARD_USERS, are then required: a
|
|
125
|
+
* half-configured sign-in is a startup error.
|
|
109
126
|
*/
|
|
110
127
|
export function readDashboardConfig(env) {
|
|
111
128
|
if (DASHBOARD_VARIABLES.every((name) => optional(env, name) === undefined))
|
|
@@ -130,8 +147,14 @@ export function readDashboardConfig(env) {
|
|
|
130
147
|
if (others.includes(sessionSecret)) {
|
|
131
148
|
throw new SlotlockConfigError('SLOTLOCK_SESSION_SECRET must differ from every other secret');
|
|
132
149
|
}
|
|
133
|
-
|
|
134
|
-
|
|
150
|
+
let users;
|
|
151
|
+
try {
|
|
152
|
+
users = readDashboardUsers(env);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
users = undefined;
|
|
156
|
+
}
|
|
157
|
+
if (users === undefined) {
|
|
135
158
|
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_USERS is required to serve the dashboard: * for every GitHub account, or a comma-separated list of GitHub user ids (gh api users/<login> --jq .id)');
|
|
136
159
|
}
|
|
137
160
|
const assistantSecret = readSecret(env, 'SLOTLOCK_ASSISTANT_SECRET');
|
|
@@ -152,7 +175,7 @@ export function readDashboardConfig(env) {
|
|
|
152
175
|
githubClientId,
|
|
153
176
|
githubClientSecret,
|
|
154
177
|
sessionSecret,
|
|
155
|
-
allowedUsers: users
|
|
178
|
+
allowedUsers: users,
|
|
156
179
|
...(orgs !== undefined ? { organizations: orgs } : {}),
|
|
157
180
|
...(name !== undefined && name !== '' ? { name } : {}),
|
|
158
181
|
...(assistantSecret !== undefined ? { assistantSecret } : {}),
|
|
@@ -372,3 +395,18 @@ export function readTrustedProxyHeader(env) {
|
|
|
372
395
|
}
|
|
373
396
|
return value.toLowerCase();
|
|
374
397
|
}
|
|
398
|
+
/** The request field that carries SLOTLOCK_TRUSTED_PROXY_SECRET; not cf-…, which Cloudflare's rules may not set. */
|
|
399
|
+
export const SLOTLOCK_TRUSTED_PROXY_PROOF_FIELD = 'x-slotlock-proxy-proof';
|
|
400
|
+
/**
|
|
401
|
+
* SLOTLOCK_TRUSTED_PROXY_SECRET: a value the proxy in front adds to every request in
|
|
402
|
+
* SLOTLOCK_TRUSTED_PROXY_PROOF_FIELD, such as a Cloudflare transform rule. With it, the trusted
|
|
403
|
+
* proxy field is believed only beside that value, so a process that reaches the server without
|
|
404
|
+
* passing the proxy (another container on its network) cannot choose its own client address.
|
|
405
|
+
*/
|
|
406
|
+
export function readTrustedProxySecret(env, trustedProxyHeader) {
|
|
407
|
+
const value = readSecret(env, 'SLOTLOCK_TRUSTED_PROXY_SECRET');
|
|
408
|
+
if (value !== undefined && trustedProxyHeader === undefined) {
|
|
409
|
+
throw new SlotlockConfigError('SLOTLOCK_TRUSTED_PROXY_SECRET proves SLOTLOCK_TRUSTED_PROXY_HEADER: set both, or neither');
|
|
410
|
+
}
|
|
411
|
+
return value;
|
|
412
|
+
}
|
package/dashboard.js
CHANGED
|
@@ -645,6 +645,9 @@ export function createSlotlockDashboard(options) {
|
|
|
645
645
|
const base = publicUrl.pathname.replace(/\/$/, '');
|
|
646
646
|
const root = `${base}/dashboard`;
|
|
647
647
|
const callbackUrl = `${origin}${root}/callback`;
|
|
648
|
+
// Its cookies are Secure, but a first visit over http:// could still be answered by someone else:
|
|
649
|
+
// a browser that has seen this goes straight to https:// for a year. Not for a local http one.
|
|
650
|
+
const hsts = publicUrl.protocol === 'https:' ? { 'Strict-Transport-Security': 'max-age=31536000' } : {};
|
|
648
651
|
const service = new URL(options.serviceUrl ?? options.publicUrl);
|
|
649
652
|
const serviceRoot = `${service.origin}${service.pathname.replace(/\/$/, '')}`;
|
|
650
653
|
const mcpUrl = `${serviceRoot}/mcp`;
|
|
@@ -774,6 +777,7 @@ export function createSlotlockDashboard(options) {
|
|
|
774
777
|
'X-Content-Type-Options': 'nosniff',
|
|
775
778
|
'Referrer-Policy': REFERRER_POLICY,
|
|
776
779
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
780
|
+
...hsts,
|
|
777
781
|
...headers,
|
|
778
782
|
},
|
|
779
783
|
});
|
|
@@ -1365,6 +1369,11 @@ ${entries.length === 0
|
|
|
1365
1369
|
const resolveHost = assistantOptions?.resolve ??
|
|
1366
1370
|
(async (hostname) => (await dnsLookup(hostname, { all: true })).map(({ address }) => address));
|
|
1367
1371
|
const PRIVATE_URL_REFUSAL = 'That URL is, or resolves to, a local or private network address (loopback, private, link-local, shared, unique-local or cloud metadata), which this server does not reach. For a model on its own machine or network, the server’s operator can allow them with SLOTLOCK_ASSISTANT_PRIVATE_URLS=on.';
|
|
1372
|
+
// The dashboard's own host and the server's: a model there would be sent the organization's key
|
|
1373
|
+
// and the conversation by this deployment itself, on a public name the private-address check passes.
|
|
1374
|
+
const ownHosts = new Set([publicUrl.hostname, service.hostname]);
|
|
1375
|
+
const namesOwnHost = (baseUrl) => ownHosts.has(new URL(baseUrl).hostname);
|
|
1376
|
+
const OWN_HOST_REFUSAL = 'That URL is this Slotlock deployment’s own address. Give the address of the model’s API.';
|
|
1368
1377
|
const assistantTimeout = assistantOptions?.timeoutMs ?? 30_000;
|
|
1369
1378
|
const assistantTurnTimeout = assistantOptions?.turnTimeoutMs ?? 90_000;
|
|
1370
1379
|
const conversationTtl = assistantOptions?.conversationTtlMs ?? 2 * 3_600_000;
|
|
@@ -1493,6 +1502,8 @@ ${entries.length === 0
|
|
|
1493
1502
|
const settings = store ? await store.getSettings(tenantOf(session)) : null;
|
|
1494
1503
|
if (!settings || !assistantOptions)
|
|
1495
1504
|
return null;
|
|
1505
|
+
if (namesOwnHost(settings.baseUrl))
|
|
1506
|
+
return { problem: OWN_HOST_REFUSAL };
|
|
1496
1507
|
if (!slotlockAssistantUrlAllowed(settings.baseUrl, privateUrls) ||
|
|
1497
1508
|
!(await checkSlotlockAssistantHost(settings.baseUrl, { privateUrls, resolve: resolveHost }))) {
|
|
1498
1509
|
return { problem: PRIVATE_URL_REFUSAL };
|
|
@@ -1662,6 +1673,8 @@ ${settings
|
|
|
1662
1673
|
: 'Give an https base URL (http only for localhost or 127.0.0.1), with no user name, query or #fragment.');
|
|
1663
1674
|
}
|
|
1664
1675
|
const baseUrl = checkedUrl.url;
|
|
1676
|
+
if (namesOwnHost(baseUrl))
|
|
1677
|
+
return settingsPage(session, 400, OWN_HOST_REFUSAL);
|
|
1665
1678
|
if (!(await checkSlotlockAssistantHost(baseUrl, { privateUrls, resolve: resolveHost }))) {
|
|
1666
1679
|
return settingsPage(session, 400, PRIVATE_URL_REFUSAL);
|
|
1667
1680
|
}
|
|
@@ -2644,7 +2657,7 @@ code, pre, kbd, input[readonly] { font-family: var(--mono); font-size: 12.5px; }
|
|
|
2644
2657
|
.me form { margin: 0 0 0 auto; }
|
|
2645
2658
|
.login { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
|
2646
2659
|
.sign-out .icon { width: 15px; height: 15px; }
|
|
2647
|
-
.page { width: 100%; max-width:
|
|
2660
|
+
.page { width: 100%; max-width: 1600px; margin-inline: auto; padding: var(--space-8) var(--space-8) 72px; display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-5); align-content: start; min-width: 0; }
|
|
2648
2661
|
.page-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; min-height: 36px; margin-bottom: var(--space-1); }
|
|
2649
2662
|
|
|
2650
2663
|
/* Type */
|
package/ddl.d.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/** The start of the message with which the deployment DDL refuses a `slotlock` schema. */
|
|
2
2
|
export declare const SLOTLOCK_SCHEMA_CONTROL_REFUSAL = "Slotlock will not deploy into schema slotlock";
|
|
3
3
|
/**
|
|
4
|
-
* The version
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* The schema version this release creates, recorded in `slotlock.schema_version`. Up to 0.2
|
|
5
|
+
* recorded nothing (0.1 was 1, 0.2 was 2); 0.3 to 0.5 recorded 3. 0.6 is 4, so a 0.5 release
|
|
6
|
+
* refuses it rather than restoring the trigger that names a coverage source by its guessable
|
|
7
|
+
* digest.
|
|
8
8
|
*/
|
|
9
9
|
export declare const SLOTLOCK_SCHEMA_VERSION = 4;
|
|
10
10
|
/** The start of the message with which the deployment DDL refuses a schema a newer release migrated. */
|
|
11
11
|
export declare const SLOTLOCK_SCHEMA_NEWER_REFUSAL = "Slotlock will not migrate a schema a newer release migrated";
|
|
12
12
|
/**
|
|
13
|
-
* What every migration checks before
|
|
14
|
-
*
|
|
15
|
-
* and the schema version. Run with search_path pinned, as the DDL pins it.
|
|
13
|
+
* What every migration checks before changing anything, even when it finds nothing to change: no
|
|
14
|
+
* other role controls the schema, then server and schema version. Run with search_path pinned.
|
|
16
15
|
*/
|
|
17
16
|
export declare const SLOTLOCK_DEPLOYMENT_CHECKS = "DO $slotlock_schema_control$\nDECLARE\n refusals text[];\nBEGIN\n SELECT pg_catalog.array_agg(refusal.reason ORDER BY refusal.reason)\n INTO refusals\n FROM (\n SELECT 'schema slotlock is owned by ' || owned_by.rolname AS reason\n FROM pg_catalog.pg_namespace ns\n JOIN pg_catalog.pg_roles owned_by ON owned_by.oid = ns.nspowner\n WHERE ns.nspname = 'slotlock'\n AND owned_by.rolname <> CURRENT_USER AND NOT owned_by.rolsuper\n UNION\n SELECT COALESCE(grantee_role.rolname, 'PUBLIC') || ' can create objects in schema slotlock'\n FROM pg_catalog.pg_namespace ns\n CROSS JOIN LATERAL pg_catalog.aclexplode(ns.nspacl) acl\n LEFT JOIN pg_catalog.pg_roles grantee_role ON grantee_role.oid = acl.grantee\n WHERE ns.nspname = 'slotlock'\n AND acl.privilege_type = 'CREATE'\n AND acl.grantee <> ns.nspowner\n AND (acl.grantee = 0\n OR (grantee_role.rolname <> CURRENT_USER AND NOT grantee_role.rolsuper))\n UNION\n SELECT ident.type || ' ' || ident.identity || ' is owned by ' || owned_by.rolname\n FROM pg_catalog.pg_shdepend owned\n JOIN pg_catalog.pg_roles owned_by ON owned_by.oid = owned.refobjid\n CROSS JOIN LATERAL pg_catalog.pg_identify_object(owned.classid, owned.objid, owned.objsubid) ident\n WHERE owned.dbid = (SELECT db.oid FROM pg_catalog.pg_database db\n WHERE db.datname = pg_catalog.current_database())\n AND owned.deptype = 'o'\n AND owned.refclassid = 'pg_catalog.pg_authid'::pg_catalog.regclass\n AND owned_by.rolname <> CURRENT_USER AND NOT owned_by.rolsuper\n AND ident.schema = 'slotlock'\n UNION\n SELECT 'trigger ' || trg.tgname || ' on slotlock.' || rel.relname || ' runs '\n || trg.tgfoid::pg_catalog.regprocedure::pg_catalog.text\n || ', owned by ' || owned_by.rolname\n FROM pg_catalog.pg_trigger trg\n JOIN pg_catalog.pg_class rel ON rel.oid = trg.tgrelid\n JOIN pg_catalog.pg_namespace ns ON ns.oid = rel.relnamespace\n JOIN pg_catalog.pg_proc fn ON fn.oid = trg.tgfoid\n JOIN pg_catalog.pg_roles owned_by ON owned_by.oid = fn.proowner\n WHERE ns.nspname = 'slotlock'\n AND NOT trg.tgisinternal\n AND owned_by.rolname <> CURRENT_USER AND NOT owned_by.rolsuper\n UNION\n SELECT COALESCE(grantee_role.rolname, 'PUBLIC') || ' can create triggers on slotlock.'\n || rel.relname\n FROM pg_catalog.pg_class rel\n JOIN pg_catalog.pg_namespace ns ON ns.oid = rel.relnamespace\n CROSS JOIN LATERAL pg_catalog.aclexplode(rel.relacl) acl\n LEFT JOIN pg_catalog.pg_roles grantee_role ON grantee_role.oid = acl.grantee\n WHERE ns.nspname = 'slotlock'\n AND acl.privilege_type = 'TRIGGER'\n AND acl.grantee <> rel.relowner\n AND (acl.grantee = 0\n OR (grantee_role.rolname <> CURRENT_USER AND NOT grantee_role.rolsuper))\n UNION\n SELECT 'extension ' || ext.extname || ' in schema slotlock is owned by ' || owned_by.rolname\n FROM pg_catalog.pg_extension ext\n JOIN pg_catalog.pg_namespace ns ON ns.oid = ext.extnamespace\n JOIN pg_catalog.pg_roles owned_by ON owned_by.oid = ext.extowner\n WHERE ns.nspname = 'slotlock'\n AND owned_by.rolname <> CURRENT_USER AND NOT owned_by.rolsuper\n ) refusal;\n IF refusals IS NOT NULL THEN\n RAISE EXCEPTION 'Slotlock will not deploy into schema slotlock: another role controls it'\n USING ERRCODE = 'insufficient_privilege',\n DETAIL = pg_catalog.to_jsonb(refusals)::pg_catalog.text,\n HINT = 'The deployment role must own schema slotlock and every object in it, and no other role may create objects in it.';\n END IF;\nEND\n$slotlock_schema_control$;\nDO $slotlock_schema_preconditions$\nDECLARE\n installed integer;\nBEGIN\n IF pg_catalog.current_setting('server_version_num')::integer < 160000 THEN\n RAISE EXCEPTION 'Slotlock needs PostgreSQL 16 or later; this server is %',\n pg_catalog.current_setting('server_version')\n USING ERRCODE = 'feature_not_supported';\n END IF;\n IF pg_catalog.to_regclass('slotlock.schema_version') IS NOT NULL THEN\n EXECUTE 'SELECT version FROM slotlock.schema_version WHERE singleton' INTO installed;\n IF installed > 4 THEN\n RAISE EXCEPTION 'Slotlock will not migrate a schema a newer release migrated (version %; this release is version 4)', installed\n USING ERRCODE = 'object_not_in_prerequisite_state',\n HINT = 'Migrate and serve it with the release that migrated it, or a later one.';\n END IF;\n END IF;\nEND\n$slotlock_schema_preconditions$;\n";
|
|
18
17
|
/** Active (unrevoked, unexpired) API keys one tenant may hold. */
|
|
@@ -43,21 +42,21 @@ export declare const SLOTLOCK_SCHEMA_TABLES: readonly ["schema_version"];
|
|
|
43
42
|
/** The SECURITY DEFINER function through which a server reads the version it is serving. */
|
|
44
43
|
export declare const SLOTLOCK_SCHEMA_FUNCTIONS: readonly ["slotlock.installed_schema_version()"];
|
|
45
44
|
/** The table of suspended tenants: they may read, and write nothing (store.ts withTenant). */
|
|
46
|
-
export declare const SLOTLOCK_TENANT_ADMIN_TABLES: readonly ["tenant_suspensions", "webhook_tenants", "rate_buckets", "assistant_tenants"];
|
|
45
|
+
export declare const SLOTLOCK_TENANT_ADMIN_TABLES: readonly ["tenant_suspensions", "webhook_tenants", "rate_buckets", "assistant_tenants", "maintenance_tenants"];
|
|
47
46
|
/**
|
|
48
47
|
* The SECURITY DEFINER functions through which a server checks a tenant's suspension and erases a
|
|
49
48
|
* tenant, by signature.
|
|
50
49
|
*/
|
|
51
|
-
export declare const SLOTLOCK_TENANT_ADMIN_FUNCTIONS: readonly ["slotlock.tenant_suspended(text)", "slotlock.erase_tenant(text)", "slotlock.register_webhook_tenant(text)", "slotlock.webhook_tenants(text)", "slotlock.unregister_webhook_tenant(text)", "slotlock.take_rate_token(text, double precision, double precision)", "slotlock.register_assistant_tenant(text, timestamptz)", "slotlock.sweep_assistant_conversations(integer)"];
|
|
50
|
+
export declare const SLOTLOCK_TENANT_ADMIN_FUNCTIONS: readonly ["slotlock.tenant_suspended(text)", "slotlock.erase_tenant(text)", "slotlock.register_webhook_tenant(text)", "slotlock.webhook_tenants(text)", "slotlock.unregister_webhook_tenant(text)", "slotlock.take_rate_token(text, double precision, double precision)", "slotlock.register_assistant_tenant(text, timestamptz)", "slotlock.sweep_assistant_conversations(integer)", "slotlock.list_maintenance_tenant()", "slotlock.maintenance_tenants(text)"];
|
|
52
51
|
/**
|
|
53
52
|
* Functions only the deployment role runs: suspending and lifting a suspension. No other role is
|
|
54
53
|
* granted them, so a serving role cannot lift a suspension.
|
|
55
54
|
*/
|
|
56
55
|
export declare const SLOTLOCK_OPERATOR_FUNCTIONS: readonly ["slotlock.suspend_tenant(text, text)", "slotlock.unsuspend_tenant(text)"];
|
|
57
56
|
/** Every SECURITY DEFINER function the serving role may execute, by signature. */
|
|
58
|
-
export declare const SLOTLOCK_DEFINER_FUNCTIONS: readonly ("slotlock.authenticate_api_key(bytea)" | "slotlock.create_api_key(text, text, text, bytea, text[], timestamptz, text)" | "slotlock.dashboard_session_ended(bytea, timestamptz)" | "slotlock.erase_api_keys(text)" | "slotlock.erase_tenant(text)" | "slotlock.installed_schema_version()" | "slotlock.list_api_keys(text)" | "slotlock.register_assistant_tenant(text, timestamptz)" | "slotlock.register_webhook_tenant(text)" | "slotlock.revoke_api_key(text, uuid)" | "slotlock.rotate_api_key(text, uuid, text, bytea)" | "slotlock.sweep_assistant_conversations(integer)" | "slotlock.take_rate_token(text, double precision, double precision)" | "slotlock.tenant_suspended(text)" | "slotlock.unregister_webhook_tenant(text)" | "slotlock.use_dashboard_token(text, bytea, timestamptz, bytea)" | "slotlock.webhook_tenants(text)")[];
|
|
57
|
+
export declare const SLOTLOCK_DEFINER_FUNCTIONS: readonly ("slotlock.authenticate_api_key(bytea)" | "slotlock.create_api_key(text, text, text, bytea, text[], timestamptz, text)" | "slotlock.dashboard_session_ended(bytea, timestamptz)" | "slotlock.erase_api_keys(text)" | "slotlock.erase_tenant(text)" | "slotlock.installed_schema_version()" | "slotlock.list_api_keys(text)" | "slotlock.list_maintenance_tenant()" | "slotlock.maintenance_tenants(text)" | "slotlock.register_assistant_tenant(text, timestamptz)" | "slotlock.register_webhook_tenant(text)" | "slotlock.revoke_api_key(text, uuid)" | "slotlock.rotate_api_key(text, uuid, text, bytea)" | "slotlock.sweep_assistant_conversations(integer)" | "slotlock.take_rate_token(text, double precision, double precision)" | "slotlock.tenant_suspended(text)" | "slotlock.unregister_webhook_tenant(text)" | "slotlock.use_dashboard_token(text, bytea, timestamptz, bytea)" | "slotlock.webhook_tenants(text)")[];
|
|
59
58
|
/** Tables no role but their owner reads or writes, reached only through SLOTLOCK_DEFINER_FUNCTIONS. */
|
|
60
|
-
export declare const SLOTLOCK_DEFINER_TABLES: readonly ("api_key_retired_digests" | "api_keys" | "assistant_tenants" | "dashboard_tokens" | "rate_buckets" | "schema_version" | "tenant_suspensions" | "webhook_tenants")[];
|
|
59
|
+
export declare const SLOTLOCK_DEFINER_TABLES: readonly ("api_key_retired_digests" | "api_keys" | "assistant_tenants" | "dashboard_tokens" | "maintenance_tenants" | "rate_buckets" | "schema_version" | "tenant_suspensions" | "webhook_tenants")[];
|
|
61
60
|
/**
|
|
62
61
|
* The advisory lock every server transaction for a tenant holds shared (store.ts withTenant), and
|
|
63
62
|
* suspension and erasure take exclusively, keyed by this text's hashtextextended(…, 0).
|
|
@@ -68,16 +67,12 @@ export declare const SLOTLOCK_VALIDATION_DDL: string;
|
|
|
68
67
|
/** Outbox ids one batch of the coverage-change rename reads (by primary key) in one transaction. */
|
|
69
68
|
export declare const SLOTLOCK_COVERAGE_CHANGE_RENAME_BATCH = 5000;
|
|
70
69
|
/**
|
|
71
|
-
* One batch of the rename of coverage changes an earlier release wrote: of the outbox rows
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* can match it any more, and a made-up id would look like a source the tenant could find. Selects
|
|
78
|
-
* the last id read (`last`, null when there was none) and how many it renamed (`renamed`), `INTO`
|
|
79
|
-
* `into` when given (PL/pgSQL). Rows in the new shape are left alone, so a batch run again changes
|
|
80
|
-
* nothing.
|
|
70
|
+
* One batch of the rename of coverage changes an earlier release wrote: of the outbox rows after
|
|
71
|
+
* `after` up to `head` (bigint SQL expressions), the first `batch` by id; each `coverage.*` row not
|
|
72
|
+
* yet in shape gets its source_id as subject and `data.source_id`, and loses `source` and
|
|
73
|
+
* `last_error` (before 0.5.3 these carried the source itself, a URL with a token; 0.5.3's unkeyed
|
|
74
|
+
* digest could confirm a guess). A deleted source becomes `src_redacted`. Selects `last` (null when
|
|
75
|
+
* none) and `renamed`, `INTO` `into` when given. Idempotent.
|
|
81
76
|
*/
|
|
82
77
|
export declare function slotlockCoverageChangeRenameSql(after: string, head: string, batch: string, into?: string): string;
|
|
83
78
|
/** The whole deployment DDL, run in one transaction: the schema, then its constraints validated. */
|
|
@@ -95,12 +90,10 @@ export declare const SLOTLOCK_TENANT_CONTEXT_SETTING = "slotlock.tenant_ref";
|
|
|
95
90
|
*/
|
|
96
91
|
export declare const SLOTLOCK_TENANT_TABLES: readonly ["resources", "reservations", "reservation_tombstones", "reservation_conflict_archive", "calendar_events", "calendar_event_attendees", "calendar_event_reminders", "calendar_event_exceptions", "calendar_event_occurrences", "calendar_event_tombstones", "calendar_event_commands", "calendar_coverage", "hold_commands", "outbox", "outbox_pruned", "webhook_endpoints", "waitlist_entries", "booking_rule_defaults", "assistant_settings", "assistant_conversations"];
|
|
97
92
|
/**
|
|
98
|
-
* Build the grants for the application role: USAGE on the
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* the known ways out; these statements check nothing). Rerun after every `applySchema()`, since a
|
|
103
|
-
* release may add a table.
|
|
93
|
+
* Build the grants for the application role: USAGE on the schema, DML on the store's tables and
|
|
94
|
+
* EXECUTE on the API key and dashboard functions, nothing else. The role must not be able to leave
|
|
95
|
+
* forced RLS (`grantApplicationRole` checks; these statements do not). Rerun after every
|
|
96
|
+
* `applySchema()`, since a release may add a table.
|
|
104
97
|
*/
|
|
105
98
|
export declare function createSlotlockApplicationRoleGrantsDdl(role: string): string;
|
|
106
99
|
/**
|