@acms-subir/core 0.3.33 → 0.3.34

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @acms-subir/core
2
2
 
3
+ ## 0.3.34
4
+
5
+ ### Patch Changes
6
+
7
+ - 2e58587: The privacy notice (English and Spanish) now says that, on Android, the app reports which notification channels it created, and that important notices such as cancellations may reach every device with notifications on whatever topics it chose. With announcements on, it names urgent announcements and says the app remembers on the phone which announcements were read or closed, which is never sent to the site. The App store answers list the channels too. Sites with push get a new automatic "last updated" date.
8
+
9
+ The public app API (`/api/v1/*`) now tells browsers to revalidate it every time (`Cache-Control: public, max-age=0, s-maxage=60`) and gives CDNs the stale-while-revalidate policy in `CDN-Cache-Control`. An Expo web build or web app no longer starts once from an old `/site` copy after the site updates. Native apps and pages are unchanged.
10
+
3
11
  ## 0.3.33
4
12
 
5
13
  ### Patch Changes
package/integration.mjs CHANGED
@@ -653,7 +653,8 @@ function acmsVitePlugin(userConfig) {
653
653
  * @param {false | {maxAge?: number, staleWhileRevalidate?: number, memory?: boolean}} [options.cache]
654
654
  * Shared caching of public pages and the public app API (`/api/v1/*`):
655
655
  * `Cache-Control: public, s-maxage=<maxAge>, stale-while-revalidate=<staleWhileRevalidate>`
656
- * (defaults 60 and 86400). Admin, previews, signed-in visitors, bookings,
656
+ * (defaults 60 and 86400); the API sends that as `CDN-Cache-Control` and
657
+ * `public, max-age=0, s-maxage=<maxAge>` to browsers, which revalidate it. Admin, previews, signed-in visitors, bookings,
657
658
  * forms and every non-GET request stay `private, no-store`. Every content
658
659
  * change clears the cache (Workers Cache, or the Cloudflare zone when
659
660
  * CLOUDFLARE_ZONE_ID + CLOUDFLARE_API_TOKEN are set). `memory: false` turns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/core",
3
- "version": "0.3.33",
3
+ "version": "0.3.34",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -8,6 +8,12 @@ import { I18N } from '../i18n';
8
8
 
9
9
  export const PRIVATE_CACHE_CONTROL = 'private, no-store';
10
10
  export const publicCacheControl = () => `public, s-maxage=${CACHE.maxAge}, stale-while-revalidate=${CACHE.staleWhileRevalidate}`;
11
+ // The public app API: browsers (Expo web, web apps) revalidate by ETag every
12
+ // time, since stale-while-revalidate would hand them an old /site without a
13
+ // feature the app just got. CDNs that read CDN-Cache-Control (RFC 9213, e.g.
14
+ // Cloudflare) still serve it stale while they refresh, like pages; others fall
15
+ // back to s-maxage.
16
+ export const apiCacheControl = () => `public, max-age=0, s-maxage=${CACHE.maxAge}`;
11
17
  // publicResponse's own header: browsers/apps always revalidate by ETag. The
12
18
  // policy upgrades it with s-maxage so a CDN can share it.
13
19
  export const API_REVALIDATE = 'public, max-age=0, must-revalidate';
@@ -64,12 +70,13 @@ export function applyCachePolicy(request: Request, response: Response, pathname
64
70
  if (!CACHE.enabled) return response;
65
71
  if (OWN_HEADERS.test(pathname)) return response;
66
72
  const current = response.headers.get('cache-control');
67
- const set = (value: string) => {
68
- try { response.headers.set('Cache-Control', value); return response; }
73
+ const set = (value: string, cdn?: string) => {
74
+ const write = (h: Headers) => { h.set('Cache-Control', value); if (cdn) h.set('CDN-Cache-Control', cdn); };
75
+ try { write(response.headers); return response; }
69
76
  catch {
70
77
  // immutable headers (a proxied/fetched response): copy it
71
78
  const copy = new Response(response.body, response);
72
- copy.headers.set('Cache-Control', value);
79
+ write(copy.headers);
73
80
  return copy;
74
81
  }
75
82
  };
@@ -85,5 +92,6 @@ export function applyCachePolicy(request: Request, response: Response, pathname
85
92
  warnCookie(pathname);
86
93
  return set(PRIVATE_CACHE_CONTROL);
87
94
  }
95
+ if (pathname.startsWith('/api/v1/')) return set(apiCacheControl(), publicCacheControl());
88
96
  return set(publicCacheControl());
89
97
  }
@@ -29,7 +29,7 @@ export function appPrivacyAnswers(f: LegalFacts, privacyUrl: string): AppPrivacy
29
29
  google: 'Personal info → Email address. Collected, not shared. Purpose: App functionality, Account management. Optional.',
30
30
  });
31
31
  if (f.push.enabled) rows.push({
32
- data: 'Push token, platform, language and chosen topics', when: 'When someone turns on notifications',
32
+ data: 'Push token, platform, language, chosen topics and Android notification channels', when: 'When someone turns on notifications',
33
33
  apple: 'Identifiers → Device ID. Purpose: App Functionality. Linked to the user. Not used for tracking.',
34
34
  google: 'Device or other IDs. Collected, not shared (Expo, Apple and Google deliver on your behalf). Purpose: App functionality, Developer communications. Optional.',
35
35
  });
@@ -3,6 +3,7 @@ import type { ContentRepository } from '../repo/types';
3
3
  import type { Settings } from '../schema';
4
4
  import { CALENDAR, bookingsEnabled } from '../calendar/config';
5
5
  import { PUSH } from '../push/config';
6
+ import { ANNOUNCEMENTS } from '../announcements/config';
6
7
  import { AUTH } from '../auth/config';
7
8
  import { EDITOR } from '../editor/config';
8
9
  import { SITE_ROLES } from '../roles';
@@ -39,6 +40,8 @@ export interface LegalFacts {
39
40
  /** maps provider name, or '' */
40
41
  maps: string;
41
42
  live: boolean;
43
+ /** acms({ announcements }): urgent ones reach every device; read and closed state stays in the app */
44
+ announcements: boolean;
42
45
  analytics: { umami: boolean; replay: boolean; meta: boolean; tiktok: boolean };
43
46
  ai: boolean;
44
47
  offsiteBackups: boolean;
@@ -79,6 +82,7 @@ export async function legalFacts(repo: ContentRepository, settings: Settings, lo
79
82
  email: email && !email.error && email.provider !== 'none' && email.config.enabled ? EMAIL_NAMES[email.provider] ?? email.provider : '',
80
83
  maps: (() => { const m = mapsStatus(locals); return m.enabled ? m.label.replace(/ \(self-hosted\)$/, '') : ''; })(),
81
84
  live: settings.live.mode !== 'off',
85
+ announcements: ANNOUNCEMENTS.enabled,
82
86
  analytics: {
83
87
  umami: a.umami.enabled && !!a.umami.websiteId,
84
88
  replay: a.umami.enabled && !!a.umami.websiteId && a.umami.replay.enabled,
@@ -52,8 +52,8 @@ export function privacyEn(f: LegalFacts, country: string): PolicySection[] {
52
52
  'To see your bookings in our app, you can sign in with your email address. We email you a six-digit code that expires after 10 minutes. We keep your email address and that sign-in on your device until you sign out or don’t use it for 90 days.',
53
53
  ] });
54
54
  if (f.push.enabled) s.push({ id: 'push', title: 'Our app and notifications', text: [
55
- 'If you turn on notifications in our app, we store a push token for your device (an identifier that lets Apple or Google deliver notifications to it), whether it’s iOS or Android, your language and the topics you choose. We use them only to send the notifications you asked for, in your language. Notifications are delivered through Expo’s push service (expo.dev), Apple Push Notification service and Firebase Cloud Messaging.',
56
- `You choose the topics in the app and can change them at any time; we never add a topic without asking you. To stop, turn notifications off in the app or in your phone’s settings. Turning them off in the app removes your device from our list right away. Devices that haven’t opened the app for ${days(f.push.retentionDays)}, and devices whose app was removed, are deleted automatically. We keep a record of each notification sent (its text, link and number of recipients, not who received it) for one year. The app works fully without notifications.`,
55
+ 'If you turn on notifications in our app, we store a push token for your device (an identifier that lets Apple or Google deliver notifications to it), whether it’s iOS or Android, your language, the topics you choose and, on Android, which notification channels the app created (such as one for urgent notices). We use them only to send the notifications you asked for, in your language. Notifications are delivered through Expo’s push service (expo.dev), Apple Push Notification service and Firebase Cloud Messaging.',
56
+ `You choose the topics in the app and can change them at any time; we never add a topic without asking you. ${f.announcements ? 'Urgent announcements, such as cancellations, go to every device with notifications turned on, whatever topics it chose.' : 'Important notices, such as cancellations, may go to every device with notifications turned on, whatever topics it chose.'} To stop, turn notifications off in the app or in your phone’s settings. Turning them off in the app removes your device from our list right away. Devices that haven’t opened the app for ${days(f.push.retentionDays)}, and devices whose app was removed, are deleted automatically. We keep a record of each notification sent (its text, link and number of recipients, not who received it) for one year. The app works fully without notifications.`,
57
57
  ] });
58
58
  if (f.email) s.push({ id: 'email', title: 'Email', text: [
59
59
  `We send email through ${f.email}, which processes your email address and the message. We keep a delivery record (recipient and status) for 30 days, and keep addresses that bounce or report our email as spam on a list so we stop writing to them.`,
@@ -79,7 +79,9 @@ export function privacyEn(f: LegalFacts, country: string): PolicySection[] {
79
79
  ] });
80
80
  s.push({ id: 'cookies', title: 'Cookies and storage on your device', text: [
81
81
  `${f.auth === 'clerk' ? 'Clerk’s sign-in cookies' : 'A sign-in cookie (up to seven days)'} for people who sign in; a preview cookie only for staff previews and design share links (up to 30 days); and, in your browser, your light or dark mode choice, the announcements you close and, for one visit, the live video notices you close.${pixels ? ` ${pixelNames} ${f.analytics.meta && f.analytics.tiktok ? 'set their' : 'sets its'} own advertising cookies.` : ' We don’t use cookies for advertising.'}`,
82
- ...(app ? ['Our app may keep the content it last loaded on your phone so it works offline, and remembers your notification choice there.'] : []),
82
+ ...(app ? [f.announcements
83
+ ? 'Our app may keep the content it last loaded on your phone so it works offline, and remembers there your notification choice and which announcements you’ve read or closed. That stays on your phone and is never sent to us.'
84
+ : 'Our app may keep the content it last loaded on your phone so it works offline, and remembers your notification choice there.'] : []),
83
85
  ] });
84
86
  s.push({ id: 'sharing', title: 'Who receives your information', text: [
85
87
  `We don’t sell your personal information${pixels ? '' : ' or share it for advertising'}. These service providers process it on our behalf and only for the purposes above: ${list(processors(f))}. They must protect it at least as this notice describes. Some of them may store it outside your country, including in the United States.`,
@@ -53,8 +53,8 @@ export function privacyEs(f: LegalFacts, country: string): PolicySection[] {
53
53
  'Para ver sus reservaciones en nuestra app, puede iniciar sesión con su correo electrónico. Le enviamos un código de seis dígitos que vence a los 10 minutos. Guardamos su correo y esa sesión en su dispositivo hasta que la cierre o pasen 90 días sin usarla.',
54
54
  ] });
55
55
  if (f.push.enabled) s.push({ id: 'push', title: 'Nuestra app y las notificaciones', text: [
56
- 'Si activa las notificaciones en nuestra app, guardamos un identificador de notificaciones de su dispositivo (que permite a Apple o Google entregárselas), si es iOS o Android, su idioma y los temas que elija. Solo los usamos para enviarle las notificaciones que pidió, en su idioma. Las notificaciones se entregan a través del servicio de Expo (expo.dev), Apple Push Notification service y Firebase Cloud Messaging.',
57
- `Usted elige los temas en la app y puede cambiarlos cuando quiera; nunca agregamos un tema sin preguntarle. Para dejar de recibirlas, desactive las notificaciones en la app o en los ajustes de su teléfono. Al desactivarlas en la app, su dispositivo se elimina de nuestra lista de inmediato. Los dispositivos que no han abierto la app en ${days(f.push.retentionDays)}, y aquellos en los que se borró la app, se eliminan automáticamente. Guardamos un registro de cada notificación enviada (su texto, su enlace y el número de destinatarios, no quiénes la recibieron) durante un año. La app funciona por completo sin notificaciones.`,
56
+ 'Si activa las notificaciones en nuestra app, guardamos un identificador de notificaciones de su dispositivo (que permite a Apple o Google entregárselas), si es iOS o Android, su idioma, los temas que elija y, en Android, qué canales de notificación creó la app (por ejemplo, uno para avisos urgentes). Solo los usamos para enviarle las notificaciones que pidió, en su idioma. Las notificaciones se entregan a través del servicio de Expo (expo.dev), Apple Push Notification service y Firebase Cloud Messaging.',
57
+ `Usted elige los temas en la app y puede cambiarlos cuando quiera; nunca agregamos un tema sin preguntarle. ${f.announcements ? 'Los anuncios urgentes, como una cancelación, llegan a todos los dispositivos con las notificaciones activadas, sin importar los temas que eligieron.' : 'Los avisos importantes, como una cancelación, pueden llegar a todos los dispositivos con las notificaciones activadas, sin importar los temas que eligieron.'} Para dejar de recibirlas, desactive las notificaciones en la app o en los ajustes de su teléfono. Al desactivarlas en la app, su dispositivo se elimina de nuestra lista de inmediato. Los dispositivos que no han abierto la app en ${days(f.push.retentionDays)}, y aquellos en los que se borró la app, se eliminan automáticamente. Guardamos un registro de cada notificación enviada (su texto, su enlace y el número de destinatarios, no quiénes la recibieron) durante un año. La app funciona por completo sin notificaciones.`,
58
58
  ] });
59
59
  if (f.email) s.push({ id: 'email', title: 'Correo electrónico', text: [
60
60
  `Enviamos correos a través de ${f.email}, que trata su dirección de correo y el mensaje. Guardamos un registro de entrega (destinatario y estado) durante 30 días, y mantenemos en una lista las direcciones que rebotan o marcan nuestros correos como spam para dejar de escribirles.`,
@@ -80,7 +80,9 @@ export function privacyEs(f: LegalFacts, country: string): PolicySection[] {
80
80
  ] });
81
81
  s.push({ id: 'cookies', title: 'Cookies y almacenamiento en su dispositivo', text: [
82
82
  `${f.auth === 'clerk' ? 'Las cookies de inicio de sesión de Clerk' : 'Una cookie de inicio de sesión (hasta siete días)'} para quienes inician sesión; una cookie de vista previa solo para las vistas previas del personal y los enlaces para compartir diseños (hasta 30 días); y, en su navegador, su preferencia de modo claro u oscuro, los anuncios que cierra y, durante una visita, los avisos de video en vivo que cierra.${pixels ? ` ${pixelNames} ${both ? 'guardan sus propias' : 'guarda sus propias'} cookies de publicidad.` : ' No usamos cookies con fines publicitarios.'}`,
83
- ...(app ? ['Nuestra app puede guardar en su teléfono el contenido que cargó por última vez para funcionar sin conexión, y recuerda ahí su preferencia de notificaciones.'] : []),
83
+ ...(app ? [f.announcements
84
+ ? 'Nuestra app puede guardar en su teléfono el contenido que cargó por última vez para funcionar sin conexión, y recuerda ahí su preferencia de notificaciones y qué anuncios leyó o cerró. Eso se queda en su teléfono y nunca se nos envía.'
85
+ : 'Nuestra app puede guardar en su teléfono el contenido que cargó por última vez para funcionar sin conexión, y recuerda ahí su preferencia de notificaciones.'] : []),
84
86
  ] });
85
87
  s.push({ id: 'sharing', title: 'Quién recibe sus datos', text: [
86
88
  `No vendemos sus datos personales${pixels ? '' : ' ni los compartimos con fines publicitarios'}. Estos proveedores los tratan por cuenta nuestra y solo para los fines descritos: ${list(processors(f))}. Deben protegerlos al menos como lo describe este aviso. Algunos pueden guardarlos fuera de su país, incluido Estados Unidos.`,