@acms-subir/core 0.3.9 → 0.3.11

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 (161) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/cli/common.mjs +2 -2
  3. package/cli/legacy.mjs +2 -1
  4. package/cli/main.mjs +17 -3
  5. package/cli/migrate.mjs +102 -0
  6. package/cli/migrations.generated.mjs +1044 -0
  7. package/cli/mobile-doctor.mjs +9 -0
  8. package/cli/monitor.mjs +189 -0
  9. package/cli/preview.mjs +1 -1
  10. package/deploy/Dockerfile +5 -1
  11. package/deploy/compose.yml +21 -0
  12. package/docs/live-video.md +1 -1
  13. package/docs/mobile.md +23 -2
  14. package/docs/theme-authoring.md +6 -0
  15. package/integration.mjs +74 -4
  16. package/migrate.mjs +5 -0
  17. package/package.json +9 -11
  18. package/src/actions/index.ts +11 -31
  19. package/src/components/AppAccess.tsx +117 -0
  20. package/src/components/DesignManager.tsx +29 -10
  21. package/src/components/EmailSettings.tsx +53 -11
  22. package/src/components/MonitoringSettings.tsx +112 -0
  23. package/src/components/ThemeTransfer.tsx +211 -0
  24. package/src/components/auth/ClerkSession.astro +80 -0
  25. package/src/components/auth/ReverifyForms.astro +61 -0
  26. package/src/components/auth/clerk.ts +169 -0
  27. package/src/components/import/ImportExport.tsx +16 -6
  28. package/src/components/import/runner.ts +3 -0
  29. package/src/layouts/Admin.astro +42 -12
  30. package/src/layouts/Site.astro +6 -1
  31. package/src/lib/api.ts +8 -4
  32. package/src/lib/auth/accounts.ts +94 -0
  33. package/src/lib/auth/config.ts +22 -0
  34. package/src/lib/auth/login-limit.ts +15 -0
  35. package/src/lib/auth/request.ts +161 -0
  36. package/src/lib/auth/store.ts +72 -0
  37. package/src/lib/auth.ts +3 -0
  38. package/src/lib/backup/archive.ts +26 -0
  39. package/src/lib/backup/content.ts +1 -1
  40. package/src/lib/backup/offsite.ts +173 -0
  41. package/src/lib/backup/store.ts +28 -0
  42. package/src/lib/booking/availability.ts +15 -2
  43. package/src/lib/booking/service.ts +53 -1
  44. package/src/lib/cache/config.ts +7 -0
  45. package/src/lib/cache/memory.ts +39 -0
  46. package/src/lib/cache/policy.ts +89 -0
  47. package/src/lib/cache/purge.ts +89 -0
  48. package/src/lib/calendar/admin.ts +41 -0
  49. package/src/lib/designPreview.ts +2 -2
  50. package/src/lib/dynamicBlocks.ts +1 -1
  51. package/src/lib/editor/auth.ts +35 -5
  52. package/src/lib/editor/http.ts +4 -1
  53. package/src/lib/editor/me.ts +4 -0
  54. package/src/lib/editor/openapi.ts +49 -1
  55. package/src/lib/editor/roles.ts +20 -0
  56. package/src/lib/editor/schedule.ts +340 -0
  57. package/src/lib/editor/schemas.ts +36 -1
  58. package/src/lib/editor/site-data.ts +110 -0
  59. package/src/lib/editor/store.ts +13 -7
  60. package/src/lib/editor/token-route.ts +2 -1
  61. package/src/lib/email/accounts.ts +4 -0
  62. package/src/lib/email/service.ts +9 -1
  63. package/src/lib/email/store.ts +10 -0
  64. package/src/lib/email/templates.ts +8 -1
  65. package/src/lib/email/types.ts +1 -0
  66. package/src/lib/forms/store.ts +5 -0
  67. package/src/lib/mobile/access-admin.ts +34 -0
  68. package/src/lib/mobile/access.ts +28 -0
  69. package/src/lib/mobile/calendar.ts +26 -13
  70. package/src/lib/mobile/content.ts +21 -1
  71. package/src/lib/mobile/forms.ts +77 -0
  72. package/src/lib/mobile/guest-store.ts +62 -0
  73. package/src/lib/mobile/guest.ts +160 -0
  74. package/src/lib/mobile/http.ts +6 -5
  75. package/src/lib/mobile/links.ts +69 -0
  76. package/src/lib/mobile/manage.ts +114 -0
  77. package/src/lib/mobile/openapi.ts +40 -4
  78. package/src/lib/mobile/route.ts +63 -4
  79. package/src/lib/mobile/schemas.ts +52 -1
  80. package/src/lib/mobile/search.ts +95 -0
  81. package/src/lib/mobile/site.ts +8 -2
  82. package/src/lib/monitoring.ts +171 -0
  83. package/src/lib/net.ts +82 -14
  84. package/src/lib/repo/migrate-runner.ts +96 -0
  85. package/src/lib/repo/migrations.ts +104 -0
  86. package/src/lib/repo/sql.ts +122 -44
  87. package/src/lib/repo/sqlite.ts +14 -1
  88. package/src/lib/repo/types.ts +31 -3
  89. package/src/lib/scheduler.ts +14 -0
  90. package/src/lib/schema.ts +35 -1
  91. package/src/lib/theme-transfer.ts +163 -0
  92. package/src/lib/theme.ts +5 -0
  93. package/src/lib/webhooks.ts +7 -2
  94. package/src/middleware.ts +41 -11
  95. package/src/pages/[...slug].astro +3 -2
  96. package/src/pages/account/[action].astro +6 -0
  97. package/src/pages/admin/app-auth.astro +21 -7
  98. package/src/pages/admin/calendar/resources/link.astro +2 -0
  99. package/src/pages/admin/design.astro +2 -1
  100. package/src/pages/admin/import.astro +5 -3
  101. package/src/pages/admin/index.astro +3 -2
  102. package/src/pages/admin/integrations.astro +3 -2
  103. package/src/pages/admin/login.astro +236 -25
  104. package/src/pages/admin/logout.ts +16 -2
  105. package/src/pages/admin/mobile.astro +6 -2
  106. package/src/pages/admin/settings.astro +43 -5
  107. package/src/pages/admin/users.astro +350 -59
  108. package/src/pages/api/backup.ts +10 -26
  109. package/src/pages/api/design/export.ts +72 -0
  110. package/src/pages/api/design/import.ts +41 -0
  111. package/src/pages/api/email/settings.ts +9 -5
  112. package/src/pages/api/health.ts +17 -0
  113. package/src/pages/api/live/oauth/youtube/callback.ts +5 -4
  114. package/src/pages/api/live/settings.ts +2 -2
  115. package/src/pages/api/media/upload.ts +2 -2
  116. package/src/pages/api/mobile/access.ts +22 -0
  117. package/src/pages/api/monitoring.ts +22 -0
  118. package/src/pages/api/ops/status.ts +2 -1
  119. package/src/pages/api/push/admin.ts +3 -2
  120. package/src/pages/api/restore.ts +6 -3
  121. package/src/pages/api/v1/admin/bookings/[id]/[action].ts +7 -1
  122. package/src/pages/api/v1/admin/bookings/index.ts +10 -2
  123. package/src/pages/api/v1/admin/calendars/[id].ts +16 -0
  124. package/src/pages/api/v1/admin/calendars/index.ts +12 -0
  125. package/src/pages/api/v1/admin/forms/[id]/responses.ts +7 -0
  126. package/src/pages/api/v1/admin/forms/index.ts +6 -0
  127. package/src/pages/api/v1/admin/links.ts +13 -0
  128. package/src/pages/api/v1/admin/resources/[id]/blocks/[blockId].ts +10 -0
  129. package/src/pages/api/v1/admin/resources/[id]/blocks/index.ts +12 -0
  130. package/src/pages/api/v1/admin/resources/[id]/index.ts +17 -0
  131. package/src/pages/api/v1/admin/resources/[id]/links/[linkId].ts +16 -0
  132. package/src/pages/api/v1/admin/resources/[id]/links/index.ts +12 -0
  133. package/src/pages/api/v1/admin/resources/index.ts +12 -0
  134. package/src/pages/api/v1/admin/schedule.ts +7 -0
  135. package/src/pages/api/v1/bookings/manage/availability.ts +6 -0
  136. package/src/pages/api/v1/bookings/manage/cancel.ts +6 -0
  137. package/src/pages/api/v1/bookings/manage/ics.ts +6 -0
  138. package/src/pages/api/v1/bookings/manage/index.ts +6 -0
  139. package/src/pages/api/v1/bookings/manage/reschedule.ts +6 -0
  140. package/src/pages/api/v1/calendars.ts +1 -1
  141. package/src/pages/api/v1/forms/[id]/index.ts +7 -0
  142. package/src/pages/api/v1/forms/[id]/responses.ts +6 -0
  143. package/src/pages/api/v1/guest/bookings.ts +6 -0
  144. package/src/pages/api/v1/guest/code.ts +6 -0
  145. package/src/pages/api/v1/guest/revoke.ts +6 -0
  146. package/src/pages/api/v1/guest/token.ts +6 -0
  147. package/src/pages/api/v1/links.ts +7 -0
  148. package/src/pages/api/v1/openapi.json.ts +1 -1
  149. package/src/pages/api/v1/push/topics.ts +1 -1
  150. package/src/pages/api/v1/resources.ts +1 -1
  151. package/src/pages/api/v1/search.ts +7 -0
  152. package/src/pages/api/v1/theme.ts +1 -1
  153. package/src/pages/blog/[slug].astro +3 -2
  154. package/src/pages/book/[resource]/[link].astro +3 -3
  155. package/src/pages/book/manage/[token].astro +11 -7
  156. package/src/pages/events/[slug].astro +3 -2
  157. package/src/pages/preview/[type]/[id].astro +3 -3
  158. package/src/styles/admin.css +169 -16
  159. package/src/styles/email-settings.css +6 -0
  160. package/src/virtual.d.ts +1 -0
  161. package/src/worker.ts +7 -1
@@ -0,0 +1,7 @@
1
+ import { getForm } from '../../../../../lib/mobile/forms';
2
+ import { readRoute } from '../../../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
5
+ export const GET = readRoute(getForm, true);
6
+ export const HEAD = GET;
7
+ export const ALL = () => methodNotAllowed();
@@ -0,0 +1,6 @@
1
+ import { submitFormResponse } from '../../../../../lib/mobile/forms';
2
+ import { privateRoute } from '../../../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
5
+ export const POST = privateRoute(submitFormResponse, { write: true, status: 201 });
6
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { myBookings } from '../../../../lib/mobile/guest';
2
+ import { privateRoute } from '../../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
5
+ export const GET = privateRoute(myBookings);
6
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { requestCode } from '../../../../lib/mobile/guest';
2
+ import { privateRoute } from '../../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
5
+ export const POST = privateRoute(requestCode, { write: true, status: 202 });
6
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { revokeGuest } from '../../../../lib/mobile/guest';
2
+ import { privateRoute } from '../../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
5
+ export const POST = privateRoute(revokeGuest, { write: true });
6
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { exchangeCode } from '../../../../lib/mobile/guest';
2
+ import { privateRoute } from '../../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
5
+ export const POST = privateRoute(exchangeCode, { write: true });
6
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -0,0 +1,7 @@
1
+ import { getLinks } from '../../../lib/mobile/links';
2
+ import { readRoute } from '../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../lib/mobile/http';
5
+ export const GET = readRoute(getLinks, true, false, { timeless: true });
6
+ export const HEAD = GET;
7
+ export const ALL = () => methodNotAllowed();
@@ -2,6 +2,6 @@ import { getOpenApi } from '../../../lib/mobile/openapi';
2
2
  import { readRoute } from '../../../lib/mobile/route';
3
3
  import { methodNotAllowed } from '../../../lib/mobile/http';
4
4
  export { preflight as OPTIONS } from '../../../lib/mobile/http';
5
- export const GET = readRoute(getOpenApi, true, true);
5
+ export const GET = readRoute(getOpenApi, true, true, { timeless: true });
6
6
  export const HEAD = GET;
7
7
  export const ALL = () => methodNotAllowed();
@@ -2,6 +2,6 @@ import { listPushTopics } from '../../../../lib/push/api';
2
2
  import { readRoute } from '../../../../lib/mobile/route';
3
3
  import { methodNotAllowed } from '../../../../lib/mobile/http';
4
4
  export { preflight as OPTIONS } from '../../../../lib/mobile/http';
5
- export const GET = readRoute(listPushTopics, true);
5
+ export const GET = readRoute(listPushTopics, true, false, { timeless: true });
6
6
  export const HEAD = GET;
7
7
  export const ALL = () => methodNotAllowed();
@@ -2,6 +2,6 @@ import { listResources } from '../../../lib/mobile/calendar';
2
2
  import { readRoute } from '../../../lib/mobile/route';
3
3
  import { methodNotAllowed } from '../../../lib/mobile/http';
4
4
  export { preflight as OPTIONS } from '../../../lib/mobile/http';
5
- export const GET = readRoute(listResources, true);
5
+ export const GET = readRoute(listResources, true, false, { timeless: true });
6
6
  export const HEAD = GET;
7
7
  export const ALL = () => methodNotAllowed();
@@ -0,0 +1,7 @@
1
+ import { search } from '../../../lib/mobile/search';
2
+ import { readRoute } from '../../../lib/mobile/route';
3
+ import { methodNotAllowed } from '../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../lib/mobile/http';
5
+ export const GET = readRoute(search, true);
6
+ export const HEAD = GET;
7
+ export const ALL = () => methodNotAllowed();
@@ -2,6 +2,6 @@ import { getThemeTokens } from '../../../lib/mobile/site';
2
2
  import { readRoute } from '../../../lib/mobile/route';
3
3
  import { methodNotAllowed } from '../../../lib/mobile/http';
4
4
  export { preflight as OPTIONS } from '../../../lib/mobile/http';
5
- export const GET = readRoute(getThemeTokens);
5
+ export const GET = readRoute(getThemeTokens, true, false, { timeless: true });
6
6
  export const HEAD = GET;
7
7
  export const ALL = () => methodNotAllowed();
@@ -8,7 +8,7 @@ import { expandDynamicBlocks, extractToc } from '../../lib/dynamicBlocks';
8
8
  import TagLabel from '../../components/site/TagLabel.astro';
9
9
  import PostCard from '../../components/site/PostCard.astro';
10
10
  import { highlightCodeHtml } from '../../lib/highlightCode';
11
- import { validateSession, SESSION_COOKIE } from '../../lib/auth';
11
+ import { validateRequestSession } from '../../lib/auth/request';
12
12
  import { slugify, authorSlug, publicTags } from '../../lib/schema';
13
13
  import { resolveDesign } from '../../lib/theme';
14
14
  import { effectiveSettings } from '../../lib/designPreview';
@@ -27,7 +27,7 @@ const locale = requestLocale(Astro.url, Astro.locals);
27
27
  const rawPost = slug ? await repo.getPostBySlug(slug, col.key) : null;
28
28
  const post = rawPost ? localizeDoc(rawPost, locale) : null;
29
29
 
30
- const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
30
+ const isAdmin = (await validateRequestSession(Astro.request, Astro.cookies, Astro.locals));
31
31
  if (!post || (post.status !== 'published' && !isAdmin)) {
32
32
  return new Response(null, { status: 404, statusText: 'Not found' });
33
33
  }
@@ -111,6 +111,7 @@ const shareLi = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURI
111
111
  ---
112
112
 
113
113
  <Site
114
+ staffPreview={!!isAdmin && post.status !== 'published'}
114
115
  title={post.seoTitle || post.title}
115
116
  exactTitle={Boolean(post.seoTitle)}
116
117
  description={post.seoDesc || post.excerpt || undefined}
@@ -3,7 +3,7 @@ import Site from '../../../layouts/Site.astro';
3
3
  import BookingFlow from '../../../components/booking/BookingFlow.astro';
4
4
  import { getRepo } from '../../../lib/repo';
5
5
  import { bookingsEnabled, manageUrl, resourceUrl } from '../../../lib/calendar/config';
6
- import { availabilityContext, createResourceBooking, rescheduleBooking, BookingError } from '../../../lib/booking/service';
6
+ import { availabilityContext, createResourceBooking, guestMoveLink, guestReschedule, BookingError } from '../../../lib/booking/service';
7
7
  import { siteOrigin } from '../../../lib/seo';
8
8
  import { waitUntilFrom } from '../../../lib/webhooks';
9
9
 
@@ -36,7 +36,7 @@ if (Astro.request.method === 'POST') {
36
36
  if (token) {
37
37
  const b = await repo.getBookingByToken(token);
38
38
  if (!b || b.linkId !== link.id) throw new BookingError(404, 'That booking can’t be found.');
39
- await rescheduleBooking(repo, b, start, end, flow);
39
+ await guestReschedule(repo, b, start, end, flow);
40
40
  return Astro.redirect(`${manageUrl(b.manageToken)}?moved=1`);
41
41
  }
42
42
  const answers: Record<string, string> = {};
@@ -59,7 +59,7 @@ if (Astro.request.method === 'POST') {
59
59
  Astro.response.status = err instanceof BookingError ? err.status : 500;
60
60
  }
61
61
  }
62
- const ctx = await availabilityContext(repo, resource, link, settings, Date.now(), reschedule?.id);
62
+ const ctx = await availabilityContext(repo, resource, reschedule ? guestMoveLink(link) : link, settings, Date.now(), reschedule?.id);
63
63
  ---
64
64
 
65
65
  <Site title={`${link.name} · ${resource.name}`} description={link.description || `Book ${resource.name}.`} noindex={link.visibility === 'unlisted' || resource.visibility === 'unlisted'}>
@@ -2,7 +2,7 @@
2
2
  import Site from '../../../layouts/Site.astro';
3
3
  import { getRepo } from '../../../lib/repo';
4
4
  import { CALENDAR, bookingLinkUrl, bookingsEnabled, eventUrl, resourceUrl } from '../../../lib/calendar/config';
5
- import { setBookingStatus, BookingError } from '../../../lib/booking/service';
5
+ import { guestCancel, guestChanges, BookingError } from '../../../lib/booking/service';
6
6
  import { bookingIcs, whenText } from '../../../lib/booking/emails';
7
7
  import { resourceZone } from '../../../lib/booking/availability';
8
8
  import { eventTimeZone, locationLine } from '../../../lib/calendar/events';
@@ -38,7 +38,7 @@ if (Astro.request.method === 'POST') {
38
38
  const form = await Astro.request.formData();
39
39
  if (form.get('op') === 'cancel' && (booking.status === 'pending' || booking.status === 'confirmed')) {
40
40
  try {
41
- current = await setBookingStatus(repo, booking, 'cancelled', { origin, locals: Astro.locals, waitUntil: waitUntilFrom(Astro.locals) });
41
+ current = await guestCancel(repo, booking, { origin, locals: Astro.locals, waitUntil: waitUntilFrom(Astro.locals) });
42
42
  } catch (err) {
43
43
  error = err instanceof BookingError ? err.message : 'Couldn’t cancel — please try again.';
44
44
  }
@@ -48,6 +48,7 @@ const tz = event ? eventTimeZone(event, settings) : resource ? resourceZone(reso
48
48
  const when = whenText(current, tz, settings, link?.mode === 'day' || event?.allDay);
49
49
  const active = current.status === 'pending' || current.status === 'confirmed';
50
50
  const upcoming = current.end > Date.now();
51
+ const can = guestChanges(current, link);
51
52
  const isNew = Astro.url.searchParams.has('new');
52
53
  const moved = Astro.url.searchParams.has('moved');
53
54
  const heading = current.status === 'cancelled' ? 'Booking cancelled'
@@ -91,15 +92,18 @@ const googleUrl = `https://calendar.google.com/calendar/render?${new URLSearchPa
91
92
  </div>
92
93
  </details>
93
94
  )}
94
- {resource && link && current.kind === 'booking' && (
95
+ {resource && link && can.reschedule && (
95
96
  <a class="pc-manage-btn" href={`${bookingLinkUrl(resource.slug, link.slug)}?reschedule=${encodeURIComponent(current.manageToken)}`}>Reschedule</a>
96
97
  )}
97
- <form method="post" onsubmit="return confirm('Cancel this booking?')">
98
- <input type="hidden" name="op" value="cancel" />
99
- <button class="pc-manage-btn is-danger" type="submit">Cancel {current.kind === 'rsvp' ? 'RSVP' : 'booking'}</button>
100
- </form>
98
+ {can.cancel && (
99
+ <form method="post" onsubmit="return confirm('Cancel this booking?')">
100
+ <input type="hidden" name="op" value="cancel" />
101
+ <button class="pc-manage-btn is-danger" type="submit">Cancel {current.kind === 'rsvp' ? 'RSVP' : 'booking'}</button>
102
+ </form>
103
+ )}
101
104
  </div>
102
105
  )}
106
+ {can.closed && <p class="pc-manage-note">Changes close {can.cutoffHours} hours before the start. Please contact us to change or cancel.</p>}
103
107
  <p class="pc-bk-foot">
104
108
  {event ? <a href={eventUrl(event.slug)}>Back to {event.title}</a> : resource ? <a href={resourceUrl(resource.slug)}>Back to {resource.name}</a> : null}
105
109
  </p>
@@ -12,7 +12,7 @@ import EventPage from '../../components/EventPage.astro';
12
12
  import { getRepo } from '../../lib/repo';
13
13
  import { expandDynamicBlocks } from '../../lib/dynamicBlocks';
14
14
  import { highlightCodeHtml } from '../../lib/highlightCode';
15
- import { validateSession, SESSION_COOKIE } from '../../lib/auth';
15
+ import { validateRequestSession } from '../../lib/auth/request';
16
16
  import { effectiveSettings } from '../../lib/designPreview';
17
17
  import { resolveDesign } from '../../lib/theme';
18
18
  import { siteOrigin } from '../../lib/seo';
@@ -34,7 +34,7 @@ const rawEvent = await repo.getEventBySlug(Astro.params.slug ?? '');
34
34
  const event = rawEvent ? localizeDoc(rawEvent, locale) : null;
35
35
  const [rawSettings, allCalendars] = await Promise.all([repo.getSettings(), repo.listCalendars()]);
36
36
  const calMap = new Map(allCalendars.map((c) => [c.id, c]));
37
- const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
37
+ const isAdmin = (await validateRequestSession(Astro.request, Astro.cookies, Astro.locals));
38
38
  if (!event || (!eventIsPublic(event, calMap) && !isAdmin)) return new Response(null, { status: 404, statusText: 'Not found' });
39
39
 
40
40
  const baseSettings = await effectiveSettings(rawSettings, Astro.url, Astro.cookies, Astro.locals);
@@ -81,6 +81,7 @@ const props = { event, occurrence, settings, calendars, html, upcoming, origin,
81
81
  ---
82
82
 
83
83
  <Site
84
+ staffPreview={!!isAdmin && !eventIsPublic(event, calMap)}
84
85
  title={event.seoTitle || event.title}
85
86
  exactTitle={Boolean(event.seoTitle)}
86
87
  description={event.seoDesc || event.excerpt || undefined}
@@ -6,7 +6,7 @@ import { renderMarkdown } from '../../../lib/markdown';
6
6
  import { verifyPreviewToken } from '../../../lib/preview';
7
7
  import { expandDynamicBlocks } from '../../../lib/dynamicBlocks';
8
8
  import { highlightCodeHtml } from '../../../lib/highlightCode';
9
- import { validateSession, SESSION_COOKIE } from '../../../lib/auth';
9
+ import { validateRequestSession } from '../../../lib/auth/request';
10
10
  import { mergeEventDraft, mergePageDraft, mergePostDraft } from '../../../lib/drafts';
11
11
  import EventPage from '../../../components/EventPage.astro';
12
12
  import { featuredOccurrence } from '../../../lib/calendar/events';
@@ -19,7 +19,7 @@ import { siteOrigin } from '../../../lib/seo';
19
19
  const { type, id } = Astro.params;
20
20
  if (type !== 'post' && type !== 'page' && type !== 'event') return new Response(null, { status: 404 });
21
21
 
22
- const isAdmin = (await validateSession(Astro.cookies.get(SESSION_COOKIE)?.value, Astro.locals));
22
+ const isAdmin = (await validateRequestSession(Astro.request, Astro.cookies, Astro.locals));
23
23
  const token = Astro.url.searchParams.get('token') ?? undefined;
24
24
  if (!isAdmin && !verifyPreviewToken(type, id!, token)) {
25
25
  return new Response(null, { status: 404 });
@@ -53,7 +53,7 @@ const fmt = (ts: number | null) =>
53
53
  ts ? new Date(ts).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '';
54
54
  ---
55
55
 
56
- <Site title={post?.title ?? event?.title ?? page?.seoTitle ?? page?.title}>
56
+ <Site title={post?.title ?? event?.title ?? page?.seoTitle ?? page?.title} staffPreview={!!isAdmin}>
57
57
  {event && eventView?.occurrence && (
58
58
  <EventPage event={event} occurrence={eventView.occurrence} settings={eventView.settings} calendars={eventView.calendars} html={eventView.html} upcoming={[]} origin={eventView.origin} preview />
59
59
  )}
@@ -757,6 +757,25 @@ dialog.modal[open] { animation: modal-in 0.16s ease-out; }
757
757
  .btn.block { width: 100%; justify-content: center; min-height: 38px; }
758
758
  .login-note { margin-top: 14px; }
759
759
  .login-for-app { text-align: center; margin: -8px 0 14px; color: var(--muted); }
760
+ .login-card.is-clerk { max-width: 420px; }
761
+ .login-mount { min-height: 180px; }
762
+ .login-mount[hidden] { display: none; }
763
+ .login-loading { display: flex; align-items: center; justify-content: center; gap: 10px; min-height: 180px; margin: 0; color: var(--muted); font-size: 13px; }
764
+ .login-spinner { width: 16px; height: 16px; border-radius: 50%; border: 2px solid var(--line-strong); border-top-color: var(--accent); animation: login-spin 0.8s linear infinite; }
765
+ @keyframes login-spin { to { transform: rotate(360deg); } }
766
+ @media (prefers-reduced-motion: reduce) { .login-spinner { animation-duration: 2.4s; } }
767
+ .login-clerk [data-clerk-error] { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
768
+ .login-clerk [data-clerk-error][hidden] { display: none; }
769
+ .login-state { text-align: center; }
770
+ .login-state h1 { font-size: 19px; letter-spacing: -0.02em; margin: 10px 0 6px; }
771
+ .login-state p { color: var(--text-2); margin: 0 0 10px; overflow-wrap: anywhere; }
772
+ .login-state .hint { margin-bottom: 18px; line-height: 1.5; }
773
+ .login-state-icon { display: inline-grid; place-items: center; width: 40px; height: 40px; border-radius: var(--r-md); background: var(--sel); color: var(--text-2); }
774
+ .login-state-icon svg { width: 20px; height: 20px; }
775
+ .login-state-icon.is-warn { background: var(--amber-soft); color: var(--amber); }
776
+ .login-state-icon.is-error { background: var(--red-soft); color: var(--red); }
777
+ .login-actions { display: grid; gap: 8px; }
778
+ [data-clerk-profile][hidden] { display: none; }
760
779
 
761
780
  /* editor-app sign-in (consent) — same paper card as login, a little wider */
762
781
  .consent-wrap { align-items: start; padding-top: max(8vh, 20px); }
@@ -1238,19 +1257,72 @@ input.mono { font-family: ui-monospace, 'SF Mono', monospace; font-size: 12.5px;
1238
1257
  .grow-text { flex: 1; min-width: 220px; }
1239
1258
  .grow-text .hint { margin-left: 6px; }
1240
1259
  .hint.tight { margin: 2px 0 0; }
1241
- .user-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
1242
- .user-form-row { display: flex; gap: 8px; flex-wrap: wrap; width: 100%; align-items: center; }
1243
- .user-form .w-name { width: 150px; }
1244
- .user-form .w-role { width: 100px; }
1245
- .user-form .w-pass { width: 180px; }
1246
- .user-form .w-slug { width: 140px; }
1247
- .user-form .w-grow { flex: 1; min-width: 220px; }
1248
- .user-form .w-full { width: 100%; }
1249
- .user-email { min-width: 180px; }
1250
1260
  .td-actions { text-align: right; width: 80px; }
1251
- @media (max-width: 767px) {
1252
- .user-form .w-name, .user-form .w-role, .user-form .w-pass, .user-form .w-slug, .user-form .w-grow { width: 100%; min-width: 0; flex: 1 1 100%; }
1253
- }
1261
+ /* Users: sign-in strip, tabs, member table, profile drawer */
1262
+ .auth-strip { display: flex; align-items: center; gap: 12px; padding: 12px 16px; margin-bottom: 18px; }
1263
+ .auth-mark { flex: none; display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--r-md); background: var(--sel); color: var(--text-2); }
1264
+ .auth-mark svg { width: 17px; height: 17px; }
1265
+ .auth-main { flex: 1; min-width: 0; }
1266
+ .auth-main h2 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13.5px; margin: 0 0 2px; }
1267
+ .auth-main .hint { margin: 0; line-height: 1.5; }
1268
+ .auth-where { flex: none; margin: 0; font-size: 12px; }
1269
+ .users-tabs { margin-bottom: 12px; }
1270
+ .users-tabs .count { font-size: 11px; font-weight: 600; color: var(--muted); background: var(--panel-2); border-radius: var(--r-full); padding: 0 6px; min-width: 18px; text-align: center; }
1271
+ .users-tabs [aria-selected='true'] .count { color: var(--text-2); background: var(--sel); }
1272
+ .users-panel[hidden] { display: none; }
1273
+ .users-panel.devices-card { margin-top: 0; }
1274
+ .users-panel-hint { margin: 0; padding: 14px 18px 4px; }
1275
+ .users-table tr[data-user-row] { cursor: pointer; }
1276
+ .users-table td { padding-top: 10px; padding-bottom: 10px; }
1277
+ .user-cell { display: flex; align-items: center; gap: 11px; min-width: 0; }
1278
+ .avatar.user-av { width: 32px; height: 32px; font-size: 11px; }
1279
+ .user-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.35; }
1280
+ .user-who .t-title, .user-who .t-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1281
+ .user-you { margin-left: 7px; font-size: 10.5px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--faint); }
1282
+ dialog.modal.drawer {
1283
+ margin: 0 0 0 auto; width: min(440px, 100vw); height: 100dvh; max-height: 100dvh; max-width: 100vw;
1284
+ border-radius: var(--r-lg) 0 0 var(--r-lg); border-left: 1px solid var(--line-strong);
1285
+ flex-direction: column; overflow: hidden;
1286
+ }
1287
+ dialog.modal.drawer[open] { display: flex; animation: drawer-in 0.2s ease-out; }
1288
+ @keyframes drawer-in { from { opacity: 0.4; transform: translateX(28px); } }
1289
+ .drawer-head { display: flex; align-items: center; gap: 12px; padding: 18px 20px 16px; border-bottom: 1px solid var(--line); }
1290
+ .avatar.drawer-av { width: 46px; height: 46px; font-size: 15px; }
1291
+ .drawer-title { flex: 1; min-width: 0; }
1292
+ .drawer-title h2 { font-size: 16px; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1293
+ .drawer-title a { font-size: 12px; color: var(--muted); text-decoration: none; }
1294
+ .drawer-title a:hover { color: var(--accent-deep); }
1295
+ .drawer-body { flex: 1; overflow-y: auto; padding: 4px 20px 8px; }
1296
+ .drawer-body > .flash { margin: 14px 0 0; }
1297
+ .drawer-sec { padding: 16px 0 6px; border-bottom: 1px solid var(--line); }
1298
+ .drawer-sec:last-child { border-bottom: 0; }
1299
+ .drawer-sec h3 { margin: 0 0 10px; font-size: 11px; font-weight: 600; letter-spacing: 0.07em; text-transform: uppercase; color: var(--faint); }
1300
+ .drawer-kv { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; font-size: 13px; }
1301
+ .drawer-kv .k { color: var(--muted); flex: none; }
1302
+ .drawer-kv .v { display: flex; align-items: center; gap: 8px; min-width: 0; }
1303
+ .drawer-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1304
+ .drawer-link { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
1305
+ .drawer-link input { flex: 1; min-width: 0; font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; font-size: 12px; }
1306
+ .drawer-link .hint { flex-basis: 100%; margin: 0; }
1307
+ label.field > .slug-wrap { display: flex; margin: 0; font-size: inherit; font-weight: inherit; color: var(--text); }
1308
+ .drawer-sec .slug-wrap input { flex: 1; min-width: 0; }
1309
+ .drawer-foot { display: flex; align-items: center; gap: 8px; padding: 12px 20px; border-top: 1px solid var(--line); background: var(--panel-2); }
1310
+ .drawer-foot .spacer { flex: 1; }
1311
+ input.is-readonly { background: transparent; border-style: dashed; color: var(--text-2); }
1312
+ .callout { margin: 0 0 12px; padding: 10px 12px; border-radius: var(--r-md); background: var(--blue-soft); color: var(--blue); font-size: 12.5px; line-height: 1.5; }
1313
+ .callout.warn { background: var(--amber-soft); color: var(--amber); }
1314
+ @media (max-width: 640px) {
1315
+ dialog.modal.drawer { width: 100vw; border-radius: 0; border-left: 0; }
1316
+ .auth-strip { flex-wrap: wrap; }
1317
+ .auth-where { flex-basis: 100%; margin-left: 44px; }
1318
+ .users-table th:nth-child(3), .users-table td:nth-child(3) { display: none; }
1319
+ }
1320
+ /* Users: flash messages + Clerk re-verification */
1321
+ .users-flash { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
1322
+ .users-flash-actions { display: inline-flex; gap: 6px; flex-wrap: wrap; }
1323
+ .reverify-text { margin: 0 0 8px; font-size: 14px; line-height: 1.5; }
1324
+ .reverify-dialog .hint { margin: 0; }
1325
+ [data-acms-reverify][hidden] { display: none; }
1254
1326
  .sheet:focus, .sheet:focus-visible { outline: none; }
1255
1327
 
1256
1328
  /* ---- calendar module (events list, calendars) --------------------------- */
@@ -1511,10 +1583,6 @@ a.bk-row:hover, .bk-row.is-sel { background: var(--hover); }
1511
1583
  .dm-status-actions { margin-left: 0; width: 100%; }
1512
1584
  }
1513
1585
 
1514
- /* role: marker + text (no badges) */
1515
- .role-mark { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; font-weight: 600; color: var(--text-2); }
1516
- .role-mark i { width: 7px; height: 7px; border-radius: 2px; background: var(--blue); }
1517
- .role-mark i.is-owner { background: var(--accent); }
1518
1586
 
1519
1587
  /* Users → Signed-in apps */
1520
1588
  .devices-card { margin-top: 20px; }
@@ -1590,3 +1658,88 @@ a.bk-row:hover, .bk-row.is-sel { background: var(--hover); }
1590
1658
  /* Bookings → Day view: a room held by an event (tinted, dashed; links to the event) */
1591
1659
  .bk-blk.is-hold { background: color-mix(in srgb, var(--accent) 10%, var(--panel)); border: 1px dashed var(--accent-ring); z-index: 0; }
1592
1660
  .bk-blk:not(.is-hold) { z-index: 1; }
1661
+ /* Admin → Mobile app → App access: plain rows, square switches (no pills) */
1662
+ .acc { display: grid; gap: 16px; margin-bottom: 24px; }
1663
+ .acc .card { margin: 0; }
1664
+ .acc-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-top: 1px solid var(--line); }
1665
+ .acc .card-head + .acc-row { border-top: 0; }
1666
+ .acc-row > div:first-child { min-width: 0; }
1667
+ .acc-row b { display: block; font-size: 14px; }
1668
+ .acc-row small { display: block; color: var(--muted); font-size: 12.5px; margin-top: 2px; }
1669
+ .acc-row small.acc-warn { color: var(--amber); }
1670
+ .acc-actions { display: flex; align-items: center; gap: 10px; flex: none; }
1671
+ .acc-switch { width: 38px; height: 22px; border-radius: 7px; border: 0; padding: 0; background: var(--line-strong); position: relative; flex: none; cursor: pointer; }
1672
+ .acc-switch i { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 5px; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / 25%); transition: left 0.15s; }
1673
+ .acc-switch.on { background: var(--accent); }
1674
+ .acc-switch.on i { left: 19px; }
1675
+ .acc-switch:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; }
1676
+ .acc-switch:disabled { opacity: 0.5; cursor: not-allowed; }
1677
+ .acc-row input.acc-num { width: 96px; flex: none; text-align: right; }
1678
+ @media (max-width: 640px) {
1679
+ /* switches and numbers stay beside their label; the role picker takes its own line */
1680
+ .acc-row { flex-wrap: wrap; }
1681
+ .acc-row > div:first-child { flex: 1 1 180px; }
1682
+ .acc-row .seg { width: 100%; }
1683
+ .acc-row .seg > * { flex: 1; }
1684
+ }
1685
+
1686
+ /* Settings → Monitoring (Uptime Kuma) */
1687
+ .mon { display: grid; gap: 16px; }
1688
+ .mon .flash { margin: 0; }
1689
+ .mon-dot { display: inline-block; width: 9px; height: 9px; border-radius: 3px; margin-right: 8px; vertical-align: 1px; background: var(--faint); }
1690
+ .mon-dot.up { background: var(--green); }
1691
+ .mon-dot.down { background: var(--red); }
1692
+ .mon-dot.pending, .mon-dot.maintenance { background: var(--amber); }
1693
+ .mon-self { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 18px; font-size: 13px; color: var(--text-2); padding: 12px 14px; background: var(--panel-2); border-radius: var(--r-md); }
1694
+ .mon-self b { color: var(--text); }
1695
+ .mon-self a { margin-left: auto; color: var(--muted); font-size: 12.5px; }
1696
+ .mon-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
1697
+ .mon-tile { background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md); padding: 12px 14px; min-width: 0; }
1698
+ .mon-tile.is-down { background: var(--red-soft); border-color: rgb(192 59 59 / 25%); }
1699
+ .mon-name { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1700
+ .mon-sub { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
1701
+ .mon-sub .nw { white-space: nowrap; }
1702
+ .mon-msg { color: var(--red); font-size: 12.5px; margin-top: 4px; overflow-wrap: anywhere; }
1703
+ .mon-beats { display: flex; gap: 2px; margin-top: 10px; height: 14px; }
1704
+ .mon-beats i { flex: 1; max-width: 8px; border-radius: 2px; background: var(--green); opacity: 0.75; }
1705
+ .mon-beats i.down { background: var(--red); opacity: 1; }
1706
+ .mon-beats i.pending, .mon-beats i.maintenance { background: var(--amber); }
1707
+ .mon-empty { border: 1px dashed var(--line-strong); border-radius: var(--r-md); padding: 14px 16px; }
1708
+ .mon-empty p { margin: 4px 0 0; color: var(--text-2); font-size: 13px; }
1709
+ .mon-cmd { display: block; margin: 8px 0; padding: 8px 10px; overflow-x: auto; white-space: nowrap; font-size: 12.5px; }
1710
+ .mon-fields { display: grid; gap: 14px; }
1711
+ .mon-field { display: grid; gap: 6px; font-size: 13px; }
1712
+ .mon-field > span { font-weight: 600; color: var(--text); }
1713
+ .mon-field > span small { font-weight: 400; color: var(--muted); margin-left: 6px; }
1714
+ .mon-field > small { color: var(--muted); }
1715
+ .mon-field input { width: 100%; min-width: 0; }
1716
+ .mon-inline { display: flex; gap: 8px; }
1717
+ .mon-inline input { flex: 1; }
1718
+ .mon-inline .btn { flex: none; }
1719
+ .mon-actions { display: flex; justify-content: flex-end; gap: 8px; }
1720
+ @media (max-width: 640px) {
1721
+ .mon-self a { margin-left: 0; }
1722
+ .mon-field > span small { display: block; margin: 2px 0 0; }
1723
+ .mon-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
1724
+ .mon-tile { padding: 10px 12px; }
1725
+ .mon-sub { font-size: 12px; }
1726
+ }
1727
+
1728
+ /* Design → Export / Import theme (ThemeTransfer) */
1729
+ .tt-muted { color: var(--muted); }
1730
+ .tt-small { font-size: 12px; margin: 10px 0 0; }
1731
+ .check-row .tt-muted { font-size: 12px; margin-left: 2px; }
1732
+ .tt-note { font-size: 12.5px; color: var(--text-2); background: var(--panel-2); border-radius: var(--r-md); padding: 9px 11px; margin: 10px 0 0; }
1733
+ .tt-note.warn { background: var(--amber-soft); color: var(--amber); }
1734
+ .tt-drop { display: flex; align-items: center; justify-content: center; border: 1.5px dashed var(--line-strong); border-radius: var(--r-md); padding: 26px 12px; cursor: pointer; color: var(--text-2); }
1735
+ .tt-drop:hover { border-color: var(--accent); color: var(--text); }
1736
+ .tt-drop input { position: absolute; width: 1px; height: 1px; opacity: 0; }
1737
+ .tt-drop:focus-within { box-shadow: 0 0 0 3px var(--accent-ring); }
1738
+ .tt-pkg { border: 1px solid var(--line); border-radius: var(--r-md); background: var(--panel); padding: 10px 12px; margin-bottom: 12px; }
1739
+ .tt-pkg small { display: block; color: var(--muted); font-size: 12px; }
1740
+ .tt-list { list-style: none; margin: 0; padding: 0; }
1741
+ .tt-list li { padding: 9px 0; border-top: 1px solid var(--line); font-size: 13px; }
1742
+ .tt-list li:first-child { border-top: 0; }
1743
+ .tt-list small { display: block; color: var(--muted); font-size: 12px; }
1744
+ .tt-list .check-row { margin: 0; }
1745
+ .tt-list .imp-mark { font-size: 11px; font-weight: 600; color: var(--green); background: var(--green-soft); border-radius: 999px; padding: 1px 7px; margin-left: 4px; }
@@ -63,6 +63,12 @@
63
63
  .ems-note { background: var(--amber-soft); color: var(--text-2); border-radius: var(--r-md); padding: 9px 13px; margin: 0 0 12px; font-size: 13px; }
64
64
  .ems-pw { max-width: 340px; margin-top: 4px; }
65
65
  .ems-pw .hint { display: block; margin-top: 4px; }
66
+ .ems-verify { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 4px; padding: 10px 12px; background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-md); }
67
+ .ems-verify p { flex: 1; min-width: 200px; margin: 0; font-size: 12.5px; color: var(--muted); line-height: 1.45; }
68
+ .ems-verify p b { color: var(--text-2); font-weight: 600; }
69
+ .ems-verify-mark { flex: none; display: grid; place-items: center; width: 28px; height: 28px; border-radius: var(--r-sm); background: var(--sel); color: var(--text-2); }
70
+ .ems-verify-mark svg { width: 16px; height: 16px; }
71
+ .ems-verify-actions { display: inline-flex; gap: 6px; }
66
72
  .ems-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
67
73
  .ems-row { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 12px; }
68
74
  .ems-row .field { margin-bottom: 0; }
package/src/virtual.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  declare module 'virtual:acms-config' {
4
4
  export const userConfig: {
5
5
  coreVersion: string;
6
+ auth?: { provider: 'builtin' | 'clerk' };
6
7
  brand: { name?: string; version?: string; stack?: string };
7
8
  seed: { siteTitle?: string; tagline?: string; heroHeading?: string; heroSub?: string };
8
9
  // resolved (defaults applied + validated) by integration.mjs; `posts`
package/src/worker.ts CHANGED
@@ -3,6 +3,7 @@ import { App } from 'astro/app';
3
3
  import { handle } from '@astrojs/cloudflare/handler';
4
4
  import { D1Repository, type D1Database } from './lib/repo/d1';
5
5
  import { runScheduledTasks } from './lib/scheduler';
6
+ import { EXECUTION_CONTEXT } from './lib/cache/purge';
6
7
 
7
8
  // Custom Workers entrypoint (wired via the adapter's workerEntryPoint
8
9
  // option): identical fetch behavior to the stock entry, plus a cron-driven
@@ -19,11 +20,16 @@ export function createExports(manifest: SSRManifest) {
19
20
  return {
20
21
  default: {
21
22
  async fetch(request: HandleRequest, env: HandleEnv, context: HandleContext) {
23
+ // the adapter's locals.runtime.ctx has no `cache`; the middleware
24
+ // reads Workers Cache from here (lib/cache/purge.ts)
25
+ Reflect.set(request, EXECUTION_CONTEXT, context);
22
26
  return handle(manifest, app, request, env, context);
23
27
  },
24
28
  async scheduled(_controller: unknown, env: HandleEnv, context: HandleContext) {
25
29
  const repo = new D1Repository(env.DB as D1Database);
26
- context.waitUntil(runScheduledTasks(repo, { runtime: { env } }).then(report => {
30
+ // ctx carries Workers Cache (ctx.cache) for the purge job; `scheduled`
31
+ // tells it a missing ctx.cache here doesn't mean caching is off
32
+ context.waitUntil(runScheduledTasks(repo, { runtime: { env, ctx: context, scheduled: true } }).then(report => {
27
33
  if (!report.ok) throw new Error('Scheduled maintenance failed; see authenticated operations status.');
28
34
  }));
29
35
  },