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.
Files changed (100) hide show
  1. package/CHANGELOG.md +492 -0
  2. package/CONTRIBUTING.md +12 -5
  3. package/README.md +50 -1544
  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 +560 -49
  8. package/agent-store-backend.d.ts +39 -7
  9. package/agent-store-backend.js +1439 -104
  10. package/api-keys.d.ts +69 -3
  11. package/api-keys.js +218 -29
  12. package/application-api.d.ts +59 -6
  13. package/application-api.js +842 -39
  14. package/approvals.d.ts +88 -0
  15. package/approvals.js +293 -0
  16. package/assistant.d.ts +58 -27
  17. package/assistant.js +151 -53
  18. package/billing.d.ts +114 -0
  19. package/billing.js +258 -0
  20. package/booking-rules.d.ts +30 -41
  21. package/booking-rules.js +17 -25
  22. package/change-watch.d.ts +1 -0
  23. package/change-watch.js +1 -1
  24. package/cli.js +3 -4
  25. package/config.d.ts +42 -6
  26. package/config.js +114 -7
  27. package/dashboard-icon.d.ts +1 -0
  28. package/dashboard-icon.js +5 -0
  29. package/dashboard.d.ts +203 -7
  30. package/dashboard.js +1558 -64
  31. package/ddl.d.ts +67 -35
  32. package/ddl.js +2732 -162
  33. package/engine.d.ts +6 -9
  34. package/engine.js +5 -9
  35. package/event-feed.d.ts +53 -35
  36. package/event-feed.js +201 -64
  37. package/event-schema.js +6 -9
  38. package/hooks.d.ts +2 -4
  39. package/hooks.js +2 -4
  40. package/ical-worker-thread.js +3 -4
  41. package/ical-worker.d.ts +8 -10
  42. package/ical-worker.js +7 -9
  43. package/index.d.ts +15 -4
  44. package/index.js +8 -2
  45. package/interop.js +1 -4
  46. package/maintenance.d.ts +24 -10
  47. package/maintenance.js +59 -28
  48. package/mcp-app.d.ts +7 -9
  49. package/mcp-app.js +7 -9
  50. package/mcp-modern.d.ts +14 -16
  51. package/mcp-modern.js +23 -27
  52. package/metrics.d.ts +41 -0
  53. package/metrics.js +22 -1
  54. package/node-server.d.ts +9 -9
  55. package/node-server.js +25 -34
  56. package/openapi.json +14675 -5957
  57. package/otel.d.ts +86 -0
  58. package/otel.js +273 -0
  59. package/overage.d.ts +33 -0
  60. package/overage.js +115 -0
  61. package/package-version.js +2 -4
  62. package/package.json +1 -1
  63. package/partner-billing.d.ts +81 -0
  64. package/partner-billing.js +333 -0
  65. package/partner-names.d.ts +8 -0
  66. package/partner-names.js +10 -0
  67. package/partners.d.ts +147 -0
  68. package/partners.js +375 -0
  69. package/plans.d.ts +92 -0
  70. package/plans.js +115 -0
  71. package/quotas.d.ts +108 -0
  72. package/quotas.js +258 -0
  73. package/rate-limit.d.ts +27 -2
  74. package/rate-limit.js +106 -23
  75. package/resource-profile.d.ts +135 -0
  76. package/resource-profile.js +263 -0
  77. package/rest-api.d.ts +15 -14
  78. package/rest-api.js +79 -15
  79. package/rules.d.ts +6 -7
  80. package/rules.js +30 -42
  81. package/sandbox.d.ts +70 -0
  82. package/sandbox.js +295 -0
  83. package/self-host.d.ts +46 -4
  84. package/self-host.js +990 -41
  85. package/slotlock.manifest.json +1 -1
  86. package/store.d.ts +482 -165
  87. package/store.js +1460 -484
  88. package/sync.d.ts +44 -59
  89. package/sync.js +325 -469
  90. package/teams.d.ts +25 -0
  91. package/teams.js +63 -0
  92. package/timezone.d.ts +25 -33
  93. package/timezone.js +50 -53
  94. package/types.d.ts +130 -57
  95. package/usage.d.ts +106 -0
  96. package/usage.js +238 -0
  97. package/webhooks.d.ts +22 -25
  98. package/webhooks.js +35 -40
  99. package/weekly-hours.d.ts +3 -0
  100. package/weekly-hours.js +1 -1
package/quotas.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ import { type SlotlockAgentOperation, SlotlockAgentOperationError } from './agent-server.js';
2
+ import type { SlotlockApplicationOperation } from './application-api.js';
3
+ import { SLOTLOCK_QUOTA_METRICS, SLOTLOCK_QUOTA_NOTICE_PERCENTS } from './ddl.js';
4
+ import type { SlotlockPlanQuotas } from './plans.js';
5
+ import type { SlotlockSql } from './store.js';
6
+ import type { SlotlockRecordedUsageMetric } from './usage.js';
7
+ export { SLOTLOCK_QUOTA_METRICS, SLOTLOCK_QUOTA_NOTICE_PERCENTS };
8
+ export type SlotlockQuotaMetric = (typeof SLOTLOCK_QUOTA_METRICS)[number];
9
+ /**
10
+ * The operations that can make a booking (a reservation, an event, a confirmed hold) or hold a slot
11
+ * for one: refused once the bookings quota is used up. Everything else, reads and cancellations
12
+ * included, runs until the operations quota is.
13
+ */
14
+ export declare const SLOTLOCK_BOOKING_OPERATIONS: readonly (SlotlockAgentOperation | SlotlockApplicationOperation)[];
15
+ /**
16
+ * Operations a quota never refuses, though each still counts: reading the month's usage, so a
17
+ * tenant can always see where it stands, and erasing the tenant, which no allowance may hold up.
18
+ */
19
+ export declare const SLOTLOCK_QUOTA_EXEMPT_OPERATIONS: readonly SlotlockApplicationOperation[];
20
+ /** A tenant's operations and bookings so far this month, as the database holds them. */
21
+ export interface SlotlockQuotaUse {
22
+ operations: number;
23
+ bookings: number;
24
+ }
25
+ /** One quota of a tenant this month. */
26
+ export interface SlotlockQuotaStanding {
27
+ metric: SlotlockQuotaMetric;
28
+ /** The plan's quota. */
29
+ quota: number;
30
+ /** Where use is refused: the quota, or past it on a plan that allows a margin (Developer 110%). */
31
+ stop: number;
32
+ used: number;
33
+ /** The month's first day, `YYYY-MM-DD` (UTC). */
34
+ periodStart: string;
35
+ /** When the month ends and the counts start again (the next month's first instant, UTC). */
36
+ resetsAt: Date;
37
+ }
38
+ export type SlotlockQuotaCheck =
39
+ /** `standings` is empty for a tenant with no plan, which no quota binds. */
40
+ {
41
+ ok: true;
42
+ standings: SlotlockQuotaStanding[];
43
+ } | {
44
+ ok: false;
45
+ exceeded: SlotlockQuotaStanding;
46
+ standings: SlotlockQuotaStanding[];
47
+ };
48
+ /** A share of a quota the tenant reached, to note once (slotlock.note_usage_threshold). */
49
+ export interface SlotlockQuotaNotice {
50
+ tenantRef: string;
51
+ metric: SlotlockQuotaMetric;
52
+ percent: (typeof SLOTLOCK_QUOTA_NOTICE_PERCENTS)[number];
53
+ used: number;
54
+ quota: number;
55
+ /** The month's first day, `YYYY-MM-DD`. */
56
+ periodStart: string;
57
+ }
58
+ export interface SlotlockQuotasOptions {
59
+ /**
60
+ * The quotas the tenant's plan gives it (plans.ts readSlotlockTenantQuotas: a partner account's
61
+ * own, else its plan's), or null when it has none: then no quota binds it.
62
+ */
63
+ plan(tenantRef: string): Promise<SlotlockPlanQuotas | null>;
64
+ /** The tenant's operations and bookings from `fromDay` (`YYYY-MM-DD`) until now. */
65
+ used(tenantRef: string, fromDay: string): Promise<SlotlockQuotaUse>;
66
+ /**
67
+ * What this server counted and has not recorded yet (the usage meter's `local`). Without it, only
68
+ * what the database holds is counted.
69
+ */
70
+ local?(tenantRef: string, metric: SlotlockRecordedUsageMetric, fromDay: string): {
71
+ unrecorded: number;
72
+ counted: number;
73
+ };
74
+ /** Note a share reached; the database sends each once. Not awaited; a failure is tried again. */
75
+ notice?(notice: SlotlockQuotaNotice): Promise<unknown>;
76
+ /** A plan or count that could not be read (the check then lets the operation run), or a notice. */
77
+ onError?(error: unknown): void;
78
+ /** Milliseconds since the epoch, for tests; defaults to `Date.now`. */
79
+ now?: () => number;
80
+ }
81
+ export interface SlotlockQuotas {
82
+ /**
83
+ * Where the tenant stands against its quotas, and whether `operation` may run. Never throws: a
84
+ * plan or count it cannot read lets the operation run, on the last counts it read if any.
85
+ */
86
+ check(args: {
87
+ tenantRef: string;
88
+ operation: SlotlockAgentOperation | SlotlockApplicationOperation;
89
+ }): Promise<SlotlockQuotaCheck>;
90
+ }
91
+ /** The month an instant falls in: its first day and the next month's first instant, UTC. */
92
+ export declare function slotlockQuotaPeriod(at: number): {
93
+ periodStart: string;
94
+ resetsAt: Date;
95
+ };
96
+ export declare function createSlotlockQuotas(options: SlotlockQuotasOptions): SlotlockQuotas;
97
+ /** `slotlock.tenant_quota_usage`: the tenant's operations and bookings from `fromDay` until now. */
98
+ export declare function readSlotlockQuotaUse(sql: SlotlockSql, tenantRef: string, fromDay: string): Promise<SlotlockQuotaUse>;
99
+ /** `slotlock.note_usage_threshold`: true when this was the first notice of that share this month. */
100
+ export declare function noteSlotlockQuotaThreshold(sql: SlotlockSql, notice: SlotlockQuotaNotice): Promise<boolean>;
101
+ /** The refusal of an operation over a quota: `quota_exceeded` (429), naming the quota and its reset. */
102
+ export declare function slotlockQuotaRefusal(exceeded: SlotlockQuotaStanding): SlotlockAgentOperationError;
103
+ /** The answer's fields for a tenant's quotas: RateLimit list members, and Retry-After when refused. */
104
+ export declare function slotlockQuotaFields(standings: readonly SlotlockQuotaStanding[], at: number): {
105
+ rateLimit: string[];
106
+ policy: string[];
107
+ retryAfter: number;
108
+ };
package/quotas.js ADDED
@@ -0,0 +1,258 @@
1
+ // Monthly quotas (RFC 0003 part 3): each hosted plan allows so many operations and bookings a UTC
2
+ // calendar month (plans.ts SLOTLOCK_PLAN_QUOTAS). The server checks each operation against them as
3
+ // it is authorized: past the operations quota every operation is refused, past the bookings quota
4
+ // only those that can make a booking, so a tenant can still read and cancel. A refusal is
5
+ // `quota_exceeded` (429) with Retry-After at the month's end (rate-limit.ts renders it), and at 80%
6
+ // and 100% of each quota the tenant's feed records `usage.threshold_reached` once.
7
+ //
8
+ // What a check counts: the database's month so far (slotlock.tenant_quota_usage: the usage recorded,
9
+ // and the bookings in the feed not yet rolled up), read at most every 30 seconds, and every 2 near
10
+ // the stop; plus this server's operations the database does not hold yet (the usage meter's), and
11
+ // the booking operations it ran since the read. Other servers' operations of the last 15 seconds or
12
+ // so are not seen, so a busy tenant can go a little past its stop; never by more than that.
13
+ // Off unless the deployment asks for it (SLOTLOCK_QUOTAS=on, with SLOTLOCK_USAGE=on).
14
+ import { SlotlockAgentOperationError } from './agent-server.js';
15
+ import { SLOTLOCK_QUOTA_METRICS, SLOTLOCK_QUOTA_NOTICE_PERCENTS } from './ddl.js';
16
+ export { SLOTLOCK_QUOTA_METRICS, SLOTLOCK_QUOTA_NOTICE_PERCENTS };
17
+ /**
18
+ * The operations that can make a booking (a reservation, an event, a confirmed hold) or hold a slot
19
+ * for one: refused once the bookings quota is used up. Everything else, reads and cancellations
20
+ * included, runs until the operations quota is.
21
+ */
22
+ export const SLOTLOCK_BOOKING_OPERATIONS = Object.freeze([
23
+ 'slotlock_hold_slots',
24
+ 'slotlock_book_together',
25
+ 'slotlock_book_pool',
26
+ 'slotlock_create_event',
27
+ 'slotlock_acquire_hold',
28
+ 'slotlock_confirm_hold',
29
+ 'slotlock_put_reservation',
30
+ ]);
31
+ /**
32
+ * Operations a quota never refuses, though each still counts: reading the month's usage, so a
33
+ * tenant can always see where it stands, and erasing the tenant, which no allowance may hold up.
34
+ */
35
+ export const SLOTLOCK_QUOTA_EXEMPT_OPERATIONS = Object.freeze([
36
+ 'slotlock_get_usage',
37
+ 'slotlock_erase_tenant',
38
+ ]);
39
+ /** How long a tenant's plan and counts are trusted, and how long near its stop. */
40
+ const REFRESH_MS = 30_000;
41
+ const NEAR_STOP_REFRESH_MS = 2_000;
42
+ /** The share of a stop past which counts are read again at NEAR_STOP_REFRESH_MS. */
43
+ const NEAR_STOP = 0.9;
44
+ /** Tenants one server remembers; past this the least recently checked tenth is dropped. */
45
+ const MAX_TENANTS = 50_000;
46
+ /** The month an instant falls in: its first day and the next month's first instant, UTC. */
47
+ export function slotlockQuotaPeriod(at) {
48
+ const date = new Date(at);
49
+ const year = date.getUTCFullYear();
50
+ const month = date.getUTCMonth();
51
+ return {
52
+ periodStart: new Date(Date.UTC(year, month, 1)).toISOString().slice(0, 10),
53
+ resetsAt: new Date(Date.UTC(year, month + 1, 1)),
54
+ };
55
+ }
56
+ export function createSlotlockQuotas(options) {
57
+ const now = options.now ?? Date.now;
58
+ const entries = new Map();
59
+ const reading = new Map();
60
+ const noted = new Set();
61
+ const report = (error) => {
62
+ try {
63
+ options.onError?.(error);
64
+ }
65
+ catch {
66
+ // A reporter's failure changes nothing.
67
+ }
68
+ };
69
+ async function read(tenantRef, periodStart) {
70
+ const at = now();
71
+ const local = options.local?.(tenantRef, 'operations', periodStart) ?? { unrecorded: 0, counted: 0 };
72
+ try {
73
+ const plan = await options.plan(tenantRef);
74
+ const used = plan ? await options.used(tenantRef, periodStart) : { operations: 0, bookings: 0 };
75
+ const entry = {
76
+ plan,
77
+ periodStart,
78
+ at,
79
+ used: { operations: Number(used.operations) || 0, bookings: Number(used.bookings) || 0 },
80
+ unrecorded: local.unrecorded,
81
+ countedAtRead: local.counted,
82
+ bookingsSinceRead: 0,
83
+ };
84
+ if (!entries.has(tenantRef) && entries.size >= MAX_TENANTS) {
85
+ for (const key of [...entries.keys()].slice(0, Math.ceil(MAX_TENANTS / 10)))
86
+ entries.delete(key);
87
+ }
88
+ entries.delete(tenantRef);
89
+ entries.set(tenantRef, entry);
90
+ return entry;
91
+ }
92
+ catch (error) {
93
+ report(error);
94
+ return null;
95
+ }
96
+ }
97
+ function standingsOf(tenantRef, entry, resetsAt) {
98
+ if (!entry.plan)
99
+ return [];
100
+ const quotas = entry.plan;
101
+ const local = options.local?.(tenantRef, 'operations', entry.periodStart);
102
+ // The read's unrecorded operations, and every one this meter counted since. A running total
103
+ // below the one at the read means the meter started its totals again: all of it is new.
104
+ const counted = local?.counted ?? entry.countedAtRead;
105
+ const operations = entry.used.operations + entry.unrecorded + (counted >= entry.countedAtRead ? counted - entry.countedAtRead : counted);
106
+ const bookings = entry.used.bookings + entry.bookingsSinceRead;
107
+ const standing = (metric, used) => ({
108
+ metric,
109
+ quota: quotas[metric],
110
+ stop: Math.floor((quotas[metric] * quotas.stopPercent) / 100),
111
+ used,
112
+ periodStart: entry.periodStart,
113
+ resetsAt,
114
+ });
115
+ return [standing('operations', operations), standing('bookings', bookings)];
116
+ }
117
+ function notify(tenantRef, standings) {
118
+ if (!options.notice)
119
+ return;
120
+ for (const standing of standings) {
121
+ for (const percent of SLOTLOCK_QUOTA_NOTICE_PERCENTS) {
122
+ if (standing.used * 100 < standing.quota * percent)
123
+ continue;
124
+ const key = `${tenantRef}\0${standing.periodStart}\0${standing.metric}\0${percent}`;
125
+ if (noted.has(key))
126
+ continue;
127
+ if (noted.size >= MAX_TENANTS)
128
+ noted.clear();
129
+ noted.add(key);
130
+ try {
131
+ options
132
+ .notice({
133
+ tenantRef,
134
+ metric: standing.metric,
135
+ percent,
136
+ used: standing.used,
137
+ quota: standing.quota,
138
+ periodStart: standing.periodStart,
139
+ })
140
+ .catch((error) => {
141
+ // Tried again at the next check that finds the share reached.
142
+ noted.delete(key);
143
+ report(error);
144
+ });
145
+ }
146
+ catch (error) {
147
+ noted.delete(key);
148
+ report(error);
149
+ }
150
+ }
151
+ }
152
+ }
153
+ return {
154
+ async check({ tenantRef, operation }) {
155
+ const at = now();
156
+ const { periodStart, resetsAt } = slotlockQuotaPeriod(at);
157
+ let entry = entries.get(tenantRef);
158
+ const fresh = (found) => {
159
+ if (found.periodStart !== periodStart)
160
+ return false;
161
+ const age = at - found.at;
162
+ if (age < 0 || age >= REFRESH_MS)
163
+ return false;
164
+ if (age < NEAR_STOP_REFRESH_MS)
165
+ return true;
166
+ // Near a stop, the counts are read again sooner: the estimate since the read is rougher.
167
+ return !standingsOf(tenantRef, found, resetsAt).some((standing) => standing.used >= standing.stop * NEAR_STOP);
168
+ };
169
+ if (!entry || !fresh(entry)) {
170
+ let pending = reading.get(tenantRef);
171
+ if (!pending) {
172
+ pending = read(tenantRef, periodStart).finally(() => reading.delete(tenantRef));
173
+ reading.set(tenantRef, pending);
174
+ }
175
+ const read_ = await pending;
176
+ // A read that failed keeps the last counts of this month, if any; else nothing binds.
177
+ if (read_)
178
+ entry = read_;
179
+ else if (entry?.periodStart !== periodStart)
180
+ return { ok: true, standings: [] };
181
+ }
182
+ if (!entry)
183
+ return { ok: true, standings: [] };
184
+ let standings = standingsOf(tenantRef, entry, resetsAt);
185
+ // Booking operations let run since the read are attempts, and some were refused or failed:
186
+ // before refusing one on that estimate, read the database's count again.
187
+ const booking = SLOTLOCK_BOOKING_OPERATIONS.includes(operation);
188
+ const bookingStanding = standings[1];
189
+ if (booking && entry.bookingsSinceRead > 0 && bookingStanding && bookingStanding.used >= bookingStanding.stop) {
190
+ let pending = reading.get(tenantRef);
191
+ if (!pending) {
192
+ pending = read(tenantRef, periodStart).finally(() => reading.delete(tenantRef));
193
+ reading.set(tenantRef, pending);
194
+ }
195
+ const again = await pending;
196
+ if (again) {
197
+ entry = again;
198
+ standings = standingsOf(tenantRef, entry, resetsAt);
199
+ }
200
+ }
201
+ const current = entry;
202
+ // Bookings are noticed as the database last counted them, never on the estimate since.
203
+ notify(tenantRef, standings.map((standing) => standing.metric === 'bookings' ? { ...standing, used: current.used.bookings } : standing));
204
+ if (SLOTLOCK_QUOTA_EXEMPT_OPERATIONS.includes(operation)) {
205
+ return { ok: true, standings };
206
+ }
207
+ const [operations, bookings] = standings;
208
+ if (operations && operations.used >= operations.stop) {
209
+ return { ok: false, exceeded: operations, standings };
210
+ }
211
+ if (booking && bookings && bookings.used >= bookings.stop) {
212
+ return { ok: false, exceeded: bookings, standings };
213
+ }
214
+ if (booking && current.plan)
215
+ current.bookingsSinceRead += 1;
216
+ return { ok: true, standings };
217
+ },
218
+ };
219
+ }
220
+ /** `slotlock.tenant_quota_usage`: the tenant's operations and bookings from `fromDay` until now. */
221
+ export async function readSlotlockQuotaUse(sql, tenantRef, fromDay) {
222
+ const [row] = await sql `
223
+ SELECT operations, bookings FROM slotlock.tenant_quota_usage(${tenantRef}::text, ${fromDay}::date)`;
224
+ return { operations: Number(row?.operations ?? 0), bookings: Number(row?.bookings ?? 0) };
225
+ }
226
+ /** `slotlock.note_usage_threshold`: true when this was the first notice of that share this month. */
227
+ export async function noteSlotlockQuotaThreshold(sql, notice) {
228
+ const [row] = await sql `
229
+ SELECT slotlock.note_usage_threshold(
230
+ ${notice.tenantRef}::text, ${notice.periodStart}::date, ${notice.metric}::text,
231
+ ${notice.percent}::integer, ${notice.used}::bigint, ${notice.quota}::bigint) AS noted`;
232
+ return row?.noted === true;
233
+ }
234
+ /** The refusal of an operation over a quota: `quota_exceeded` (429), naming the quota and its reset. */
235
+ export function slotlockQuotaRefusal(exceeded) {
236
+ return new SlotlockAgentOperationError('quota_exceeded', 429, {
237
+ metric: exceeded.metric,
238
+ quota: String(exceeded.quota),
239
+ stop: String(exceeded.stop),
240
+ used: String(exceeded.used),
241
+ resets_at: exceeded.resetsAt.toISOString(),
242
+ });
243
+ }
244
+ /** The answer's fields for a tenant's quotas: RateLimit list members, and Retry-After when refused. */
245
+ export function slotlockQuotaFields(standings, at) {
246
+ const rateLimit = [];
247
+ const policy = [];
248
+ let retryAfter = 1;
249
+ for (const standing of standings) {
250
+ const name = `${standing.metric}_month`;
251
+ const seconds = Math.max(1, Math.ceil((standing.resetsAt.getTime() - at) / 1_000));
252
+ const window = Math.round((standing.resetsAt.getTime() - Date.parse(`${standing.periodStart}T00:00:00Z`)) / 1_000);
253
+ rateLimit.push(`"${name}";r=${Math.max(0, standing.stop - standing.used)};t=${seconds}`);
254
+ policy.push(`"${name}";q=${standing.stop};w=${window}`);
255
+ retryAfter = Math.max(retryAfter, seconds);
256
+ }
257
+ return { rateLimit, policy, retryAfter };
258
+ }
package/rate-limit.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type SlotlockAgentOperation, type SlotlockAgentPrincipal } from './agent-server.js';
2
2
  import { type SlotlockApplicationOperation } from './application-api.js';
3
- import type { SlotlockLimits, SlotlockRate } from './config.js';
3
+ import { type SlotlockLimits, type SlotlockRate } from './config.js';
4
4
  import type { SlotlockNodeServerFetchTarget } from './node-server.js';
5
- export type SlotlockRefusalReason = 'auth_refused' | 'rate_limited' | 'lookup_overflow' | 'series_writes_busy' | 'maintenance_busy' | 'request_too_large';
5
+ import { type SlotlockQuotas } from './quotas.js';
6
+ export type SlotlockRefusalReason = 'auth_refused' | 'rate_limited' | 'lookup_overflow' | 'series_writes_busy' | 'maintenance_busy' | 'request_too_large' | 'quota_exceeded';
6
7
  /** One `refusals` log line: counts since the last one, and the most refused clients as pseudonyms. */
7
8
  export interface SlotlockRefusalCounts extends Record<SlotlockRefusalReason, number> {
8
9
  window_seconds: number;
@@ -16,6 +17,11 @@ export interface SlotlockRequestLimitsOptions {
16
17
  limits: SlotlockLimits;
17
18
  /** A lower-case request field a trusted proxy sets to the client address (config.ts). */
18
19
  trustedProxyHeader?: string;
20
+ /**
21
+ * With `trustedProxyHeader`, the value the proxy also sets in x-slotlock-proxy-proof
22
+ * (SLOTLOCK_TRUSTED_PROXY_SECRET). A request without it exactly is counted by its connection.
23
+ */
24
+ trustedProxySecret?: string;
19
25
  /** The public URL's path, without a trailing slash (`''` at the origin root). */
20
26
  basePath: string;
21
27
  /** Paths, under the base path, of the dashboard's sign-in and its callback, when it is served. */
@@ -40,6 +46,14 @@ export interface SlotlockRequestLimitsOptions {
40
46
  shared?: {
41
47
  take(bucket: string, rate: SlotlockRate): Promise<boolean>;
42
48
  };
49
+ /**
50
+ * Monthly quotas (quotas.ts, SLOTLOCK_QUOTAS=on), checked by `quota`: an operation over its
51
+ * tenant's quota is refused `quota_exceeded` (429), with Retry-After at the month's end, and
52
+ * every operation's answer lists the tenant's quotas in its RateLimit fields.
53
+ */
54
+ quotas?: SlotlockQuotas;
55
+ /** Milliseconds since the epoch, for the quotas' reset; defaults to `Date.now`. */
56
+ clock?: () => number;
43
57
  }
44
58
  export interface SlotlockRequestLimits {
45
59
  /** The agent server's `authenticate`: the request's lookup from `fetch`, else a lookup now. */
@@ -48,6 +62,8 @@ export interface SlotlockRequestLimits {
48
62
  consumeRateLimit(args: {
49
63
  principal: SlotlockAgentPrincipal;
50
64
  operation: SlotlockAgentOperation | 'protocol';
65
+ /** A dry run of a write, charged as a read. */
66
+ dryRun?: boolean;
51
67
  }): Promise<boolean>;
52
68
  /**
53
69
  * For `authorize`: when the operation may expand a recurring series, or runs maintenance, take its
@@ -60,6 +76,15 @@ export interface SlotlockRequestLimits {
60
76
  operation: SlotlockAgentOperation | SlotlockApplicationOperation;
61
77
  input: Record<string, unknown>;
62
78
  }): Promise<void>;
79
+ /**
80
+ * For `authorize`: with `quotas`, refuse an operation over its tenant's monthly quota, throwing a
81
+ * `SlotlockAgentOperationError` (`quota_exceeded`, 429) that names the metric, the quota, the stop
82
+ * and when the month resets. Without `quotas`, or for a tenant on no plan, nothing.
83
+ */
84
+ quota(args: {
85
+ principal: SlotlockAgentPrincipal;
86
+ operation: SlotlockAgentOperation | SlotlockApplicationOperation;
87
+ }): Promise<void>;
63
88
  /** Put the limits in front of `target`: the listener's fetch target. */
64
89
  serve(target: SlotlockNodeServerFetchTarget): SlotlockNodeServerFetchTarget;
65
90
  /** The client address a request reaching `serve`'s target was counted as (the dashboard's). */