slotlock 0.6.3 → 0.8.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +511 -0
  2. package/CONTRIBUTING.md +12 -5
  3. package/README.md +50 -1551
  4. package/SECURITY.md +5 -2
  5. package/SPEC.md +1 -1
  6. package/agent-server.d.ts +160 -8
  7. package/agent-server.js +559 -44
  8. package/agent-store-backend.d.ts +34 -1
  9. package/agent-store-backend.js +1399 -38
  10. package/api-keys.d.ts +69 -3
  11. package/api-keys.js +217 -28
  12. package/application-api.d.ts +56 -3
  13. package/application-api.js +826 -15
  14. package/approvals.d.ts +88 -0
  15. package/approvals.js +293 -0
  16. package/assistant.d.ts +70 -1
  17. package/assistant.js +478 -50
  18. package/billing.d.ts +114 -0
  19. package/billing.js +258 -0
  20. package/config.d.ts +25 -4
  21. package/config.js +72 -3
  22. package/dashboard-assistant-ui.d.ts +21 -0
  23. package/dashboard-assistant-ui.js +438 -0
  24. package/dashboard-icon.d.ts +1 -0
  25. package/dashboard-icon.js +5 -0
  26. package/dashboard-markdown.d.ts +2 -0
  27. package/dashboard-markdown.js +157 -0
  28. package/dashboard-settings-ui.d.ts +1 -0
  29. package/dashboard-settings-ui.js +78 -0
  30. package/dashboard.d.ts +227 -7
  31. package/dashboard.js +2313 -247
  32. package/ddl.d.ts +51 -12
  33. package/ddl.js +2566 -54
  34. package/event-feed.d.ts +30 -4
  35. package/event-feed.js +171 -19
  36. package/index.d.ts +15 -4
  37. package/index.js +8 -2
  38. package/maintenance.d.ts +10 -0
  39. package/maintenance.js +16 -2
  40. package/metrics.d.ts +41 -0
  41. package/metrics.js +22 -0
  42. package/openapi.json +14675 -5957
  43. package/otel.d.ts +86 -0
  44. package/otel.js +273 -0
  45. package/overage.d.ts +33 -0
  46. package/overage.js +115 -0
  47. package/package.json +1 -1
  48. package/partner-billing.d.ts +81 -0
  49. package/partner-billing.js +333 -0
  50. package/partner-names.d.ts +8 -0
  51. package/partner-names.js +10 -0
  52. package/partners.d.ts +147 -0
  53. package/partners.js +375 -0
  54. package/plans.d.ts +92 -0
  55. package/plans.js +115 -0
  56. package/quotas.d.ts +108 -0
  57. package/quotas.js +258 -0
  58. package/rate-limit.d.ts +21 -1
  59. package/rate-limit.js +82 -15
  60. package/resource-profile.d.ts +135 -0
  61. package/resource-profile.js +263 -0
  62. package/rest-api.d.ts +2 -0
  63. package/rest-api.js +68 -3
  64. package/sandbox.d.ts +70 -0
  65. package/sandbox.js +295 -0
  66. package/self-host.d.ts +33 -5
  67. package/self-host.js +950 -22
  68. package/slotlock.manifest.json +1 -1
  69. package/store.d.ts +373 -4
  70. package/store.js +1212 -118
  71. package/teams.d.ts +25 -0
  72. package/teams.js +63 -0
  73. package/timezone.js +13 -1
  74. package/types.d.ts +95 -1
  75. package/usage.d.ts +106 -0
  76. package/usage.js +238 -0
@@ -0,0 +1,78 @@
1
+ // The dashboard Settings pages' styles: a section list beside one column of cards, label-left rows,
2
+ // provider tiles, and the assistant's status card. dashboard.ts renders the pages.
3
+ export const SLOTLOCK_SETTINGS_CSS = `
4
+ .settings { display: grid; grid-template-columns: 176px minmax(0, 640px); gap: var(--space-8); align-items: start; }
5
+ .subnav { display: grid; gap: 2px; position: sticky; top: var(--space-6); }
6
+ .subnav a { display: flex; align-items: center; gap: var(--space-2); height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-sm); color: var(--ink-2); font-size: 14px; text-decoration: none; }
7
+ .subnav a:hover { background: var(--hover); color: var(--ink); }
8
+ .subnav a[aria-current="page"] { background: var(--surface); color: var(--ink); font-weight: 500; box-shadow: inset 0 0 0 1px var(--line); }
9
+ .subnav .nav-dot { width: 6px; height: 6px; border-radius: 50%; margin-left: auto; background: var(--good); }
10
+ .settings-body { display: grid; gap: var(--space-6); min-width: 0; }
11
+ .settings-body > .notice { margin: 0; }
12
+ .card-head { margin: 0 0 calc(var(--space-2) - var(--space-6)); font-size: 13px; font-weight: 600; color: var(--ink-3); }
13
+ .set-card { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin: 0; }
14
+ .row { display: grid; grid-template-columns: minmax(120px, 160px) minmax(0, 1fr); gap: var(--space-4); align-items: center; min-height: 56px; padding: 12px 20px; border-top: 1px solid var(--line-soft); }
15
+ .row.stack { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); align-items: start; padding-top: 16px; padding-bottom: 16px; }
16
+ .row:first-child { border-top: 0; }
17
+ .row-label { font-size: 14px; font-weight: 500; color: var(--ink); }
18
+ .row-value { font-size: 14px; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
19
+ .row-value.muted { color: var(--ink-2); }
20
+ .row input:not([type="radio"]):not([type="checkbox"]), .row select { width: 100%; }
21
+ .mono { font-family: var(--mono); font-size: 13px; }
22
+ .card-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: 12px 20px; border-top: 1px solid var(--line); background: color-mix(in srgb, var(--surface) 70%, var(--canvas)); }
23
+ .card-foot .fine { margin: 0; }
24
+ .card-foot .primary { flex: none; }
25
+ .status-head { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto auto; gap: var(--space-3); align-items: center; padding: 16px 20px; }
26
+ .status-head form { margin: 0; }
27
+ .mono-tile { display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--radius); background: var(--sunken); border: 1px solid var(--line); color: var(--ink); font-weight: 600; font-size: 14px; }
28
+ .status-name { display: grid; gap: 2px; min-width: 0; }
29
+ .status-name strong { font-size: 15px; font-weight: 600; color: var(--ink); }
30
+ .status-name code { color: var(--ink-3); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: none; padding: 0; }
31
+ .badge { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: var(--radius-full); font-size: 12px; font-weight: 500; color: var(--ink-2); background: var(--sunken); white-space: nowrap; }
32
+ .badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--ink-3); }
33
+ .badge.ok { color: var(--good); background: color-mix(in srgb, var(--good) 14%, transparent); }
34
+ .badge.ok::before { background: var(--good); }
35
+ .badge.bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }
36
+ .badge.bad::before { background: var(--bad); }
37
+ .status-card .notice { margin: 0; border-radius: 0; border-width: 1px 0 0; box-shadow: none; font-size: 13px; padding: 10px 20px; }
38
+ .status-card .notice.done { color: var(--ink); }
39
+ .tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-2); }
40
+ .tile { position: relative; display: flex; align-items: center; gap: var(--space-2); height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); font-size: 14px; color: var(--ink); cursor: pointer; transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease); }
41
+ .tile:hover { background: var(--hover); }
42
+ .tile input { position: absolute; opacity: 0; pointer-events: none; }
43
+ .tile .tile-mark { display: grid; place-items: center; flex: none; width: 20px; height: 20px; border-radius: 5px; background: var(--sunken); color: var(--ink-2); font-size: 11px; font-weight: 600; }
44
+ .tile:has(input:checked) { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
45
+ .tile:has(input:checked) .tile-mark { background: var(--accent); color: var(--surface); }
46
+ .tile:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--ring); }
47
+ .provider-form:not(:has(input[name="preset"][value="custom"]:checked)) .custom-only { display: none; }
48
+ .segmented { justify-self: start; display: inline-flex; padding: 2px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--sunken); gap: 2px; }
49
+ .segmented label { position: relative; display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 6px; font-size: 13px; color: var(--ink-2); cursor: pointer; }
50
+ .segmented input { position: absolute; opacity: 0; pointer-events: none; }
51
+ .segmented label:has(input:checked) { background: var(--surface); color: var(--ink); box-shadow: 0 0 0 1px var(--line); }
52
+ .field-with-button { display: flex; gap: var(--space-2); align-items: center; }
53
+ .field-with-button input { flex: 1; min-width: 0; }
54
+ .field-with-button button { flex: none; }
55
+ .key-set { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
56
+ .key-set code { background: none; padding: 0; color: var(--ink-2); letter-spacing: 0.04em; }
57
+ .key-set details { display: contents; }
58
+ .key-set details > summary { list-style: none; cursor: pointer; color: var(--accent); font-size: 13px; }
59
+ .key-set details > summary::-webkit-details-marker { display: none; }
60
+ .key-set details[open] > summary { display: none; }
61
+ .key-set details[open] input { flex-basis: 100%; }
62
+ .danger-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: 14px 20px; }
63
+ .danger-row strong { font-size: 14px; font-weight: 500; color: var(--ink); }
64
+ .danger-row details.act > summary { color: var(--bad); }
65
+ @media (max-width: 760px) {
66
+ .settings { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
67
+ .subnav { position: static; display: flex; overflow-x: auto; gap: 0; border-bottom: 1px solid var(--line); scrollbar-width: none; }
68
+ .subnav::-webkit-scrollbar { display: none; }
69
+ .subnav a { height: 40px; border-radius: 0; flex: none; }
70
+ .subnav a[aria-current="page"] { background: none; box-shadow: inset 0 -2px 0 var(--accent); }
71
+ .row { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); padding: 12px 16px; }
72
+ .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
73
+ .status-head { grid-template-columns: 32px minmax(0, 1fr) auto; padding: 14px 16px; }
74
+ .status-head form { grid-column: 1 / -1; }
75
+ .card-foot { flex-direction: column; align-items: stretch; padding: 12px 16px; }
76
+ .card-foot .primary { height: 44px; }
77
+ }
78
+ `;
package/dashboard.d.ts CHANGED
@@ -1,19 +1,28 @@
1
- import type { SlotlockApiKeyStore } from './api-keys.js';
2
- import { type SlotlockAgentCalendarBackend, type SlotlockAgentOperation, type SlotlockAgentOperationDispatchOptions, type SlotlockAgentPrincipal } from './agent-server.js';
1
+ import type { SlotlockDashboardTeams } from './teams.js';
2
+ import { type SlotlockApiKeyStore } from './api-keys.js';
3
+ import { type SlotlockAgentCalendarBackend, type SlotlockAgentOperation, type SlotlockAgentOperationDispatchOptions, type SlotlockAgentPrincipal, type SlotlockAgentUsage } from './agent-server.js';
3
4
  import { type SlotlockAssistantStore } from './assistant.js';
4
5
  import { type SlotlockRate } from './config.js';
5
6
  import { type SlotlockBookingRules } from './booking-rules.js';
7
+ import { type SlotlockPlan, type SlotlockTenantPlan } from './plans.js';
8
+ import { type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceProfileInput } from './resource-profile.js';
9
+ import { type SlotlockUsageRow } from './usage.js';
6
10
  import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
7
11
  import type { SlotlockSql, SlotlockStore, SlotlockWaitlistEntry } from './store.js';
8
- import type { Interval, SlotlockAgendaEntry, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
12
+ import { type SlotlockSandboxScenario } from './sandbox.js';
13
+ import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
9
14
  /** Resources one dashboard tenant may hold. */
10
- export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 100;
15
+ export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 500;
11
16
  /**
12
17
  * A tenant's calendar resources, as the dashboard lists and adds them. Each write takes, last, the
13
18
  * person who made it (`github:<user id>`) as `actor`, to record on the event feed.
14
19
  */
15
20
  export interface SlotlockDashboardResources {
16
21
  list(tenantRef: string): Promise<SlotlockResource[]>;
22
+ /** How many resources the tenant has, archived ones included, as a resource limit counts them. */
23
+ count?(tenantRef: string): Promise<number>;
24
+ /** The tenant's metered usage from `from` to `to`, UTC days inclusive (usage.ts). */
25
+ usage?(tenantRef: string, from: string, to: string): Promise<SlotlockUsageRow[]>;
17
26
  /**
18
27
  * Create the resource, or re-zone the tenant's resource with that reference. Refuse a new one
19
28
  * (an error with `code: 'resource_limit_reached'`) once the tenant has
@@ -39,8 +48,84 @@ export interface SlotlockDashboardResources {
39
48
  setRules?(tenantRef: string, id: string, rules: SlotlockBookingRules | null, actor?: string): Promise<SlotlockResource | null>;
40
49
  /** The tenant's default booking rules and its pools', which a resource's own rules overlay. */
41
50
  ruleDefaults?(tenantRef: string): Promise<SlotlockBookingRuleDefaults>;
51
+ /**
52
+ * Change one of the tenant's resources' profile (resource-profile.ts): the fields given, keeping
53
+ * the rest. As setRules; `code: 'invalid_resource_profile'`, with the `field`.
54
+ */
55
+ setProfile?(tenantRef: string, id: string, profile: SlotlockResourceProfileInput, actor?: string): Promise<SlotlockResource | null>;
56
+ /**
57
+ * Make one of the tenant's resources a part of a composite (RFC 0003 G8), or no part. A refusal
58
+ * carries the store's `code`: `parent_not_found`, `composite_too_deep`, `composite_parts_limit`
59
+ * or `composite_busy`.
60
+ */
61
+ setParent?(tenantRef: string, id: string, parentId: string | null, actor?: string): Promise<SlotlockResource | null>;
42
62
  /** Put one of the tenant's resources in a pool, or none: as setRules; `code: 'invalid_pool'`. */
43
63
  setPool?(tenantRef: string, id: string, pool: string | null, actor?: string): Promise<SlotlockResource | null>;
64
+ /**
65
+ * The tenant's locations and groups (RFC 0003 G2, G3), and where each resource is in them. A
66
+ * refusal carries the store's `code`: `invalid_location` or `invalid_group` (with the `field`),
67
+ * `location_not_found`, `location_too_deep`, `location_cycle`, `location_limit_reached`,
68
+ * `location_in_use`, `group_not_found`, `group_limit_reached` or `resource_group_limit_reached`.
69
+ */
70
+ grouping?: {
71
+ /** Each under its parent, depth first, with how many resources are at each. */
72
+ locations(tenantRef: string): Promise<SlotlockLocation[]>;
73
+ /** Create the location with this reference, or change its name, parent and time zone. */
74
+ putLocation(tenantRef: string, location: {
75
+ ref: string;
76
+ name: string;
77
+ parentRef: string | null;
78
+ timezone: string | null;
79
+ }, actor?: string): Promise<SlotlockLocation>;
80
+ /** False when the tenant has no such location. */
81
+ deleteLocation(tenantRef: string, ref: string, actor?: string): Promise<boolean>;
82
+ /** By reference, with how many resources are in each. */
83
+ groups(tenantRef: string): Promise<SlotlockResourceGroup[]>;
84
+ /** Create the group with this reference, or change its name and description. */
85
+ putGroup(tenantRef: string, group: {
86
+ ref: string;
87
+ name: string;
88
+ description: string | null;
89
+ }, actor?: string): Promise<SlotlockResourceGroup>;
90
+ /** Delete a group, its resources left as they are: false when the tenant has no such group. */
91
+ deleteGroup(tenantRef: string, ref: string, actor?: string): Promise<boolean>;
92
+ /**
93
+ * Put one of the tenant's resources at a location (or none) and in exactly these groups, in one
94
+ * transaction: the resource as it then is, or null when it is not the tenant's.
95
+ */
96
+ place(tenantRef: string, id: string, place: {
97
+ location: string | null;
98
+ groups: readonly string[];
99
+ }, actor?: string): Promise<SlotlockResource | null>;
100
+ };
101
+ /**
102
+ * The tenant's approvals (RFC 0004 A4), newest first, and deciding one: a grant makes the booking
103
+ * as the agent asked for it; a refusal says why (approval_not_pending, overlap, a booking rule).
104
+ */
105
+ approvals?: {
106
+ list(tenantRef: string, limit: number): Promise<SlotlockApproval[]>;
107
+ decide(tenantRef: string, id: string, decision: 'grant' | 'decline', actor?: string, reason?: string): Promise<{
108
+ ok: true;
109
+ } | {
110
+ ok: false;
111
+ code: string;
112
+ }>;
113
+ };
114
+ /**
115
+ * The sandbox (RFC 0004 A6), for a test environment only: fill it with a scenario, empty it, and
116
+ * make what would lapse within some minutes lapse now. Each refuses a live tenant.
117
+ */
118
+ sandbox?: {
119
+ seed(tenantRef: string, scenario: SlotlockSandboxScenario): Promise<{
120
+ resources: number;
121
+ bookings: number;
122
+ }>;
123
+ reset(tenantRef: string): Promise<void>;
124
+ fastForward(tenantRef: string, minutes: number): Promise<{
125
+ holds: number;
126
+ approvals: number;
127
+ }>;
128
+ };
44
129
  /** The tenant's waitlist, oldest first, and leaving it (false when the entry is not the tenant's). */
45
130
  waitlist?: {
46
131
  list(tenantRef: string, limit: number): Promise<SlotlockWaitlistEntry[]>;
@@ -91,6 +176,18 @@ export interface SlotlockDashboardState {
91
176
  export interface SlotlockDashboardAssistantOptions {
92
177
  /** Seals each tenant's API key (SLOTLOCK_ASSISTANT_SECRET): 32+ random characters. */
93
178
  secret: string;
179
+ /**
180
+ * Secrets keys may still be sealed under (SLOTLOCK_ASSISTANT_PREVIOUS_SECRETS), newest first, at
181
+ * most SLOTLOCK_ASSISTANT_MAX_PREVIOUS_SECRETS: a key one of them opens is used, and sealed again
182
+ * under `secret` in the same request, so changing the secret makes no owner enter a key again.
183
+ * Drop an old secret once every tenant has used the assistant since the change, or keys still
184
+ * sealed under it stop opening.
185
+ */
186
+ previousSecrets?: readonly string[];
187
+ /** Each operation the assistant runs, for usage metering (the agent server's `onUsage`). */
188
+ onUsage?: (usage: SlotlockAgentUsage) => void;
189
+ /** Each message a member sends that the assistant answers, for usage metering. Never awaited. */
190
+ onMessage?: (tenantRef: string) => void;
94
191
  backend: SlotlockAgentCalendarBackend;
95
192
  /**
96
193
  * As the agent server's: false refuses an operation. Every offered operation by default. It also
@@ -135,11 +232,92 @@ export interface SlotlockDashboardAssistantOptions {
135
232
  /** How long a conversation is kept after its last message: 2 hours by default. */
136
233
  conversationTtlMs?: number;
137
234
  }
235
+ /**
236
+ * Hosted Slotlock's plans (plans.ts) and their billing: a Usage page with the tenant's plan and use,
237
+ * Upgrade (to a checkout page) and Manage (to a billing portal). Off unless set.
238
+ */
239
+ /**
240
+ * The tenant's plan and the deployment's default, read whether or not anything sells plans: with it
241
+ * the dashboard shows a Usage page (plan, limits and this month's use) and puts a tenant new to
242
+ * plans on the default before it adds what plans limit.
243
+ */
244
+ export interface SlotlockDashboardPlans {
245
+ /** The tenant's plan (its own, or the default once it applies), or null (the server's limits apply). */
246
+ plan(tenantRef: string): Promise<SlotlockTenantPlan | null>;
247
+ /** Puts a tenant new to plans on the default plan before it adds a key, resource or webhook endpoint. */
248
+ enrol?(tenantRef: string): Promise<void>;
249
+ /** The deployment's default plan and the date it applies to every tenant, or null. */
250
+ planDefault?(): Promise<{
251
+ plan: SlotlockPlan;
252
+ limits: SlotlockTenantPlan['limits'];
253
+ appliesFrom: Date;
254
+ } | null>;
255
+ /**
256
+ * Where asking for another plan goes (an https or mailto URL) when nothing sells plans here: with
257
+ * it, the Usage page offers Developer or Team by asking.
258
+ */
259
+ contact?: string;
260
+ }
261
+ export interface SlotlockDashboardBilling {
262
+ /** The tenant's plan (its own, or the default), or null when it has none (the server's limits apply). */
263
+ plan(tenantRef: string): Promise<SlotlockTenantPlan | null>;
264
+ /**
265
+ * Puts a tenant new to plans on the default plan before it adds a key, resource or webhook
266
+ * endpoint, while the default does not yet apply to every tenant (plans.ts).
267
+ */
268
+ enrol?(tenantRef: string): Promise<void>;
269
+ /** A checkout URL for the Developer plan, or the Team plan for an organization (with `teamPrices`). */
270
+ checkout(input: {
271
+ tenantRef: string;
272
+ interval: 'month' | 'year';
273
+ plan?: 'developer' | 'team';
274
+ customer: string | null;
275
+ successUrl: string;
276
+ cancelUrl: string;
277
+ }): Promise<string>;
278
+ /** A billing portal URL for the tenant's customer. */
279
+ portal(input: {
280
+ customer: string;
281
+ returnUrl: string;
282
+ }): Promise<string>;
283
+ /** The Developer plan's prices as shown, such as `$10`. */
284
+ prices: {
285
+ month: string;
286
+ year: string;
287
+ };
288
+ /**
289
+ * The Team plan's prices as shown (Teams on Cloud). With them, an organization's workspace is
290
+ * offered Team instead of Developer.
291
+ */
292
+ teamPrices?: {
293
+ month: string;
294
+ year: string;
295
+ };
296
+ /**
297
+ * Metered overage on Developer (SLOTLOCK_OVERAGE, overage.ts): its price as shown, such as `$2
298
+ * per 10,000 operations`, and the percent of the quota where use is refused.
299
+ */
300
+ overage?: {
301
+ price: string;
302
+ stopPercent: number;
303
+ };
304
+ /** Where a Custom (self-hosted) enquiry goes: an https or mailto URL. */
305
+ contact: string;
306
+ /** The https origins Upgrade and Manage redirect to, allowed in the policy's form-action. */
307
+ formOrigins: readonly string[];
308
+ }
138
309
  export interface SlotlockDashboardOptions {
139
310
  /** Where people reach the server; the dashboard lives at `<publicUrl>/dashboard`. */
140
311
  publicUrl: string;
141
312
  /** What the deployment is called in the dashboard: 1-60 characters. Defaults to `Slotlock`. */
142
313
  name?: string;
314
+ /**
315
+ * The terms people accept by signing in, an https URL. The sign-in page links them beside the
316
+ * button; without it, the page names no terms.
317
+ */
318
+ termsUrl?: string;
319
+ /** The privacy notice the sign-in page links, an https URL. */
320
+ privacyUrl?: string;
143
321
  /**
144
322
  * Where agents reach the server's MCP and A2A endpoints, which the dashboard shows, when it is
145
323
  * served from another origin (on Cloudflare Workers, with the server elsewhere). Defaults to
@@ -158,9 +336,25 @@ export interface SlotlockDashboardOptions {
158
336
  userUrl?: string;
159
337
  membershipsUrl?: string;
160
338
  };
339
+ /**
340
+ * Sign-in with Google (OpenID Connect), beside GitHub, for people without a GitHub account: each
341
+ * is their own tenant, `google:<subject>`. Not offered with a fixed list of `organizations`,
342
+ * which are GitHub's. Its redirect URI is the same `<publicUrl>/dashboard/callback`.
343
+ */
344
+ google?: {
345
+ clientId: string;
346
+ clientSecret: string;
347
+ authorizeUrl?: string;
348
+ tokenUrl?: string;
349
+ /** Google's signing keys (JWKS); https://www.googleapis.com/oauth2/v3/certs by default. */
350
+ jwksUrl?: string;
351
+ };
161
352
  /** Seals sessions and sign-in state and derives CSRF tokens: 32+ random characters. */
162
353
  sessionSecret: string;
163
- /** GitHub user ids (numbers, as strings) that may sign in, or `'*'` for every GitHub account. */
354
+ /**
355
+ * GitHub user ids (numbers, as strings) and Google accounts (`google:<subject>`) that may sign in,
356
+ * or `'*'` for every account.
357
+ */
164
358
  allowedUsers: '*' | readonly string[];
165
359
  /**
166
360
  * GitHub organization ids (numbers, as strings) this dashboard serves. With them, a tenant is an
@@ -171,8 +365,14 @@ export interface SlotlockDashboardOptions {
171
365
  * ends access at the next sign-in, at most a session (12 hours) later, and removing an
172
366
  * organization from this list signs its sessions out at once. Without it, each person is their
173
367
  * own tenant (`github:<id>`).
368
+ *
369
+ * `'enabled'` (Teams on Cloud, with `teams`): each person signs in to their own tenant and may
370
+ * switch to any organization they belong to that an owner turned on, on the Teams page; turning
371
+ * one off ends its use at once.
174
372
  */
175
- organizations?: readonly string[];
373
+ organizations?: readonly string[] | 'enabled';
374
+ /** The organizations turned on, with `organizations: 'enabled'` (teams.ts). */
375
+ teams?: SlotlockDashboardTeams;
176
376
  keys: SlotlockApiKeyStore;
177
377
  resources: SlotlockDashboardResources;
178
378
  /** Shared by every server behind this origin: see SlotlockDashboardState. */
@@ -181,6 +381,20 @@ export interface SlotlockDashboardOptions {
181
381
  availability?: readonly WeeklyAvailabilityRule[];
182
382
  /** The chat assistant; off unless set. */
183
383
  assistant?: SlotlockDashboardAssistantOptions;
384
+ /** Plans (hosted Slotlock): a Usage page, and enrolment on the default plan. Off unless set. */
385
+ plans?: SlotlockDashboardPlans;
386
+ /** Paying for plans through Stripe (hosted Slotlock); off unless set. Implies `plans` from its own. */
387
+ billing?: SlotlockDashboardBilling;
388
+ /**
389
+ * The deployment meters usage (SLOTLOCK_USAGE=on): the Usage page shows this month's, from the
390
+ * resources adapter's `usage`.
391
+ */
392
+ usage?: boolean;
393
+ /**
394
+ * The deployment holds tenants to their plans' monthly quotas (SLOTLOCK_QUOTAS=on): the Usage page
395
+ * shows this month's use against them, and warns from 80%. Needs `usage`.
396
+ */
397
+ quotas?: boolean;
184
398
  fetch?: typeof fetch;
185
399
  now?: () => number;
186
400
  /**
@@ -204,7 +418,13 @@ export interface SlotlockDashboard {
204
418
  * The dashboard's resources, in the store under each tenant's row-level security. Adds hold the cap
205
419
  * in the database (a READ COMMITTED transaction, under a per-tenant lock).
206
420
  */
207
- export declare function createSlotlockDashboardResources(store: SlotlockStore): SlotlockDashboardResources;
421
+ export declare function createSlotlockDashboardResources(store: SlotlockStore, options?: {
422
+ /**
423
+ * The agent backend (`createSlotlockStoreAgentBackend`) a granted approval's booking is made
424
+ * through: with it, the dashboard lists approvals and decides them.
425
+ */
426
+ agentBackend?: SlotlockAgentCalendarBackend;
427
+ }): SlotlockDashboardResources;
208
428
  /**
209
429
  * Records accepted write confirmations (`SlotlockAgentConfirmationOptions.consumeAcceptance`) in
210
430
  * Postgres, shared by every server on the database: true the first time a confirmation's nonce is