@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,72 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { userConfig } from 'virtual:acms-config';
3
+ import { getRepo } from '../../../lib/repo';
4
+ import { getMediaStore } from '../../../lib/media/store';
5
+ import { apiError, ApiError } from '../../../lib/api';
6
+ import { designIdentity } from '../../../lib/design-service';
7
+ import { draftNavItems, mergeDesignDraft } from '../../../lib/drafts';
8
+ import { sectionTargets } from '../../../lib/section-targets';
9
+ import { getTheme, isProjectTheme } from '../../../lib/theme';
10
+ import { buildDesignExport, mediaKeysIn, themeFileName, THEME_TRANSFER_FORMAT, type ThemeTransferManifest } from '../../../lib/theme-transfer';
11
+ import { zipStream, type ZipEntry } from '../../../lib/backup/zip';
12
+ import type { Media } from '../../../lib/schema';
13
+
14
+ // Theme export (acms-theme v1): the design as the Design screen shows it
15
+ // (draft over live) plus the images it uses, as one ZIP for Design → Import
16
+ // theme on another site or machine. ?media=0 skips images, ?nav=1 adds menus.
17
+ export const GET: APIRoute = async ({ request, cookies, locals }) => {
18
+ try {
19
+ const repo = await getRepo(locals);
20
+ const url = new URL(request.url);
21
+ const includeMedia = url.searchParams.get('media') !== '0', includeNav = url.searchParams.get('nav') === '1';
22
+ const who = await designIdentity(request, cookies, repo, 'design:read');
23
+ if ('role' in who && who.role === 'editor') throw new ApiError(403, 'Only an owner can export the theme.');
24
+ if (includeMedia && !who.scopes.includes('media:read')) throw new ApiError(403, 'Required scope: media:read (or export with media=0).');
25
+ const live = await repo.getSettings();
26
+ const settings = mergeDesignDraft(live);
27
+ const [pages, liveNav, targets] = await Promise.all([repo.listPages(), repo.listNavItems(), sectionTargets(repo, locals)]);
28
+ const design = buildDesignExport({
29
+ settings, navItems: draftNavItems(live.draftDesign) ?? liveNav, pages, links: targets.links, origin: url.origin, includeNav,
30
+ });
31
+ const now = Date.now();
32
+ const theme = getTheme(settings.activeTheme);
33
+ const media = includeMedia
34
+ ? (await Promise.all([...new Set(mediaKeysIn(design).map((k) => k.split('/')[0]))].map((id) => repo.getMedia(id)))).filter((m) => m !== null)
35
+ : [];
36
+ const manifest: ThemeTransferManifest = {
37
+ format: THEME_TRANSFER_FORMAT, version: 1, exportedAt: now,
38
+ coreVersion: String((userConfig as { coreVersion?: string }).coreVersion ?? ''),
39
+ source: { title: live.siteTitle, siteUrl: url.origin },
40
+ theme: { slug: theme.slug, name: theme.name, version: theme.version, project: isProjectTheme(theme.slug) },
41
+ includes: ['design', ...(media.length ? ['media' as const] : []), ...(includeNav ? ['nav' as const] : [])],
42
+ };
43
+ const store = getMediaStore(locals);
44
+ async function* entries(): AsyncGenerator<ZipEntry> {
45
+ const enc = new TextEncoder();
46
+ yield { name: 'manifest.json', data: enc.encode(JSON.stringify(manifest, null, 1)) };
47
+ yield { name: 'design.json', data: enc.encode(JSON.stringify(design, null, 1)) };
48
+ // a missing variant is skipped; the row still imports with what's left
49
+ const rows: Media[] = [];
50
+ for (const m of media) {
51
+ const variants: Media['variants'] = [];
52
+ let original = false;
53
+ for (const key of [m.key, ...m.variants.map((v) => v.key)]) {
54
+ const obj = await store.get(key);
55
+ if (!obj) continue;
56
+ if (key === m.key) original = true; else variants.push(m.variants.find((v) => v.key === key)!);
57
+ yield { name: `media/${key}`, data: new Uint8Array(await new Response(obj.body).arrayBuffer()) };
58
+ }
59
+ if (original) rows.push({ ...m, variants });
60
+ }
61
+ yield { name: 'media.json', data: enc.encode(JSON.stringify(rows, null, 1)) };
62
+ }
63
+ return new Response(zipStream(entries(), now), {
64
+ headers: {
65
+ 'Content-Type': 'application/zip',
66
+ 'Cache-Control': 'no-store',
67
+ 'X-Content-Type-Options': 'nosniff',
68
+ 'Content-Disposition': `attachment; filename="${themeFileName(theme.slug, now)}"`,
69
+ },
70
+ });
71
+ } catch (e) { return apiError(e); }
72
+ };
@@ -0,0 +1,41 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { z } from 'zod';
3
+ import { getRepo } from '../../../lib/repo';
4
+ import { apiError, ApiError, json, readJson } from '../../../lib/api';
5
+ import { DesignPatch, designIdentity, runDesignOperation } from '../../../lib/design-service';
6
+ import { diffDesign } from '../../../lib/designDiff';
7
+ import { sectionTargets } from '../../../lib/section-targets';
8
+ import { getTheme, listThemes } from '../../../lib/theme';
9
+ import { planDesignImport, ThemeDesignFile } from '../../../lib/theme-transfer';
10
+
11
+ // Theme import: the design.json of an acms-theme ZIP becomes a staged draft
12
+ // (never live). dryRun returns what would change; the browser uploads the
13
+ // ZIP's images through /api/import/media before the real run.
14
+ const Body = z.object({
15
+ revision: z.number().int().nonnegative(),
16
+ design: ThemeDesignFile,
17
+ nav: z.boolean().default(false),
18
+ dryRun: z.boolean().default(false),
19
+ }).strict();
20
+
21
+ export const POST: APIRoute = async ({ request, cookies, locals }) => {
22
+ try {
23
+ const repo = await getRepo(locals);
24
+ const input = Body.parse(await readJson(request, 2_000_000));
25
+ const who = await designIdentity(request, cookies, repo, 'design:write-draft');
26
+ if ('role' in who && who.role === 'editor') throw new ApiError(403, 'Only an owner can import a theme.');
27
+ const current = await repo.getSettings();
28
+ if (current.designRevision !== input.revision) throw new ApiError(409, 'Design changed. Reload and try again.');
29
+ const [pages, targets, liveNav] = await Promise.all([repo.listPages(), sectionTargets(repo, locals), repo.listNavItems()]);
30
+ const { patch, report } = planDesignImport({
31
+ file: input.design, pages, links: targets.links, installedThemes: listThemes().map((t) => t.slug),
32
+ stagedPages: (current.draftDesign?.sectionPages ?? current.sectionPages) as typeof current.sectionPages, includeNav: input.nav,
33
+ });
34
+ const parsed = DesignPatch.parse(patch);
35
+ const draft = { ...current.draftDesign, ...parsed };
36
+ const changes = diffDesign(current, liveNav, draft, (slug) => getTheme(slug).name);
37
+ if (input.dryRun) return json({ report, changes });
38
+ const result = await runDesignOperation(repo, who, { operation: 'stage', revision: input.revision, patch: parsed }, new URL(request.url).origin);
39
+ return json({ report, changes, revision: result.revision });
40
+ } catch (e) { return apiError(e); }
41
+ };
@@ -2,8 +2,10 @@ import type { APIRoute } from 'astro';
2
2
  import { z } from 'zod';
3
3
  import { getRepo } from '../../../lib/repo';
4
4
  import { ApiError, apiError, json, readJson, requireSession } from '../../../lib/api';
5
- import { validateSession, SESSION_COOKIE } from '../../../lib/auth';
5
+ import { validateRequestSession } from '../../../lib/auth/request';
6
6
  import { reauthenticate } from '../../../lib/email/accounts';
7
+ import { AUTH } from '../../../lib/auth/config';
8
+ import { getRequestAuth } from '../../../lib/auth/request';
7
9
  import { decrypt, encrypt, encryptionAvailable, resolveConfig, runtimeValue } from '../../../lib/email/config';
8
10
  import { dispatchEmail } from '../../../lib/email/service';
9
11
  import { emailReadiness } from '../../../lib/email/readiness';
@@ -12,13 +14,13 @@ import type { Credentials, PublicEmailSettings } from '../../../lib/email/types'
12
14
  const Config = z.object({provider:z.enum(['auto','none','resend','mailgun','ses','cloudflare']),source:z.enum(['env','encrypted']),from:z.string().trim().max(320),replyTo:z.string().trim().max(320),domain:z.string().trim().max(253),region:z.string().trim().max(40),configurationSet:z.string().trim().max(200),snsTopicArn:z.string().trim().max(300),enabled:z.boolean(),reminders:z.boolean(),staffNotifications:z.boolean()}).strict();
13
15
  const Secrets = z.object({apiKey:z.string().max(4096).optional(),webhookSecret:z.string().max(4096).optional(),accessKeyId:z.string().max(256).optional(),secretAccessKey:z.string().max(4096).optional(),sessionToken:z.string().max(8192).optional()}).strict();
14
16
  const Input = z.discriminatedUnion('action',[
15
- z.object({action:z.literal('save'),config:Config,credentials:Secrets.default({}),reauthPassword:z.string().max(1024)}).strict(),
17
+ z.object({action:z.literal('save'),config:Config,credentials:Secrets.default({}),reauthPassword:z.string().max(1024).default('')}).strict(),
16
18
  z.object({action:z.literal('test'),to:z.string().trim().email().max(254)}).strict(),
17
19
  z.object({action:z.literal('retry'),id:z.string().max(100)}).strict(),
18
20
  ]);
19
21
  export async function publicSettings(repo:ContentRepository,locals?:unknown):Promise<PublicEmailSettings> {
20
22
  const record=await repo.email.config(),c=resolveConfig(record,locals);
21
- return {version:c.version,config:c.config,provider:c.provider,...emailReadiness(c,(await repo.getSettings()).siteUrl,locals),environmentLocked:!!runtimeValue('EMAIL_PROVIDER',locals),credentialFields:Object.entries(c.credentials).filter(([,v])=>!!v).map(([k])=>k),activity:(await repo.email.list()).map(({payload,lease,fingerprint,rendered,...safe})=>safe),heartbeat:await repo.email.heartbeat()};
23
+ return {authProvider:AUTH.provider,version:c.version,config:c.config,provider:c.provider,...emailReadiness(c,(await repo.getSettings()).siteUrl,locals),environmentLocked:!!runtimeValue('EMAIL_PROVIDER',locals),credentialFields:Object.entries(c.credentials).filter(([,v])=>!!v).map(([k])=>k),activity:(await repo.email.list()).map(({payload,lease,fingerprint,rendered,...safe})=>safe),heartbeat:await repo.email.heartbeat()};
22
24
  }
23
25
  export const GET:APIRoute=async({request,cookies,locals})=>{
24
26
  try{const repo=await getRepo(locals);await requireSession(request,cookies,repo,true);return json(await publicSettings(repo,locals));}catch(e){return apiError(e);}
@@ -26,11 +28,13 @@ export const GET:APIRoute=async({request,cookies,locals})=>{
26
28
  export const POST:APIRoute=async({request,cookies,locals})=>{
27
29
  try{
28
30
  const repo=await getRepo(locals);await requireSession(request,cookies,repo,true);
29
- const userId=(await validateSession(cookies.get(SESSION_COOKIE)?.value,repo))!;
31
+ const userId=(await validateRequestSession(request, cookies, repo))!;
30
32
  const input=Input.parse(await readJson(request,20000));
31
33
  if(input.action==='save'){
32
34
  if(!await repo.email.rateLimit(`reauth:${userId}`,10,10*60000))throw new ApiError(429,'Too many attempts. Try again later.');
33
- if(!await reauthenticate(repo,userId,input.reauthPassword))throw new ApiError(403,'Your current password is required to change email settings.');
35
+ if (AUTH.provider === 'clerk') {
36
+ if (!(await getRequestAuth(request,cookies,locals)).reverified) throw new ApiError(403,'Sign out and sign in again with Clerk before changing email settings. Recent verification is required.');
37
+ } else if(!await reauthenticate(repo,userId,input.reauthPassword))throw new ApiError(403,'Your current password is required to change email settings.');
34
38
  const current=await repo.email.config();
35
39
  if(runtimeValue('EMAIL_PROVIDER',locals)&&(input.config.provider!==runtimeValue('EMAIL_PROVIDER',locals)||input.config.source!=='env'))throw new ApiError(400,'Provider is managed by EMAIL_PROVIDER in the deployment environment.');
36
40
  let encrypted='';
@@ -0,0 +1,17 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getRepo } from '../../lib/repo';
3
+ import { healthReport } from '../../lib/monitoring';
4
+
5
+ // Public health check for uptime monitors (Uptime Kuma): 200 when the site
6
+ // works, 503 when the database or the scheduler is down. Names only which part
7
+ // failed; no settings, versions or secrets. Cached 10 s.
8
+ const headers = { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' };
9
+ export const GET: APIRoute = async ({ locals }) => {
10
+ try {
11
+ const report = await healthReport(await getRepo(locals), locals);
12
+ return Response.json(report, { status: report.status === 'down' ? 503 : 200, headers });
13
+ } catch {
14
+ return Response.json({ status: 'down', checks: { database: 'down', scheduler: 'stalled', email: 'off' }, checkedAt: new Date().toISOString() }, { status: 503, headers });
15
+ }
16
+ };
17
+ export const HEAD: APIRoute = async (c) => { const r = await GET(c); return new Response(null, { status: r.status, headers }); };
@@ -1,14 +1,15 @@
1
1
  import type { APIRoute } from 'astro';
2
2
  import { getRepo } from '../../../../../lib/repo';
3
3
  import { ApiError } from '../../../../../lib/api';
4
- import { validateSession, SESSION_COOKIE, OWNER_ENV } from '../../../../../lib/auth';
4
+ import { OWNER_ENV } from '../../../../../lib/auth';
5
+ import { requestSessionBinding, validateRequestSession } from '../../../../../lib/auth/request';
5
6
  import { finishOAuth } from '../../../../../lib/live/service';
6
- export const GET:APIRoute=async({cookies,locals,url})=>{
7
+ export const GET:APIRoute=async({request,cookies,locals,url})=>{
7
8
  const headers={'Cache-Control':'no-store','Referrer-Policy':'no-referrer'};
8
9
  try{
9
- const repo=await getRepo(locals),session=cookies.get(SESSION_COOKIE)?.value;
10
+ const repo=await getRepo(locals),session=await requestSessionBinding(request,cookies,locals);
10
11
  // OAuth is intentionally a cross-site GET. Session-bound, one-use state provides CSRF protection.
11
- const user=await validateSession(session,repo);
12
+ const user=await validateRequestSession(request,cookies,repo);
12
13
  if(!session||!user||user!==OWNER_ENV&&(await repo.getUser(user))?.role!=='owner')throw new ApiError(403,'Sign in as the site owner and start authorization again.');
13
14
  await finishOAuth(repo,session,url,locals);
14
15
  return new Response(null,{status:303,headers:{...headers,Location:'/admin/live?live=connected'}});
@@ -2,7 +2,7 @@ import type { APIRoute } from 'astro';
2
2
  import { z } from 'zod';
3
3
  import { getRepo } from '../../../lib/repo';
4
4
  import { ApiError, apiError, json, readJson, requireSession } from '../../../lib/api';
5
- import { SESSION_COOKIE } from '../../../lib/auth';
5
+ import { requestSessionBinding } from '../../../lib/auth/request';
6
6
  import { Provider } from '../../../lib/live/types';
7
7
  import { adminState, beginOAuth, disconnect, saveConnection, syncConnection, validateConfig } from '../../../lib/live/service';
8
8
  const Input=z.discriminatedUnion('action',[
@@ -26,7 +26,7 @@ export const POST:APIRoute=async({request,cookies,locals,url})=>{
26
26
  switch(input.action){
27
27
  case 'saveConnection':await saveConnection(repo,input,locals);break;
28
28
  case 'test':await syncConnection(repo,input.provider,locals,true);break;
29
- case 'authorize':return json({authorizationUrl:await beginOAuth(repo,cookies.get(SESSION_COOKIE)!.value,url,locals)});
29
+ case 'authorize':return json({authorizationUrl:await beginOAuth(repo,await requestSessionBinding(request,cookies,locals),url,locals)});
30
30
  case 'disconnect':await disconnect(repo,input.provider,locals);break;
31
31
  case 'saveConfig':await repo.updateSettings({live:validateConfig(input.config)});break;
32
32
  case 'selectChannel':{
@@ -1,5 +1,5 @@
1
1
  import type { APIRoute } from 'astro';
2
- import { validateSession, SESSION_COOKIE } from '../../../lib/auth';
2
+ import { validateRequestSession } from '../../../lib/auth/request';
3
3
  import { getRepo } from '../../../lib/repo';
4
4
  import { ApiError } from '../../../lib/api';
5
5
  import { storeUpload } from '../../../lib/media/upload';
@@ -7,7 +7,7 @@ import { storeUpload } from '../../../lib/media/upload';
7
7
  // Multipart upload from the admin; the rules live in lib/media/upload.ts
8
8
  // (shared with editor apps).
9
9
  export const POST: APIRoute = async ({ request, cookies, locals }) => {
10
- if (!(await validateSession(cookies.get(SESSION_COOKIE)?.value, locals))) {
10
+ if (!(await validateRequestSession(request, cookies, locals))) {
11
11
  return Response.json({ error: 'Unauthorized' }, { status: 401 });
12
12
  }
13
13
  const form = await request.formData();
@@ -0,0 +1,22 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getRepo } from '../../../lib/repo';
3
+ import { ApiError, apiError, json, readJson, requireSession } from '../../../lib/api';
4
+ import { AppAccessInput, appAccessAction, appAccessState } from '../../../lib/mobile/access-admin';
5
+
6
+ // Admin → Mobile app → App access (owner only, this site's admin only).
7
+ export const GET: APIRoute = async ({ request, cookies, locals }) => {
8
+ try {
9
+ const repo = await getRepo(locals);
10
+ await requireSession(request, cookies, repo, true);
11
+ return json(await appAccessState(repo, locals));
12
+ } catch (e) { return apiError(e); }
13
+ };
14
+ export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
15
+ try {
16
+ const repo = await getRepo(locals);
17
+ await requireSession(request, cookies, repo, true);
18
+ if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Change this from this site’s admin.');
19
+ await appAccessAction(repo, AppAccessInput.parse(await readJson(request, 5_000)));
20
+ return json(await appAccessState(repo, locals));
21
+ } catch (e) { return apiError(e); }
22
+ };
@@ -0,0 +1,22 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getRepo } from '../../lib/repo';
3
+ import { ApiError, apiError, json, readJson, requireSession } from '../../lib/api';
4
+ import { MonitoringInput, monitoringAction, monitoringState } from '../../lib/monitoring';
5
+
6
+ // Settings → Monitoring (owner only, this site's admin only).
7
+ export const GET: APIRoute = async ({ request, cookies, locals }) => {
8
+ try {
9
+ const repo = await getRepo(locals);
10
+ await requireSession(request, cookies, repo, true);
11
+ return json(await monitoringState(repo, locals));
12
+ } catch (e) { return apiError(e); }
13
+ };
14
+ export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
15
+ try {
16
+ const repo = await getRepo(locals);
17
+ await requireSession(request, cookies, repo, true);
18
+ if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Change this from this site’s admin.');
19
+ await monitoringAction(repo, MonitoringInput.parse(await readJson(request, 5_000)), locals);
20
+ return json(await monitoringState(repo, locals));
21
+ } catch (e) { return apiError(e); }
22
+ };
@@ -17,7 +17,8 @@ export const GET: APIRoute = async ({ request, locals }) => {
17
17
  return Response.json({ ok: canonicalHttps && secretsPresent && schedulerHealthy,
18
18
  database: env?.DB ? 'd1' : 'sqlite', media: env?.MEDIA ? 'r2' : 'filesystem',
19
19
  canonicalHttps, secretsPresent, schedulerHealthy, scheduler,
20
- }, { headers });
20
+ // 503 when anything is wrong, so a plain HTTP monitor sees it too
21
+ }, { status: canonicalHttps && secretsPresent && schedulerHealthy ? 200 : 503, headers });
21
22
  } catch { return Response.json({ ok: false, error: 'Unable to check deployment.' }, { status: 503, headers }); }
22
23
  };
23
24
  // Explicit authenticated probe; writes only a random short-lived object.
@@ -1,7 +1,8 @@
1
1
  import type { APIRoute } from 'astro';
2
2
  import { getRepo } from '../../../lib/repo';
3
3
  import { ApiError, apiError, json, readJson, requireSession } from '../../../lib/api';
4
- import { OWNER_ENV, SESSION_COOKIE, validateSession } from '../../../lib/auth';
4
+ import { OWNER_ENV } from '../../../lib/auth';
5
+ import { validateRequestSession } from '../../../lib/auth/request';
5
6
  import { AdminPushInput, adminPushAction, adminPushState } from '../../../lib/push/api';
6
7
 
7
8
  // Admin → Mobile app (owner only; registered only when push is enabled).
@@ -19,7 +20,7 @@ export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
19
20
  // sending reaches every installed app: only from this site's admin
20
21
  if (request.headers.get('origin') !== url.origin) throw new ApiError(403, 'Send from this site’s admin.');
21
22
  const input = AdminPushInput.parse(await readJson(request, 20_000));
22
- const id = await validateSession(cookies.get(SESSION_COOKIE)?.value, repo);
23
+ const id = await validateRequestSession(request, cookies, repo);
23
24
  const author = id === OWNER_ENV ? 'Owner' : (await repo.getUser(id ?? ''))?.name ?? '';
24
25
  const result = await adminPushAction(repo, input, { url, locals, author });
25
26
  return json({ ...result, state: input.action === 'count' ? undefined : await adminPushState(repo, url, locals) });
@@ -1,12 +1,13 @@
1
1
  import type { APIRoute } from 'astro';
2
- import { validateSession, OWNER_ENV, SESSION_COOKIE } from '../../lib/auth';
2
+ import { OWNER_ENV } from '../../lib/auth';
3
+ import { getRequestAuth, validateRequestSession } from '../../lib/auth/request';
3
4
  import { getRepo } from '../../lib/repo';
4
5
  import { getMediaStore } from '../../lib/media/store';
5
6
  import { unzip } from '../../lib/backup/unzip';
6
7
  import { BackupContent } from '../../lib/backup/content';
7
8
 
8
9
  export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
9
- const sessionUserId = (await validateSession(cookies.get(SESSION_COOKIE)?.value, locals));
10
+ const sessionUserId = (await validateRequestSession(request, cookies, locals));
10
11
  if (!sessionUserId) return Response.json({ error: 'Unauthorized' }, { status: 401 });
11
12
  const repo = await getRepo(locals);
12
13
  const sessionUser = sessionUserId !== OWNER_ENV ? await repo.getUser(sessionUserId) : null;
@@ -45,6 +46,8 @@ export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
45
46
  return Response.json({ error: 'content.json failed validation — is this from a newer acms?' }, { status: 400 });
46
47
  }
47
48
  const content = parsed.data;
49
+ const auth = await getRequestAuth(request, cookies, locals);
50
+ if (auth.clerk && (!auth.reverified || !content.users.some(u => u.id === sessionUserId && u.role === 'owner' && u.email === sessionUser?.email))) return Response.json({error:'A Clerk restore requires recent verification and a backup containing your current owner account. Sign in again and choose a matching backup.'}, {status:409});
48
51
  delete content.settings.ai;
49
52
  delete content.settings.agent;
50
53
 
@@ -86,7 +89,7 @@ export const POST: APIRoute = async ({ request, cookies, locals, url }) => {
86
89
  await store.put(key, buf, m.mime);
87
90
  }
88
91
  }
89
- await repo.restoreAll(content);
92
+ await repo.restoreAll(content, auth.clerk ? { issuer: auth.clerk.issuer, subject: auth.clerk.subject, userId: sessionUserId } : undefined);
90
93
 
91
94
  return Response.json({ ok: true, restored: { posts: content.posts.length, pages: content.pages.length, media: content.media.length, events: content.events.length, bookings: content.bookings.length, bookingRequests: content.bookingRequests?.length ?? 0 } }, { headers: { 'Cache-Control': 'no-store' } });
92
95
  };
@@ -1,11 +1,17 @@
1
1
  import { editorRoute, logWrite } from '../../../../../../lib/editor/http';
2
2
  import { BookingActionInput, bookingAction } from '../../../../../../lib/editor/bookings';
3
+ import { RescheduleInput, staffReschedule } from '../../../../../../lib/editor/schedule';
3
4
  import { methodNotAllowed } from '../../../../../../lib/mobile/http';
4
5
  import { readJson } from '../../../../../../lib/api';
5
6
  export { preflight as OPTIONS } from '../../../../../../lib/mobile/http';
6
- // approve | decline | cancel — the guest is emailed, as from Admin → Bookings
7
+ // approve | decline | cancel | reschedule — the guest is emailed, as from Admin → Bookings
7
8
  export const POST = editorRoute(async (c) => {
8
9
  const { id, action } = c.context.params;
10
+ if (action === 'reschedule') {
11
+ const result = await staffReschedule(c, id!, RescheduleInput.parse(await readJson(c.context.request, 5_000)));
12
+ await logWrite(c, 'booking-reschedule', `booking:${id}`);
13
+ return result;
14
+ }
9
15
  const body = c.context.request.headers.get('content-type') ? BookingActionInput.parse(await readJson(c.context.request, 5_000)) : {};
10
16
  const result = await bookingAction(c.repo, id!, action!, body, { url: c.context.url, locals: c.context.locals });
11
17
  await logWrite(c, `booking-${action}`, `booking:${id}`);
@@ -1,6 +1,14 @@
1
- import { editorRoute } from '../../../../../lib/editor/http';
1
+ import { editorRoute, logWrite } from '../../../../../lib/editor/http';
2
2
  import { InboxQuery, inbox } from '../../../../../lib/editor/bookings';
3
+ import { StaffBookingInput, staffBook } from '../../../../../lib/editor/schedule';
3
4
  import { methodNotAllowed, queryParams } from '../../../../../lib/mobile/http';
5
+ import { readJson } from '../../../../../lib/api';
4
6
  export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
5
7
  export const GET = editorRoute(async ({ repo, context }) => inbox(repo, InboxQuery.parse(queryParams(context.url))));
6
- export const ALL = () => methodNotAllowed('GET, OPTIONS');
8
+ // book on someone's behalf (the guest gets the usual emails)
9
+ export const POST = editorRoute(async (c) => {
10
+ const b = await staffBook(c, StaffBookingInput.parse(await readJson(c.context.request, 50_000)));
11
+ await logWrite(c, 'booking-create', `booking:${b.id}`);
12
+ return b;
13
+ }, { write: true, status: 201 });
14
+ export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
@@ -0,0 +1,16 @@
1
+ import { editorRoute, logWrite } from '../../../../../lib/editor/http';
2
+ import { CalendarPatch, deleteCalendarFor, updateCalendarFor } from '../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
6
+ export const PATCH = editorRoute(async (c) => {
7
+ const cal = await updateCalendarFor(c, c.context.params.id!, CalendarPatch.parse(await readJson(c.context.request, 10_000)));
8
+ await logWrite(c, 'calendar-update', `calendar:${cal.id}`);
9
+ return cal;
10
+ }, { write: true });
11
+ export const DELETE = editorRoute(async (c) => {
12
+ const cal = await deleteCalendarFor(c, c.context.params.id!);
13
+ await logWrite(c, 'calendar-delete', `calendar:${cal.id}`);
14
+ return cal;
15
+ }, { write: true });
16
+ export const ALL = () => methodNotAllowed('PATCH, DELETE, OPTIONS');
@@ -0,0 +1,12 @@
1
+ import { editorRoute, logWrite } from '../../../../../lib/editor/http';
2
+ import { CalendarBody, createCalendarFor, listCalendarsFor } from '../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
6
+ export const GET = editorRoute(async (c) => listCalendarsFor(c));
7
+ export const POST = editorRoute(async (c) => {
8
+ const cal = await createCalendarFor(c, CalendarBody.parse(await readJson(c.context.request, 10_000)));
9
+ await logWrite(c, 'calendar-create', `calendar:${cal.id}`);
10
+ return cal;
11
+ }, { write: true, status: 201 });
12
+ export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
@@ -0,0 +1,7 @@
1
+ import { editorRoute } from '../../../../../../lib/editor/http';
2
+ import { ResponsesQuery, formResponsesFor } from '../../../../../../lib/editor/site-data';
3
+ import { methodNotAllowed, queryParams } from '../../../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../../../lib/mobile/http';
5
+ // administrators and owners only: responses hold personal details
6
+ export const GET = editorRoute(async (c) => formResponsesFor(c, c.context.params.id!, ResponsesQuery.parse(queryParams(c.context.url))));
7
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { editorRoute } from '../../../../../lib/editor/http';
2
+ import { listFormsFor } from '../../../../../lib/editor/site-data';
3
+ import { methodNotAllowed } from '../../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
5
+ export const GET = editorRoute(async (c) => listFormsFor(c));
6
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -0,0 +1,13 @@
1
+ import { editorRoute, logWrite } from '../../../../lib/editor/http';
2
+ import { LinksPut, getLinksFor, putLinksFor } from '../../../../lib/editor/site-data';
3
+ import { methodNotAllowed } from '../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
6
+ export const GET = editorRoute(async (c) => getLinksFor(c));
7
+ // replaces the whole page (profile + blocks in order)
8
+ export const PUT = editorRoute(async (c) => {
9
+ const result = await putLinksFor(c, LinksPut.parse(await readJson(c.context.request, 300_000)));
10
+ await logWrite(c, 'links-update', 'links');
11
+ return result;
12
+ }, { write: true });
13
+ export const ALL = () => methodNotAllowed('GET, PUT, OPTIONS');
@@ -0,0 +1,10 @@
1
+ import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
2
+ import { removeBlock } from '../../../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
5
+ export const DELETE = editorRoute(async (c) => {
6
+ const b = await removeBlock(c, c.context.params.id!, c.context.params.blockId!);
7
+ await logWrite(c, 'block-delete', `booking:${b.id}`);
8
+ return b;
9
+ }, { write: true });
10
+ export const ALL = () => methodNotAllowed('DELETE, OPTIONS');
@@ -0,0 +1,12 @@
1
+ import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
2
+ import { BlockInput, addBlock } from '../../../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
6
+ // close a room for a while (no booking can land on it)
7
+ export const POST = editorRoute(async (c) => {
8
+ const b = await addBlock(c, c.context.params.id!, BlockInput.parse(await readJson(c.context.request, 5_000)));
9
+ await logWrite(c, 'block-create', `booking:${b.id}`);
10
+ return b;
11
+ }, { write: true, status: 201 });
12
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -0,0 +1,17 @@
1
+ import { editorRoute, logWrite } from '../../../../../../lib/editor/http';
2
+ import { ResourcePatch, deleteResourceFor, getResourceFor, updateResourceFor } from '../../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../../../lib/mobile/http';
6
+ export const GET = editorRoute(async (c) => getResourceFor(c, c.context.params.id!));
7
+ export const PATCH = editorRoute(async (c) => {
8
+ const r = await updateResourceFor(c, c.context.params.id!, ResourcePatch.parse(await readJson(c.context.request, 100_000)));
9
+ await logWrite(c, 'resource-update', `resource:${r.id}`);
10
+ return r;
11
+ }, { write: true });
12
+ export const DELETE = editorRoute(async (c) => {
13
+ const r = await deleteResourceFor(c, c.context.params.id!);
14
+ await logWrite(c, 'resource-delete', `resource:${r.id}`);
15
+ return r;
16
+ }, { write: true });
17
+ export const ALL = () => methodNotAllowed('GET, PATCH, DELETE, OPTIONS');
@@ -0,0 +1,16 @@
1
+ import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
2
+ import { LinkPatch, deleteLinkFor, updateLinkFor } from '../../../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
6
+ export const PATCH = editorRoute(async (c) => {
7
+ const l = await updateLinkFor(c, c.context.params.id!, c.context.params.linkId!, LinkPatch.parse(await readJson(c.context.request, 100_000)));
8
+ await logWrite(c, 'booking-link-update', `booking-link:${l.id}`);
9
+ return l;
10
+ }, { write: true });
11
+ export const DELETE = editorRoute(async (c) => {
12
+ const l = await deleteLinkFor(c, c.context.params.id!, c.context.params.linkId!);
13
+ await logWrite(c, 'booking-link-delete', `booking-link:${l.id}`);
14
+ return l;
15
+ }, { write: true });
16
+ export const ALL = () => methodNotAllowed('PATCH, DELETE, OPTIONS');
@@ -0,0 +1,12 @@
1
+ import { editorRoute, logWrite } from '../../../../../../../lib/editor/http';
2
+ import { LinkInput, createLinkFor } from '../../../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../../../../lib/mobile/http';
6
+ // a new booking type for this room or service
7
+ export const POST = editorRoute(async (c) => {
8
+ const l = await createLinkFor(c, c.context.params.id!, LinkInput.parse(await readJson(c.context.request, 100_000)));
9
+ await logWrite(c, 'booking-link-create', `booking-link:${l.id}`);
10
+ return l;
11
+ }, { write: true, status: 201 });
12
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -0,0 +1,12 @@
1
+ import { editorRoute, logWrite } from '../../../../../lib/editor/http';
2
+ import { ResourceInput, createResourceFor, listResourcesFor } from '../../../../../lib/editor/schedule';
3
+ import { methodNotAllowed } from '../../../../../lib/mobile/http';
4
+ import { readJson } from '../../../../../lib/api';
5
+ export { preflight as OPTIONS } from '../../../../../lib/mobile/http';
6
+ export const GET = editorRoute(async (c) => listResourcesFor(c));
7
+ export const POST = editorRoute(async (c) => {
8
+ const r = await createResourceFor(c, ResourceInput.parse(await readJson(c.context.request, 100_000)));
9
+ await logWrite(c, 'resource-create', `resource:${r.id}`);
10
+ return r;
11
+ }, { write: true, status: 201 });
12
+ export const ALL = () => methodNotAllowed('GET, POST, OPTIONS');
@@ -0,0 +1,7 @@
1
+ import { editorRoute } from '../../../../lib/editor/http';
2
+ import { ScheduleQuery, schedule } from '../../../../lib/editor/schedule';
3
+ import { methodNotAllowed, queryParams } from '../../../../lib/mobile/http';
4
+ export { preflight as OPTIONS } from '../../../../lib/mobile/http';
5
+ // bookings, blocked time, events and subscribed calendars for a date range
6
+ export const GET = editorRoute(async (c) => schedule(c, ScheduleQuery.parse(queryParams(c.context.url))));
7
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { managedAvailability } from '../../../../../lib/mobile/manage';
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(managedAvailability);
6
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { cancelManaged } from '../../../../../lib/mobile/manage';
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(cancelManaged, { write: true });
6
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { managedIcs } from '../../../../../lib/mobile/manage';
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(managedIcs);
6
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { getManaged } from '../../../../../lib/mobile/manage';
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(getManaged);
6
+ export const ALL = () => methodNotAllowed('GET, OPTIONS');
@@ -0,0 +1,6 @@
1
+ import { rescheduleManaged } from '../../../../../lib/mobile/manage';
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(rescheduleManaged, { write: true });
6
+ export const ALL = () => methodNotAllowed('POST, OPTIONS');
@@ -2,6 +2,6 @@ import { listCalendars } 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(listCalendars, true);
5
+ export const GET = readRoute(listCalendars, true, false, { timeless: true });
6
6
  export const HEAD = GET;
7
7
  export const ALL = () => methodNotAllowed();