slotlock 0.9.0 → 0.10.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 (61) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/NOTICE +2 -0
  3. package/README.md +4 -2
  4. package/agent-store-backend.js +209 -15
  5. package/application-api.d.ts +10 -1
  6. package/application-api.js +213 -2
  7. package/availability-store.js +13 -5
  8. package/booking-rules.d.ts +11 -0
  9. package/booking-rules.js +21 -1
  10. package/config.d.ts +50 -0
  11. package/config.js +107 -0
  12. package/dashboard-scope.d.ts +7 -0
  13. package/dashboard-scope.js +21 -0
  14. package/dashboard.d.ts +97 -2
  15. package/dashboard.js +3068 -284
  16. package/ddl-identity.d.ts +25 -0
  17. package/ddl-identity.js +881 -0
  18. package/ddl-scim.d.ts +5 -0
  19. package/ddl-scim.js +740 -0
  20. package/ddl-sso.d.ts +15 -0
  21. package/ddl-sso.js +1033 -0
  22. package/ddl.d.ts +40 -9
  23. package/ddl.js +2123 -49
  24. package/domains.d.ts +117 -0
  25. package/domains.js +340 -0
  26. package/event-feed.d.ts +1 -1
  27. package/event-feed.js +6 -34
  28. package/holiday-import.d.ts +1 -1
  29. package/holiday-import.js +2 -3
  30. package/holidays.js +70 -11
  31. package/index.d.ts +12 -4
  32. package/index.js +8 -2
  33. package/invitation-email.d.ts +26 -0
  34. package/invitation-email.js +73 -0
  35. package/maintenance.d.ts +7 -2
  36. package/maintenance.js +15 -1
  37. package/oidc.d.ts +135 -0
  38. package/oidc.js +452 -0
  39. package/openapi.json +1886 -6
  40. package/package.json +2 -1
  41. package/scim-server.d.ts +19 -0
  42. package/scim-server.js +243 -0
  43. package/scim.d.ts +180 -0
  44. package/scim.js +416 -0
  45. package/self-host.d.ts +19 -6
  46. package/self-host.js +262 -7
  47. package/slotlock.manifest.json +1 -1
  48. package/store.d.ts +273 -2
  49. package/store.js +984 -72
  50. package/types.d.ts +33 -1
  51. package/types.js +68 -1
  52. package/workspace-domains.d.ts +146 -0
  53. package/workspace-domains.js +249 -0
  54. package/workspace-roles.d.ts +47 -0
  55. package/workspace-roles.js +304 -0
  56. package/workspace-scim.d.ts +89 -0
  57. package/workspace-scim.js +157 -0
  58. package/workspace-sso.d.ts +131 -0
  59. package/workspace-sso.js +180 -0
  60. package/workspaces.d.ts +240 -0
  61. package/workspaces.js +433 -0
@@ -12,7 +12,7 @@ import { readSlotlockHolidayCalendar } from './holiday-import.js';
12
12
  import { SLOTLOCK_OVERRIDE_RESOURCES, } from './availability-store.js';
13
13
  import { findEarliestRange } from './ranges.js';
14
14
  import { emitVAvailability, parseVAvailability, toJSCalendarEvent } from './interop.js';
15
- import { SLOTLOCK_INTERNAL_EVENT_OWNER_REF, calendarEventRollingHorizon, } from './store.js';
15
+ import { buildSlotlockTenantExport, slotlockAuditEventRecord, SLOTLOCK_INTERNAL_EVENT_OWNER_REF, calendarEventRollingHorizon, } from './store.js';
16
16
  import { slotlockResourceSearchWords, slotlockSearchResources } from './resource-search.js';
17
17
  import { maintainSlotlockTenant } from './maintenance.js';
18
18
  import { fastForwardSlotlockSandbox, raceSlotlockSandbox, resetSlotlockSandbox, seedSlotlockSandbox, } from './sandbox.js';
@@ -78,6 +78,52 @@ function assertActive(context) {
78
78
  if (context.signal.aborted)
79
79
  throw new SlotlockAgentOperationError('request_aborted', 503);
80
80
  }
81
+ /** A tenant export as the API gives it. */
82
+ function tenantExportOutput(made) {
83
+ return {
84
+ id: made.id,
85
+ status: made.status,
86
+ requested_by: made.requestedBy,
87
+ created_at: made.createdAt.toISOString(),
88
+ finished_at: made.finishedAt?.toISOString() ?? null,
89
+ expires_at: made.expiresAt.toISOString(),
90
+ parts: made.parts,
91
+ rows: made.rows,
92
+ bytes: made.bytes,
93
+ tables: made.tables,
94
+ error: made.error,
95
+ };
96
+ }
97
+ /** A key's own caps on what its agent holds (RFC 0004 A5), or undefined when it has none. */
98
+ function keyHoldCaps(context) {
99
+ const limits = context.principal.limits;
100
+ return limits?.maxHolds !== undefined || limits?.maxHeldHours !== undefined
101
+ ? {
102
+ ...(limits.maxHolds !== undefined ? { maxHolds: limits.maxHolds } : {}),
103
+ ...(limits.maxHeldHours !== undefined ? { maxHeldHours: limits.maxHeldHours } : {}),
104
+ }
105
+ : undefined;
106
+ }
107
+ /** The agent an approval holds time for, and its key's caps. */
108
+ function approvalHolder(context) {
109
+ const caps = keyHoldCaps(context);
110
+ return caps ? { ref: agentOwnerRef(context), caps } : { ref: agentOwnerRef(context) };
111
+ }
112
+ /** The person an access or erasure request names (RFC 0004 D3), as the schema checked it. */
113
+ function subjectOf(input) {
114
+ for (const kind of ['email', 'holder', 'contact', 'reference']) {
115
+ const value = input[kind];
116
+ if (typeof value === 'string')
117
+ return { [kind]: value };
118
+ }
119
+ throw new SlotlockAgentOperationError('invalid_subject', 400);
120
+ }
121
+ function subjectRefusal(error) {
122
+ if (error?.code === 'invalid_subject') {
123
+ throw new SlotlockAgentOperationError('invalid_subject', 400);
124
+ }
125
+ throw error;
126
+ }
81
127
  async function withTenant(store, context, callback, options) {
82
128
  assertActive(context);
83
129
  let value;
@@ -607,6 +653,8 @@ export function createSlotlockStoreAgentBackend(store, options) {
607
653
  source: 'agent-server',
608
654
  // The key's own caps, below the server's (RFC 0004 A5).
609
655
  ...(holderCaps ? { holderCaps } : {}),
656
+ // Its price is the tenant's (holdPrice), never the agent's own (RFC 0004 C3).
657
+ priceFromRules: true,
610
658
  // Booked only once your system records its payment (RFC 0004 C3).
611
659
  ...(input.payment_required === true ? { paymentRequired: true } : {}),
612
660
  ...(input.payment !== undefined ? { payment: amountInput(input.payment) } : {}),
@@ -665,6 +713,8 @@ export function createSlotlockStoreAgentBackend(store, options) {
665
713
  ttlMs: input.ttl_seconds * 1_000,
666
714
  source: 'slotlock:together',
667
715
  ...(holderCaps ? { holderCaps } : {}),
716
+ // Its price is the tenant's (holdPrice), never the agent's own (RFC 0004 C3).
717
+ priceFromRules: true,
668
718
  });
669
719
  // All or nothing: thrown, so the holds already made roll back with it.
670
720
  if (!result.ok) {
@@ -1893,6 +1943,22 @@ function withApprovalPolicies(store, options, maxApprovalMinutes, backend) {
1893
1943
  const windows = (await hoursWithin(tenantStore, context, [resource], reach, options)).get(resource.id) ?? [];
1894
1944
  return !coveredByOpenWindows(windows, { start, end });
1895
1945
  }
1946
+ /**
1947
+ * What an update may change without booking any time: everything else (time, resource, zone,
1948
+ * transparency, status, recurrence and its exceptions) is judged by what it would add.
1949
+ */
1950
+ const EVENT_DETAIL_FIELDS = new Set([
1951
+ 'event_id',
1952
+ 'expected_revision',
1953
+ 'expected_resource_id',
1954
+ 'idempotency_key',
1955
+ 'title',
1956
+ 'description',
1957
+ 'location',
1958
+ 'organizer',
1959
+ 'attendees',
1960
+ 'reminders',
1961
+ ]);
1896
1962
  /** The write's ask and the policies that may cover it, or null when none can. */
1897
1963
  async function plan(tenantStore, context, operation, input) {
1898
1964
  const tenantRef = context.principal.tenantRef;
@@ -1950,15 +2016,18 @@ function withApprovalPolicies(store, options, maxApprovalMinutes, backend) {
1950
2016
  const current = await tenantStore.getCalendarEvent({ tenantRef, ownerRef, externalRef: input.event_id });
1951
2017
  if (!current)
1952
2018
  return null;
2019
+ // Details alone book nothing. Any other change may: a move, or time the event did not hold
2020
+ // (made opaque or confirmed, given occurrences or fewer exceptions, another zone), judged
2021
+ // below by what the write would add to what the event holds.
2022
+ if (Object.keys(input).every((field) => EVENT_DETAIL_FIELDS.has(field)))
2023
+ return null;
1953
2024
  const resourceId = input.resource_id ?? current.resourceId;
1954
2025
  const start = input.starts_at ? new Date(input.starts_at) : current.start;
1955
2026
  const end = input.ends_at ? new Date(input.ends_at) : current.end;
1956
- // Only a move to another time or resource is a booking a policy can be about.
1957
- if (resourceId === current.resourceId && start.getTime() === current.start.getTime() && end.getTime() === current.end.getTime()) {
1958
- return null;
1959
- }
2027
+ const moved = resourceId !== current.resourceId || start.getTime() !== current.start.getTime() || end.getTime() !== current.end.getTime();
1960
2028
  asked = {
1961
- kind: 'move',
2029
+ // Time added in place is a booking of its own; a move is a move.
2030
+ kind: moved ? 'move' : 'single',
1962
2031
  stored: 'slotlock_update_event',
1963
2032
  resourceIds: [resourceId],
1964
2033
  externalRefs: [current.externalRef],
@@ -1995,18 +2064,20 @@ function withApprovalPolicies(store, options, maxApprovalMinutes, backend) {
1995
2064
  });
1996
2065
  if (asked.prior) {
1997
2066
  const { hold_id: _held, ...request } = input;
1998
- // Granted: the booking was made with this key, so the write answers it as a replay (and
1999
- // the store refuses another request with it). Without the hold the approval took over,
2000
- // as the grant made it.
2001
- if (asked.prior.status === 'granted')
2002
- return method.call(this, context, request);
2003
2067
  const kept = { ...asked.prior.request };
2004
2068
  // A pool booking is kept as the member it would take.
2005
2069
  if (operation === 'slotlock_book_pool')
2006
2070
  delete kept.resource_id;
2071
+ // The key answers only the request a person was asked about, granted or not: a grant
2072
+ // approves that booking, never another made with its key (a bundle's other resources, say,
2073
+ // which the store would take as new).
2007
2074
  if (JSON.stringify(canonical(kept)) !== JSON.stringify(canonical(request))) {
2008
2075
  throw new SlotlockAgentOperationError('idempotency_conflict', 409);
2009
2076
  }
2077
+ // Granted: the booking was made with this key, so the write answers it as a replay.
2078
+ // Without the hold the approval took over, as the grant made it.
2079
+ if (asked.prior.status === 'granted')
2080
+ return method.call(this, context, request);
2010
2081
  throw approvalAnswer(asked.prior);
2011
2082
  }
2012
2083
  const ask = asked.plan;
@@ -2030,6 +2101,11 @@ function withApprovalPolicies(store, options, maxApprovalMinutes, backend) {
2030
2101
  if (probed.replayed === true || occupied.length === 0) {
2031
2102
  return context.dryRun ? probed : method.call(this, context, input);
2032
2103
  }
2104
+ // Only time the event did not already hold needs a person: an edit or a move within its own
2105
+ // time books nothing new.
2106
+ const holds = uncovered(occupied, ask.before);
2107
+ if (holds.length === 0)
2108
+ return context.dryRun ? probed : method.call(this, context, input);
2033
2109
  const written = [...new Set(occupied.map((occupation) => occupation.resourceId))];
2034
2110
  const decided = await withTenant(store, { ...context, dryRun: false }, async (tenantStore) => {
2035
2111
  // A pool's policies are those of the member the write would take, and its pool.
@@ -2077,9 +2153,6 @@ function withApprovalPolicies(store, options, maxApprovalMinutes, backend) {
2077
2153
  expires_at: expiresAt.toISOString(),
2078
2154
  });
2079
2155
  }
2080
- const holds = uncovered(occupied, ask.before);
2081
- if (holds.length === 0)
2082
- return method.call(this, context, input);
2083
2156
  const chosen = ask.pool ? written[0] : undefined;
2084
2157
  const { hold_id: _hold, ...request } = input;
2085
2158
  const principal = context.principal;
@@ -2107,8 +2180,14 @@ function withApprovalPolicies(store, options, maxApprovalMinutes, backend) {
2107
2180
  end: ask.end,
2108
2181
  expiresAt,
2109
2182
  ...(ask.consumeHold ? { consumeHold: ask.consumeHold } : {}),
2183
+ // An agent's approvals hold time against its own caps (RFC 0004 A5); the tenant's own
2184
+ // systems have none.
2185
+ ...(forTenant(context) ? {} : { holder: approvalHolder(context) }),
2110
2186
  }), WRITE);
2111
2187
  if (!result.ok) {
2188
+ if (result.code === 'hold_limit_reached' || result.code === 'held_time_limit_reached') {
2189
+ throw new SlotlockAgentOperationError(result.code, 409, { limit: String(result.limit) });
2190
+ }
2112
2191
  if (result.code === 'approval_limit_reached') {
2113
2192
  throw new SlotlockAgentOperationError('approval_limit_reached', 429, { limit: String(result.limit ?? '') });
2114
2193
  }
@@ -2274,6 +2353,9 @@ function bookingRulesOutput(rules) {
2274
2353
  },
2275
2354
  }
2276
2355
  : {}),
2356
+ ...(rules.holdPrice !== undefined
2357
+ ? { hold_price: { amount_minor: rules.holdPrice.amountMinor, currency: rules.holdPrice.currency } }
2358
+ : {}),
2277
2359
  };
2278
2360
  }
2279
2361
  const dayWindowsOutput = (windows) => windows.map((window) => ({ start_minutes: window.startMinutes, duration_minutes: window.durationMinutes }));
@@ -2333,6 +2415,10 @@ function bookingRulesInput(value) {
2333
2415
  ...(fee.within_minutes !== undefined ? { withinMinutes: fee.within_minutes } : {}),
2334
2416
  };
2335
2417
  }
2418
+ if (input.hold_price !== undefined) {
2419
+ const price = input.hold_price;
2420
+ rules.holdPrice = { amountMinor: price.amount_minor, currency: price.currency };
2421
+ }
2336
2422
  return rules;
2337
2423
  }
2338
2424
  function resourceOutput(resource) {
@@ -3220,8 +3306,12 @@ export function createSlotlockStoreApplicationBackend(store, options = {}) {
3220
3306
  paymentRef: input.payment_ref,
3221
3307
  ...(input.received !== undefined ? { received: amountInput(input.received) } : {}),
3222
3308
  });
3223
- if (!result.ok)
3309
+ if (!result.ok) {
3310
+ if (result.code === 'payment_conflict') {
3311
+ throw new SlotlockAgentOperationError('payment_conflict', 409, { payment_ref: result.paymentRef });
3312
+ }
3224
3313
  throw applicationRefusal(result.code);
3314
+ }
3225
3315
  return { hold: holdOutput(result.hold), replayed: result.idempotent };
3226
3316
  }, WRITE);
3227
3317
  },
@@ -3858,6 +3948,110 @@ export function createSlotlockStoreApplicationBackend(store, options = {}) {
3858
3948
  surfaces: summary.surfaces,
3859
3949
  };
3860
3950
  },
3951
+ async listAuditEvents(context, input) {
3952
+ const tenantRef = tenantOf(context);
3953
+ const limit = input.limit ?? 50;
3954
+ return withTenant(store, context, async (tenant) => {
3955
+ // As the dashboard's export: where hosted plans apply, the Team plan's.
3956
+ const plan = await tenant.tenantPlan({ tenantRef });
3957
+ if (plan && (plan.plan !== 'team' || plan.status === 'canceled')) {
3958
+ throw new SlotlockAgentOperationError('team_plan_required', 403);
3959
+ }
3960
+ const events = await tenant.listAuditEvents({
3961
+ tenantRef,
3962
+ limit,
3963
+ ...(typeof input.cursor === 'string' ? { beforeId: input.cursor } : {}),
3964
+ ...(typeof input.action === 'string' ? { action: input.action } : {}),
3965
+ ...(typeof input.actor_ref === 'string' ? { actorRef: input.actor_ref } : {}),
3966
+ });
3967
+ return {
3968
+ events: events.map(slotlockAuditEventRecord),
3969
+ // A full page may have more after it; the next then answers none.
3970
+ next_cursor: events.length === limit ? (events.at(-1)?.id ?? null) : null,
3971
+ };
3972
+ });
3973
+ },
3974
+ async verifyAuditLog(context) {
3975
+ const verified = await withTenant(store, context, (tenant) => tenant.verifyAuditChain({ tenantRef: tenantOf(context) }));
3976
+ return { checked: verified.checked, broken_at: verified.brokenAt };
3977
+ },
3978
+ async createTenantExport(context) {
3979
+ const tenantRef = tenantOf(context);
3980
+ const keyId = context.principal.keyId;
3981
+ const actor = typeof keyId === 'string' && /^[0-9a-f-]{36}$/i.test(keyId)
3982
+ ? { actorType: 'api_key', actorRef: `api_key:${keyId}` }
3983
+ : { actorType: 'system', actorRef: 'application' };
3984
+ const created = await withTenant(store, context, async (tenant) => {
3985
+ const made = await tenant.createTenantExport({ tenantRef, requestedBy: actor.actorRef }).catch((error) => {
3986
+ const code = error?.code;
3987
+ if (code === 'export_in_progress' || code === 'export_limit_reached') {
3988
+ throw new SlotlockAgentOperationError(code, 409);
3989
+ }
3990
+ throw error;
3991
+ });
3992
+ await tenant.recordAuditEvent({
3993
+ tenantRef,
3994
+ event: { action: 'export.created', ...actor, targetType: 'export', targetRef: made.id },
3995
+ });
3996
+ return made;
3997
+ }, WRITE);
3998
+ // Built after the answer; maintenance builds one a stopped server left pending.
3999
+ void buildSlotlockTenantExport(store, tenantRef, created.id);
4000
+ return { export: tenantExportOutput(created) };
4001
+ },
4002
+ async listTenantExports(context) {
4003
+ const exports = await withTenant(store, context, (tenant) => tenant.listTenantExports({ tenantRef: tenantOf(context) }));
4004
+ return { exports: exports.slice(0, 100).map(tenantExportOutput) };
4005
+ },
4006
+ async getTenantExport(context, input) {
4007
+ const found = await withTenant(store, context, (tenant) => tenant.getTenantExport({ tenantRef: tenantOf(context), exportId: input.export_id }));
4008
+ if (!found)
4009
+ throw new SlotlockAgentOperationError('export_not_found', 404);
4010
+ return { export: tenantExportOutput(found) };
4011
+ },
4012
+ async getTenantExportPart(context, input) {
4013
+ const part = Number(input.part);
4014
+ const read = await withTenant(store, context, (tenant) => tenant.getTenantExportPart({ tenantRef: tenantOf(context), exportId: input.export_id, part }));
4015
+ if (!read.ok)
4016
+ throw new SlotlockAgentOperationError(read.code, read.code === 'export_not_ready' ? 409 : 404);
4017
+ return {
4018
+ export_id: input.export_id,
4019
+ part,
4020
+ table: read.table,
4021
+ rows: read.lines.map((line) => JSON.parse(line)),
4022
+ };
4023
+ },
4024
+ async deleteTenantExport(context, input) {
4025
+ const deleted = await withTenant(store, context, (tenant) => tenant.deleteTenantExport({ tenantRef: tenantOf(context), exportId: input.export_id }), WRITE);
4026
+ return { deleted };
4027
+ },
4028
+ async exportSubject(context, input) {
4029
+ const subject = subjectOf(input);
4030
+ const exported = await withTenant(store, context, (tenant) => tenant.exportSubject({ tenantRef: tenantOf(context), subject, limit: 1_000 })).catch(subjectRefusal);
4031
+ return { records: exported.records, truncated: exported.truncated };
4032
+ },
4033
+ async eraseSubject(context, input) {
4034
+ const subject = subjectOf(input);
4035
+ const tenantRef = tenantOf(context);
4036
+ const keyId = context.principal.keyId;
4037
+ return withTenant(store, context, async (tenant) => {
4038
+ const { erased } = await tenant.eraseSubject({ tenantRef, subject }).catch(subjectRefusal);
4039
+ // Who erased whom is the workspace's to know, but never the person: how they were named,
4040
+ // and what went.
4041
+ await tenant.recordAuditEvent({
4042
+ tenantRef,
4043
+ event: {
4044
+ action: 'subject.erased',
4045
+ ...(typeof keyId === 'string' && /^[0-9a-f-]{36}$/i.test(keyId)
4046
+ ? { actorType: 'api_key', actorRef: `api_key:${keyId}` }
4047
+ : { actorType: 'system', actorRef: 'application' }),
4048
+ targetType: 'subject',
4049
+ metadata: { by: Object.keys(subject)[0], ...Object.fromEntries(Object.entries(erased).filter(([, rows]) => rows > 0)) },
4050
+ },
4051
+ });
4052
+ return { erased };
4053
+ }, WRITE);
4054
+ },
3861
4055
  async enableWebhook(context, input) {
3862
4056
  return withTenant(store, context, async (tenant) => {
3863
4057
  const enabled = await tenant.enableWebhookEndpoint({
@@ -2,7 +2,7 @@ import { type SlotlockChangeOrigin } from './event-feed.js';
2
2
  import { z } from 'zod';
3
3
  import type { SlotlockAgentPrincipal } from './agent-server.js';
4
4
  import type { SlotlockTraceContext } from './mcp-modern.js';
5
- export type SlotlockApplicationOperation = 'slotlock_create_resource' | 'slotlock_update_resource' | 'slotlock_list_locations' | 'slotlock_create_location' | 'slotlock_update_location' | 'slotlock_delete_location' | 'slotlock_list_groups' | 'slotlock_create_group' | 'slotlock_delete_group' | 'slotlock_add_group_member' | 'slotlock_remove_group_member' | 'slotlock_get_resource' | 'slotlock_archive_resource' | 'slotlock_restore_resource' | 'slotlock_delete_resource' | 'slotlock_list_busy' | 'slotlock_set_resource_availability' | 'slotlock_set_tenant_availability' | 'slotlock_get_availability_defaults' | 'slotlock_set_location_availability' | 'slotlock_list_availability_exceptions' | 'slotlock_create_availability_exception' | 'slotlock_delete_availability_exception' | 'slotlock_import_holidays' | 'slotlock_list_holiday_calendars' | 'slotlock_delete_holiday_calendar' | 'slotlock_export_resource_availability' | 'slotlock_import_resource_availability' | 'slotlock_set_resource_booking_rules' | 'slotlock_set_resource_pool' | 'slotlock_set_resource_parent' | 'slotlock_list_pools' | 'slotlock_set_pool_order' | 'slotlock_get_booking_rule_defaults' | 'slotlock_set_tenant_booking_rules' | 'slotlock_set_pool_booking_rules' | 'slotlock_get_resource_history' | 'slotlock_get_coverage' | 'slotlock_record_coverage_failure' | 'slotlock_delete_coverage' | 'slotlock_put_source_busy' | 'slotlock_report_coverage_conflict' | 'slotlock_get_reservation_history' | 'slotlock_get_tenant_event_history' | 'slotlock_acquire_hold' | 'slotlock_get_hold' | 'slotlock_list_holds' | 'slotlock_confirm_hold' | 'slotlock_mark_hold_paid' | 'slotlock_release_hold' | 'slotlock_get_reservation' | 'slotlock_list_reservations' | 'slotlock_put_reservation' | 'slotlock_cancel_reservation' | 'slotlock_check_in_reservation' | 'slotlock_put_contact' | 'slotlock_get_contact' | 'slotlock_list_contacts' | 'slotlock_delete_contact' | 'slotlock_join_waitlist' | 'slotlock_list_waitlist' | 'slotlock_leave_waitlist' | 'slotlock_decline_waitlist_offer' | 'slotlock_tenant_book_together' | 'slotlock_tenant_book_pool' | 'slotlock_record_coverage' | 'slotlock_list_tenant_events' | 'slotlock_get_tenant_event' | 'slotlock_export_tenant_event' | 'slotlock_cancel_tenant_event' | 'slotlock_get_tenant' | 'slotlock_erase_tenant' | 'slotlock_run_maintenance' | 'slotlock_list_changes' | 'slotlock_create_webhook' | 'slotlock_list_webhooks' | 'slotlock_delete_webhook' | 'slotlock_enable_webhook' | 'slotlock_get_usage' | 'slotlock_list_approval_policies' | 'slotlock_put_approval_policy' | 'slotlock_delete_approval_policy' | 'slotlock_list_approvals' | 'slotlock_get_approval' | 'slotlock_decide_approval' | 'slotlock_sandbox_seed' | 'slotlock_sandbox_reset' | 'slotlock_sandbox_fast_forward' | 'slotlock_sandbox_race' | 'slotlock_partner_put_account' | 'slotlock_partner_list_accounts' | 'slotlock_partner_get_account' | 'slotlock_partner_list_keys' | 'slotlock_partner_create_key' | 'slotlock_partner_rotate_key' | 'slotlock_partner_revoke_key' | 'slotlock_partner_suspend_account' | 'slotlock_partner_unsuspend_account' | 'slotlock_partner_erase_account' | 'slotlock_partner_get_usage' | 'slotlock_partner_reconcile_usage';
5
+ export type SlotlockApplicationOperation = 'slotlock_create_resource' | 'slotlock_update_resource' | 'slotlock_list_locations' | 'slotlock_create_location' | 'slotlock_update_location' | 'slotlock_delete_location' | 'slotlock_list_groups' | 'slotlock_create_group' | 'slotlock_delete_group' | 'slotlock_add_group_member' | 'slotlock_remove_group_member' | 'slotlock_get_resource' | 'slotlock_archive_resource' | 'slotlock_restore_resource' | 'slotlock_delete_resource' | 'slotlock_list_busy' | 'slotlock_set_resource_availability' | 'slotlock_set_tenant_availability' | 'slotlock_get_availability_defaults' | 'slotlock_set_location_availability' | 'slotlock_list_availability_exceptions' | 'slotlock_create_availability_exception' | 'slotlock_delete_availability_exception' | 'slotlock_import_holidays' | 'slotlock_list_holiday_calendars' | 'slotlock_delete_holiday_calendar' | 'slotlock_export_resource_availability' | 'slotlock_import_resource_availability' | 'slotlock_set_resource_booking_rules' | 'slotlock_set_resource_pool' | 'slotlock_set_resource_parent' | 'slotlock_list_pools' | 'slotlock_set_pool_order' | 'slotlock_get_booking_rule_defaults' | 'slotlock_set_tenant_booking_rules' | 'slotlock_set_pool_booking_rules' | 'slotlock_get_resource_history' | 'slotlock_get_coverage' | 'slotlock_record_coverage_failure' | 'slotlock_delete_coverage' | 'slotlock_put_source_busy' | 'slotlock_report_coverage_conflict' | 'slotlock_get_reservation_history' | 'slotlock_get_tenant_event_history' | 'slotlock_acquire_hold' | 'slotlock_get_hold' | 'slotlock_list_holds' | 'slotlock_confirm_hold' | 'slotlock_mark_hold_paid' | 'slotlock_release_hold' | 'slotlock_get_reservation' | 'slotlock_list_reservations' | 'slotlock_put_reservation' | 'slotlock_cancel_reservation' | 'slotlock_check_in_reservation' | 'slotlock_put_contact' | 'slotlock_get_contact' | 'slotlock_list_contacts' | 'slotlock_delete_contact' | 'slotlock_join_waitlist' | 'slotlock_list_waitlist' | 'slotlock_leave_waitlist' | 'slotlock_decline_waitlist_offer' | 'slotlock_tenant_book_together' | 'slotlock_tenant_book_pool' | 'slotlock_record_coverage' | 'slotlock_list_tenant_events' | 'slotlock_get_tenant_event' | 'slotlock_export_tenant_event' | 'slotlock_cancel_tenant_event' | 'slotlock_get_tenant' | 'slotlock_erase_tenant' | 'slotlock_run_maintenance' | 'slotlock_list_changes' | 'slotlock_create_webhook' | 'slotlock_list_webhooks' | 'slotlock_delete_webhook' | 'slotlock_enable_webhook' | 'slotlock_get_usage' | 'slotlock_list_audit_events' | 'slotlock_verify_audit_log' | 'slotlock_export_subject' | 'slotlock_erase_subject' | 'slotlock_create_tenant_export' | 'slotlock_list_tenant_exports' | 'slotlock_get_tenant_export' | 'slotlock_get_tenant_export_part' | 'slotlock_delete_tenant_export' | 'slotlock_list_approval_policies' | 'slotlock_put_approval_policy' | 'slotlock_delete_approval_policy' | 'slotlock_list_approvals' | 'slotlock_get_approval' | 'slotlock_decide_approval' | 'slotlock_sandbox_seed' | 'slotlock_sandbox_reset' | 'slotlock_sandbox_fast_forward' | 'slotlock_sandbox_race' | 'slotlock_partner_put_account' | 'slotlock_partner_list_accounts' | 'slotlock_partner_get_account' | 'slotlock_partner_list_keys' | 'slotlock_partner_create_key' | 'slotlock_partner_rotate_key' | 'slotlock_partner_revoke_key' | 'slotlock_partner_suspend_account' | 'slotlock_partner_unsuspend_account' | 'slotlock_partner_erase_account' | 'slotlock_partner_get_usage' | 'slotlock_partner_reconcile_usage';
6
6
  /** The scope an application operation needs, but the partner API's (`slotlockApplicationScope`). */
7
7
  export declare const SLOTLOCK_APPLICATION_SCOPE = "admin";
8
8
  /** The scope one application operation needs: `partner` for the partner API's, else `admin`. */
@@ -113,6 +113,15 @@ export interface SlotlockApplicationBackend {
113
113
  deleteWebhook: Handler;
114
114
  enableWebhook: Handler;
115
115
  getUsage: Handler;
116
+ listAuditEvents: Handler;
117
+ verifyAuditLog: Handler;
118
+ exportSubject: Handler;
119
+ eraseSubject: Handler;
120
+ createTenantExport: Handler;
121
+ listTenantExports: Handler;
122
+ getTenantExport: Handler;
123
+ getTenantExportPart: Handler;
124
+ deleteTenantExport: Handler;
116
125
  listApprovalPolicies: Handler;
117
126
  putApprovalPolicy: Handler;
118
127
  deleteApprovalPolicy: Handler;
@@ -14,6 +14,7 @@ import { SLOTLOCK_APPROVAL_KINDS, SLOTLOCK_MAX_APPROVAL_MINUTES, SLOTLOCK_MAX_AP
14
14
  import { SLOTLOCK_PARTNER_ACCOUNT, SLOTLOCK_PARTNER_PLAN } from './partner-names.js';
15
15
  import { SLOTLOCK_EVENT_OUTPUT } from './event-schema.js';
16
16
  import { SLOTLOCK_CALENDAR_HORIZON_DAYS, SLOTLOCK_SOURCE_BUSY_MAX } from './rules.js';
17
+ import { SLOTLOCK_SUBJECT_TABLES, SLOTLOCK_TENANT_EXPORT_LEFT_OUT, SLOTLOCK_TENANT_EXPORT_TABLES, } from './types.js';
17
18
  import { SLOTLOCK_GROUPING_REF, SLOTLOCK_MAX_GROUPS_PER_RESOURCE, SLOTLOCK_MAX_LOCATION_DEPTH, SLOTLOCK_MAX_LOCATIONS, SLOTLOCK_MAX_RESOURCE_GROUPS, SLOTLOCK_POOL_ORDERS, SLOTLOCK_RESOURCE_KINDS, SLOTLOCK_RESOURCE_LABEL, SLOTLOCK_RESOURCE_PROFILE_LIMITS, } from './resource-profile.js';
18
19
  /** The scope an application operation needs, but the partner API's (`slotlockApplicationScope`). */
19
20
  export const SLOTLOCK_APPLICATION_SCOPE = 'admin';
@@ -27,6 +28,8 @@ export function slotlockApplicationScope(definition) {
27
28
  */
28
29
  const NOT_DRY_RUNNABLE = new Set([
29
30
  'slotlock_erase_tenant',
31
+ // Built after the answer, outside the request's transaction.
32
+ 'slotlock_create_tenant_export',
30
33
  'slotlock_run_maintenance',
31
34
  'slotlock_sandbox_seed',
32
35
  'slotlock_sandbox_reset',
@@ -207,6 +210,17 @@ const BOOKING_RULES = z
207
210
  })
208
211
  .strict()
209
212
  .optional(),
213
+ /**
214
+ * What holding it costs, your price (RFC 0004 C3): an agent's hold waits for this payment,
215
+ * whatever it asks, and an agent books it only from such a hold, once paid.
216
+ */
217
+ hold_price: z
218
+ .object({
219
+ amount_minor: z.number().int().min(1).max(1_000_000_000_000),
220
+ currency: z.string().regex(/^[A-Z]{3}$/),
221
+ })
222
+ .strict()
223
+ .optional(),
210
224
  })
211
225
  .strict();
212
226
  /** Default booking rules for the tenant or a pool: a resource's own rules but check-in. */
@@ -530,6 +544,67 @@ const HOLD = z
530
544
  paid_amount: PAYMENT_AMOUNT.nullable().optional(),
531
545
  })
532
546
  .strict();
547
+ /** An event of the workspace audit log (RFC 0004 D2): who did what, to what, from where. */
548
+ const AUDIT_EVENT = z
549
+ .object({
550
+ id: z.string().uuid(),
551
+ occurred_at: DATE_TIME,
552
+ /** `<area>.<verb>`, such as `api_key.created`, `authz.denied` or `subject.erased`. */
553
+ action: z.string().min(3).max(100),
554
+ actor_type: z.enum(['person', 'api_key', 'assistant', 'system']),
555
+ /** `github:<id>`, `google:<subject>`, `api_key:<id>`: a reference, never a name or an email. */
556
+ actor_ref: z.string().min(1).max(500),
557
+ actor_role: z.enum(['owner', 'admin', 'developer', 'member', 'viewer', 'billing']).nullable(),
558
+ target_type: z.string().min(1).max(32).nullable(),
559
+ target_ref: z.string().min(1).max(500).nullable(),
560
+ outcome: z.enum(['success', 'denied']),
561
+ reason: z.string().min(1).max(200).nullable(),
562
+ ip_address: z.string().min(1).max(64).nullable(),
563
+ user_agent: z.string().min(1).max(256).nullable(),
564
+ metadata: z.record(z.string(), z.unknown()),
565
+ /** Its place in the tenant's chain, and its SHA-256 (hex) over the one before and itself. */
566
+ chain_index: z.number().int().min(1).nullable(),
567
+ hash: z.string().regex(/^[0-9a-f]{64}$/).nullable(),
568
+ })
569
+ .strict();
570
+ /** Whom an access or erasure request is about (RFC 0004 D3): exactly one identifier. */
571
+ const SUBJECT = z
572
+ .object({
573
+ /** Contacts, event attendees and organizers with this email (ignoring case). */
574
+ email: z.string().trim().min(3).max(320).regex(/^[^\s@]+@[^\s@]+$/, 'must be an email address').optional(),
575
+ /** Holds, bookings and waitlist entries held under this holder reference. */
576
+ holder: z.string().min(1).max(500).optional(),
577
+ /** The contact kept under this reference (slotlock_put_contact). */
578
+ contact: z
579
+ .string()
580
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/, 'must be 1-200 letters, digits, . _ : or -, starting with a letter or digit')
581
+ .optional(),
582
+ /** The booking or event with this reference. */
583
+ reference: z.string().min(1).max(500).optional(),
584
+ })
585
+ .strict()
586
+ .refine((input) => Object.values(input).filter((value) => value !== undefined).length === 1, 'Name the person by exactly one of email, holder, contact or reference');
587
+ /** A copy of everything the tenant keeps (RFC 0004 D3), as built or being built. */
588
+ const TENANT_EXPORT = z
589
+ .object({
590
+ id: z.string().uuid(),
591
+ /** pending until it is built, then ready, or failed (error says why). */
592
+ status: z.enum(['pending', 'ready', 'failed']),
593
+ requested_by: z.string().min(1).max(200).nullable(),
594
+ created_at: DATE_TIME,
595
+ finished_at: DATE_TIME.nullable(),
596
+ /** When it is deleted. */
597
+ expires_at: DATE_TIME,
598
+ /** Parts to read, 1 to parts, with slotlock_get_tenant_export_part. */
599
+ parts: z.number().int().min(0),
600
+ rows: z.number().int().min(0),
601
+ bytes: z.number().int().min(0),
602
+ /** Rows by table, for the tables with any. */
603
+ tables: z.record(z.string(), z.number().int().min(0)),
604
+ error: z.string().min(1).max(200).nullable(),
605
+ })
606
+ .strict();
607
+ const EXPORT_ID = z.string().uuid();
533
608
  /** A person a booking may name to be told about it (RFC 0004 P3), by the tenant's reference. */
534
609
  const CONTACT_REF = z
535
610
  .string()
@@ -1123,7 +1198,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
1123
1198
  {
1124
1199
  name: 'slotlock_import_holidays',
1125
1200
  title: 'Import a holiday calendar',
1126
- description: "Close the days an iCalendar holiday calendar names, for the whole tenant, one location or one resource: the text in icalendar, or an https url the server fetches (at most 1 MB, 10 seconds; private addresses refused). Each all-day event closes the days it covers, and each occurrence of a recurring one within from to to (default today to two years on, at most three years); timed and cancelled events are skipped. The calendar is kept by name at its scope (name, by default its own X-WR-CALNAME, else the url's host and path, else Holidays): importing it again updates its closures instead of adding more, and deletes the ones it made that it no longer names within the span (removed), so a holiday it dropped goes too. With subscribe (and a url), the server keeps the url and imports it again every 30 days. At most 50 calendars a tenant (409 holiday_calendars_limit_reached). 400 invalid_holiday_calendar for text that is not one; 400 holiday_calendar_unreachable when the url cannot be read; 400 holiday_url_not_served where the server fetches no urls.",
1201
+ description: "Close the days an iCalendar holiday calendar names, for the whole tenant, one location or one resource: the text in icalendar, or an https url the server fetches (at most 1 MB, 10 seconds; private addresses refused). Each all-day event closes the days it covers, and each occurrence of a recurring one within from to to (default today to two years on, at most three years); timed and cancelled events are skipped. The calendar is kept by name at its scope (name, by default its own X-WR-CALNAME, else the url's host, else Holidays): importing it again updates its closures instead of adding more, and deletes the ones it made that it no longer names within the span (removed), so a holiday it dropped goes too. With subscribe (and a url), the server keeps the url and imports it again every 30 days. At most 50 calendars a tenant (409 holiday_calendars_limit_reached). 400 invalid_holiday_calendar for text that is not one, or one that repeats more often than daily, daily on days of the month or year, or too much to read; 400 holiday_calendar_unreachable when the url cannot be read; 400 holiday_url_not_served where the server fetches no urls.",
1127
1202
  method: 'POST',
1128
1203
  path: '/v1/availability/holidays',
1129
1204
  input: z
@@ -1265,7 +1340,7 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
1265
1340
  {
1266
1341
  name: 'slotlock_mark_hold_paid',
1267
1342
  title: 'Record a hold as paid',
1268
- description: "Record that a hold made with payment_required is paid, by the payment's reference at your processor (a Stripe PaymentIntent id, say), once you have verified it: the hold may then be confirmed. Slotlock records payment state and never moves money. The same reference again answers the same hold (replayed: true), whatever became of it since (confirmed, booked as an event, expired): the payment is kept apart from the hold. Another reference answers 409 payment_conflict. When the hold names its payment, received must be at least that amount in that currency (409 payment_amount_mismatch). The feed says so (hold.paid). A hold that does not wait for payment answers 409 payment_not_required; one that expired before it was paid, 409 hold_expired (refund the payment: its time may be someone else's); one released before it was paid, or unknown, 404 hold_not_found.",
1343
+ description: "Record that a hold made with payment_required is paid, by the payment's reference at your processor (a Stripe PaymentIntent id, say), once you have verified it: the hold may then be confirmed. Slotlock records payment state and never moves money. The same reference again answers the same hold (replayed: true) while it is held or once it is booked (confirmed, or booked as an event), whatever became of the booking since: the payment is kept apart from the hold. A hold that ended without a booking after it was paid (expired, or let go) answers 409 hold_ended: the payment was never applied, so refund it; the feed's hold.expired or hold.released says so too (payment_unapplied). Another reference answers 409 payment_conflict, naming the one recorded (payment_ref). When the hold names its payment, received must be at least that amount in that currency (409 payment_amount_mismatch). The feed says so (hold.paid). A hold that does not wait for payment answers 409 payment_not_required; one that expired before it was paid, 409 hold_expired (refund the payment: its time may be someone else's); one released before it was paid, or unknown, 404 hold_not_found.",
1269
1344
  method: 'POST',
1270
1345
  path: '/v1/holds/{hold_id}/paid',
1271
1346
  input: z
@@ -1989,6 +2064,142 @@ export const SLOTLOCK_APPLICATION_OPERATIONS = Object.freeze([
1989
2064
  invoke: 'getUsage',
1990
2065
  example: { from: '2026-11-01', to: '2026-11-30' },
1991
2066
  },
2067
+ // The audit log and requests about one person (RFC 0004 D2, D3).
2068
+ {
2069
+ name: 'slotlock_list_audit_events',
2070
+ title: 'List audit events',
2071
+ description: "The workspace audit log, newest first: who did what to its people, keys, webhooks, billing and settings, from where, and whether it was refused. Actors are references (github:<id>, api_key:<id>), never names. Each event carries its place in the tenant's hash chain and its hash (slotlock_verify_audit_log checks them). Only events of action or actor_ref when given; pass next_cursor as cursor for the next page, until it is null. Events are kept 180 days, 400 on the Team plan. Where hosted plans apply, the log is read this way on the Team plan only (403 team_plan_required).",
2072
+ method: 'GET',
2073
+ path: '/v1/audit',
2074
+ input: z
2075
+ .object({
2076
+ limit: z.number().int().min(1).max(200).default(50),
2077
+ cursor: z.string().uuid().optional(),
2078
+ action: z.string().regex(/^[a-z][a-z_]{0,31}(\.[a-z][a-z_]{0,31}){1,2}$/, 'must be an action, like api_key.created').optional(),
2079
+ actor_ref: z.string().min(1).max(500).optional(),
2080
+ })
2081
+ .strict(),
2082
+ output: z.object({ events: z.array(AUDIT_EVENT).max(200), next_cursor: z.string().uuid().nullable() }).strict(),
2083
+ risk: { readOnly: true, destructive: false, idempotent: true },
2084
+ invoke: 'listAuditEvents',
2085
+ example: { limit: 50, action: 'api_key.created' },
2086
+ },
2087
+ {
2088
+ name: 'slotlock_verify_audit_log',
2089
+ title: 'Verify the audit log',
2090
+ description: "Check the workspace audit log's hash chain from its oldest event kept: each event numbered after the one before, linked to its hash, and hashed over its own content. checked counts the events that held; broken_at is the number of the first that does not (an event changed, removed or reordered outside Slotlock), or null when every one does.",
2091
+ method: 'GET',
2092
+ path: '/v1/audit/verify',
2093
+ input: z.object({}).strict(),
2094
+ output: z.object({ checked: z.number().int().min(0), broken_at: z.number().int().min(1).nullable() }).strict(),
2095
+ risk: { readOnly: true, destructive: false, idempotent: true },
2096
+ invoke: 'verifyAuditLog',
2097
+ example: {},
2098
+ },
2099
+ {
2100
+ name: 'slotlock_export_subject',
2101
+ title: "Export a person's data",
2102
+ description: `What the tenant keeps about one person, for an access request: the rows that name them, as kept, by table (${SLOTLOCK_SUBJECT_TABLES.join(', ')}), up to 1,000 a table (truncated: true when a table had more). Name them by exactly one of email (contacts, event attendees and organizers, ignoring case), holder (holds, bookings and waitlist entries), contact (a contact's reference) or reference (a booking's or an event's). It lists what slotlock_erase_subject would delete or take them out of: the bookings, changes and approvals that name their contacts too. An event lists its time and title, and an approval its decision, never the other people they name. 400 invalid_subject when no identifier, or more than one, is given.`,
2103
+ method: 'POST',
2104
+ path: '/v1/subjects/export',
2105
+ input: SUBJECT,
2106
+ output: z
2107
+ .object({
2108
+ records: z
2109
+ .object(Object.fromEntries(SLOTLOCK_SUBJECT_TABLES.map((table) => [table, z.array(z.record(z.string(), z.unknown())).max(1_000)])))
2110
+ .strict(),
2111
+ truncated: z.boolean(),
2112
+ })
2113
+ .strict(),
2114
+ risk: { readOnly: true, destructive: false, idempotent: true },
2115
+ invoke: 'exportSubject',
2116
+ example: { email: 'ada@example.com' },
2117
+ },
2118
+ {
2119
+ name: 'slotlock_erase_subject',
2120
+ title: 'Erase a person',
2121
+ description: "Take one person out of the tenant's data, for an erasure request, at once: their contacts, attendee entries and waitlist entries are deleted; their email leaves events' attendee and organizer lists and approvals' requests; their holder reference and booking reference become one erased-<32 hex> name wherever they were (holds, bookings, ended bookings, events, the change history, approvals), and their contact's reference leaves bookings and changes. Bookings stay, as time taken, and nobody is told: the feed gets no change. Name them as for slotlock_export_subject. erased counts, by table, the rows deleted or changed; erasing again changes nothing. Recorded in the audit log (subject.erased) without the identifier. 400 invalid_subject when no identifier, or more than one, is given.",
2122
+ method: 'POST',
2123
+ path: '/v1/subjects/erase',
2124
+ input: SUBJECT,
2125
+ output: z
2126
+ .object({
2127
+ erased: z
2128
+ .object(Object.fromEntries(SLOTLOCK_SUBJECT_TABLES.map((table) => [table, z.number().int().min(0)])))
2129
+ .strict(),
2130
+ })
2131
+ .strict(),
2132
+ risk: { readOnly: false, destructive: true, idempotent: true },
2133
+ invoke: 'eraseSubject',
2134
+ example: { email: 'ada@example.com' },
2135
+ },
2136
+ {
2137
+ name: 'slotlock_create_tenant_export',
2138
+ title: 'Export the tenant',
2139
+ description: `Ask for a copy of everything the tenant keeps, to download: every table's rows as kept, read at one moment (${SLOTLOCK_TENANT_EXPORT_TABLES.join(', ')}). Secrets stay out: webhooks' signing secrets, the assistant's provider key and API keys' digests. ${Object.keys(SLOTLOCK_TENANT_EXPORT_LEFT_OUT).length} tables are left out as derived, internal or one member's own (each member's conversations with the assistant). It answers pending at once and is built in the background: slotlock_get_tenant_export says when it is ready, then read its parts with slotlock_get_tenant_export_part. Kept 24 hours once ready, then deleted. One is built at a time (409 export_in_progress) and five are kept (409 export_limit_reached); one past 512 MB fails (error export_too_large) and its operator runs slotlock tenant export. Recorded in the audit log (export.created).`,
2140
+ method: 'POST',
2141
+ path: '/v1/tenant/exports',
2142
+ input: z.object({}).strict(),
2143
+ output: z.object({ export: TENANT_EXPORT }).strict(),
2144
+ risk: { readOnly: false, destructive: false, idempotent: false },
2145
+ invoke: 'createTenantExport',
2146
+ example: {},
2147
+ },
2148
+ {
2149
+ name: 'slotlock_list_tenant_exports',
2150
+ title: 'List tenant exports',
2151
+ description: "The tenant's exports, newest first, until each is deleted: pending, ready (with how many parts and rows) or failed.",
2152
+ method: 'GET',
2153
+ path: '/v1/tenant/exports',
2154
+ input: z.object({}).strict(),
2155
+ output: z.object({ exports: z.array(TENANT_EXPORT).max(100) }).strict(),
2156
+ risk: { readOnly: true, destructive: false, idempotent: true },
2157
+ invoke: 'listTenantExports',
2158
+ example: {},
2159
+ },
2160
+ {
2161
+ name: 'slotlock_get_tenant_export',
2162
+ title: 'Get a tenant export',
2163
+ description: 'One of the tenant\'s exports: whether it is ready, its parts, rows and bytes, and rows by table. An unknown or deleted one answers 404 export_not_found.',
2164
+ method: 'GET',
2165
+ path: '/v1/tenant/exports/{export_id}',
2166
+ input: z.object({ export_id: EXPORT_ID }).strict(),
2167
+ output: z.object({ export: TENANT_EXPORT }).strict(),
2168
+ risk: { readOnly: true, destructive: false, idempotent: true },
2169
+ invoke: 'getTenantExport',
2170
+ example: { export_id: '0b6c8d1e-4f2a-4c3b-9d5e-7f8a9b0c1d2e' },
2171
+ },
2172
+ {
2173
+ name: 'slotlock_get_tenant_export_part',
2174
+ title: 'Read a part of a tenant export',
2175
+ description: 'One part of a ready export, 1 to its parts: one table\'s rows, each an object as Slotlock keeps it (without the tenant). Write each part\'s rows as JSON lines to its table\'s file to keep the export as NDJSON. 409 export_not_ready before it is ready; 404 export_not_found or export_part_not_found.',
2176
+ method: 'GET',
2177
+ path: '/v1/tenant/exports/{export_id}/parts/{part}',
2178
+ input: z.object({ export_id: EXPORT_ID, part: z.string().regex(/^[1-9][0-9]{0,6}$/, 'must be a part number, 1 or more') }).strict(),
2179
+ output: z
2180
+ .object({
2181
+ export_id: EXPORT_ID,
2182
+ part: z.number().int().min(1),
2183
+ table: z.string().min(1).max(63),
2184
+ rows: z.array(z.record(z.string(), z.unknown())).max(5_000),
2185
+ })
2186
+ .strict(),
2187
+ risk: { readOnly: true, destructive: false, idempotent: true },
2188
+ invoke: 'getTenantExportPart',
2189
+ example: { export_id: '0b6c8d1e-4f2a-4c3b-9d5e-7f8a9b0c1d2e', part: '1' },
2190
+ },
2191
+ {
2192
+ name: 'slotlock_delete_tenant_export',
2193
+ title: 'Delete a tenant export',
2194
+ description: 'Delete one of the tenant\'s exports at once, with every part, rather than waiting for it to expire. deleted is false when there was none.',
2195
+ method: 'DELETE',
2196
+ path: '/v1/tenant/exports/{export_id}',
2197
+ input: z.object({ export_id: EXPORT_ID }).strict(),
2198
+ output: z.object({ deleted: z.boolean() }).strict(),
2199
+ risk: { readOnly: false, destructive: true, idempotent: true },
2200
+ invoke: 'deleteTenantExport',
2201
+ example: { export_id: '0b6c8d1e-4f2a-4c3b-9d5e-7f8a9b0c1d2e' },
2202
+ },
1992
2203
  // Approval policies (RFC 0004 A4): writes a person approves before they are made.
1993
2204
  {
1994
2205
  name: 'slotlock_list_approval_policies',