@aglyn/tenant-data-admin 1.0.0-beta.230 → 1.0.0-beta.232

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 (48) hide show
  1. package/package.json +7 -7
  2. package/src/lib/server/account-acquisition.d.ts +30 -0
  3. package/src/lib/server/account-acquisition.js +37 -2
  4. package/src/lib/server/account-acquisition.js.map +1 -1
  5. package/src/lib/server/api-keys.d.ts +14 -1
  6. package/src/lib/server/api-keys.js +28 -0
  7. package/src/lib/server/api-keys.js.map +1 -1
  8. package/src/lib/server/content-entry-drafts.d.ts +131 -0
  9. package/src/lib/server/content-entry-drafts.js +307 -0
  10. package/src/lib/server/content-entry-drafts.js.map +1 -0
  11. package/src/lib/server/erase.js +14 -0
  12. package/src/lib/server/erase.js.map +1 -1
  13. package/src/lib/server/host-released-names.d.ts +22 -0
  14. package/src/lib/server/host-released-names.js +31 -0
  15. package/src/lib/server/host-released-names.js.map +1 -0
  16. package/src/lib/server/host-visitor-records.d.ts +1 -1
  17. package/src/lib/server/host-visitor-records.js +1 -1
  18. package/src/lib/server/host-visitor-records.js.map +1 -1
  19. package/src/lib/server/mobile-push-switch.d.ts +46 -0
  20. package/src/lib/server/mobile-push-switch.js +45 -0
  21. package/src/lib/server/mobile-push-switch.js.map +1 -0
  22. package/src/lib/server/native-push.d.ts +17 -0
  23. package/src/lib/server/native-push.js +35 -0
  24. package/src/lib/server/native-push.js.map +1 -0
  25. package/src/lib/server/notifications.js +23 -0
  26. package/src/lib/server/notifications.js.map +1 -1
  27. package/src/lib/server/personal-data-export.js +230 -0
  28. package/src/lib/server/personal-data-export.js.map +1 -1
  29. package/src/lib/server/plugin-event-outbox.d.ts +138 -0
  30. package/src/lib/server/plugin-event-outbox.js +229 -0
  31. package/src/lib/server/plugin-event-outbox.js.map +1 -0
  32. package/src/lib/server/push-apns.d.ts +73 -0
  33. package/src/lib/server/push-apns.js +225 -0
  34. package/src/lib/server/push-apns.js.map +1 -0
  35. package/src/lib/server/push-delivery.d.ts +78 -0
  36. package/src/lib/server/push-delivery.js +176 -0
  37. package/src/lib/server/push-delivery.js.map +1 -0
  38. package/src/lib/server/push-fcm.d.ts +29 -0
  39. package/src/lib/server/push-fcm.js +83 -0
  40. package/src/lib/server/push-fcm.js.map +1 -0
  41. package/src/lib/server/serve-media-cdn.d.ts +6 -0
  42. package/src/lib/server/serve-media-cdn.js +20 -3
  43. package/src/lib/server/serve-media-cdn.js.map +1 -1
  44. package/src/lib/server/site-marketing-sync.d.ts +146 -0
  45. package/src/lib/server/site-marketing-sync.js +315 -0
  46. package/src/lib/server/site-marketing-sync.js.map +1 -0
  47. package/src/lib/server/upsert-contact.js +6 -5
  48. package/src/lib/server/upsert-contact.js.map +1 -1
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ /**
18
+ * The native apps' push delivery (AGL-3651): a notification the fan-out
19
+ * wrote goes to each recipient's registered devices, grouped by transport —
20
+ * APNs for iOS and macOS (`push-apns.ts`), FCM for Android (`push-fcm.ts`).
21
+ *
22
+ * Server-only. `registerNativePushSenders` (`native-push.ts`) registers
23
+ * `nativePushSender` with the fan-out and imports this file only when a
24
+ * notification has push recipients, and this file imports each transport
25
+ * only when a device needs it.
26
+ *
27
+ * A row the registry no longer accepts is pruned here: an Expo row from the
28
+ * React Native app, a row with no transport, a token that does not fit its
29
+ * transport, and a device not seen for `MOBILE_DEVICE_STALE_MS`. A token a
30
+ * transport reports as gone is pruned after the send.
31
+ */
32
+ import { type ApnsEnvironment, type MobileDeviceApp, type MobilePushData, type MobilePushTransport } from '@aglyn/aglyn/app-utils/notification-push';
33
+ import type { Firestore } from 'firebase-admin/firestore';
34
+ import type { MobilePushPayload, MobilePushSender } from './mobile-push-switch';
35
+ /** One device a transport sends to. */
36
+ export interface PushTarget {
37
+ token: string;
38
+ app: MobileDeviceApp;
39
+ /** APNs: the app's bundle id, which is the push topic. */
40
+ topic: string;
41
+ apnsEnvironment?: ApnsEnvironment;
42
+ }
43
+ /** What every device of one notification is sent. */
44
+ export interface PushMessage {
45
+ title: string;
46
+ body?: string;
47
+ data: MobilePushData;
48
+ /** Critical and warning notifications ask the platform to deliver now. */
49
+ urgent: boolean;
50
+ }
51
+ /** A transport's verdict for each target, in order. `prune`: the token is gone; forget the row. */
52
+ export type PushOutcome = 'sent' | 'prune' | 'failed';
53
+ export interface PushTransport {
54
+ send(targets: readonly PushTarget[], message: PushMessage): Promise<PushOutcome[]>;
55
+ }
56
+ export interface PushDeliveryDeps {
57
+ db: Firestore;
58
+ /** Each transport, or null when the deployment has none configured. Resolved only when a device needs it. */
59
+ transport: (kind: MobilePushTransport) => Promise<PushTransport | null>;
60
+ now?: () => number;
61
+ }
62
+ export interface PushDeliveryResult {
63
+ sent: number;
64
+ /** Rows removed: stale, Expo or malformed rows, and tokens a transport reported gone. */
65
+ pruned: number;
66
+ failed: number;
67
+ /** Devices not sent to because their transport is not configured. */
68
+ skipped: number;
69
+ }
70
+ export declare function pushMessage(payload: MobilePushPayload, link: string | undefined): PushMessage;
71
+ /**
72
+ * Sends one notification to every live device of `uids`, each through its
73
+ * own transport. Never throws: the console notification is the record, and a
74
+ * push is a courtesy beside it.
75
+ */
76
+ export declare function deliverNativePush(uids: readonly string[], payload: MobilePushPayload, deps: PushDeliveryDeps): Promise<PushDeliveryResult>;
77
+ /** The sender `registerNativePushSenders` hands the fan-out. */
78
+ export declare const nativePushSender: MobilePushSender;
@@ -0,0 +1,176 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * The native apps' push delivery (AGL-3651): a notification the fan-out
19
+ * wrote goes to each recipient's registered devices, grouped by transport —
20
+ * APNs for iOS and macOS (`push-apns.ts`), FCM for Android (`push-fcm.ts`).
21
+ *
22
+ * Server-only. `registerNativePushSenders` (`native-push.ts`) registers
23
+ * `nativePushSender` with the fan-out and imports this file only when a
24
+ * notification has push recipients, and this file imports each transport
25
+ * only when a device needs it.
26
+ *
27
+ * A row the registry no longer accepts is pruned here: an Expo row from the
28
+ * React Native app, a row with no transport, a token that does not fit its
29
+ * transport, and a device not seen for `MOBILE_DEVICE_STALE_MS`. A token a
30
+ * transport reports as gone is pruned after the send.
31
+ */ import { isMobilePushToken, MOBILE_APP_BUNDLES, MOBILE_DEVICE_STALE_MS, MOBILE_DEVICES_COLLECTION, MOBILE_DEVICES_PER_USER, mobilePushData } from "@aglyn/aglyn/app-utils/notification-push";
32
+ import { normalizeNotificationLink } from "@aglyn/aglyn/server";
33
+ /** The most devices one notification is sent to, whatever the audience. */ const MAX_DEVICES = 1000;
34
+ /** A stored timestamp as epoch millis, or null when there is none. */ function millis(value) {
35
+ if (!value) return null;
36
+ if (typeof value === 'number') return value;
37
+ const withMillis = value;
38
+ return typeof withMillis.toMillis === 'function' ? withMillis.toMillis() : null;
39
+ }
40
+ export function pushMessage(payload, link) {
41
+ return _extends({
42
+ title: payload.title
43
+ }, payload.body ? {
44
+ body: payload.body
45
+ } : {}, {
46
+ data: mobilePushData(_extends({}, payload, {
47
+ link
48
+ })),
49
+ urgent: payload.level === 'critical' || payload.level === 'warning'
50
+ });
51
+ }
52
+ /** The link the app opens: the stored one, rewritten onto today's console routes. */ async function pushLink(db, payload) {
53
+ if (!payload.link) return undefined;
54
+ if (!payload.orgId) return normalizeNotificationLink(payload.link, {});
55
+ try {
56
+ var _payload_hostId;
57
+ const [org, host] = await Promise.all([
58
+ db.collection('orgs').doc(payload.orgId).get(),
59
+ payload.hostId ? db.collection('hosts').doc(payload.hostId).get() : Promise.resolve(null)
60
+ ]);
61
+ const slug = org.get('slug');
62
+ const subdomain = host == null ? void 0 : host.get('subdomain');
63
+ return normalizeNotificationLink(payload.link, {
64
+ orgSlug: typeof slug === 'string' ? slug : null,
65
+ hostId: (_payload_hostId = payload.hostId) != null ? _payload_hostId : null,
66
+ hostSubdomain: typeof subdomain === 'string' ? subdomain : null
67
+ });
68
+ } catch (unused) {
69
+ return normalizeNotificationLink(payload.link, {});
70
+ }
71
+ }
72
+ const isApp = (value)=>value === 'aglyn' || value === 'aglyn-pos';
73
+ /**
74
+ * Sends one notification to every live device of `uids`, each through its
75
+ * own transport. Never throws: the console notification is the record, and a
76
+ * push is a courtesy beside it.
77
+ */ export async function deliverNativePush(uids, payload, deps) {
78
+ var _deps_now;
79
+ const result = {
80
+ sent: 0,
81
+ pruned: 0,
82
+ failed: 0,
83
+ skipped: 0
84
+ };
85
+ if (!uids.length) return result;
86
+ const now = ((_deps_now = deps.now) != null ? _deps_now : Date.now)();
87
+ const prune = async (ref)=>{
88
+ await ref.delete().catch(()=>undefined);
89
+ result.pruned += 1;
90
+ };
91
+ try {
92
+ const { db } = deps;
93
+ const groups = new Map();
94
+ const seen = new Set();
95
+ let total = 0;
96
+ for (const uid of uids){
97
+ const devices = await db.collection('users').doc(uid).collection(MOBILE_DEVICES_COLLECTION).limit(MOBILE_DEVICES_PER_USER).get();
98
+ for (const device of devices.docs){
99
+ var _groups_get;
100
+ const transport = device.get('transport');
101
+ const token = device.get('token');
102
+ const app = device.get('app');
103
+ if (!isMobilePushToken(transport, token) || !isApp(app)) {
104
+ await prune(device.ref);
105
+ continue;
106
+ }
107
+ const lastSeen = millis(device.get('lastSeen'));
108
+ if (lastSeen !== null && now - lastSeen > MOBILE_DEVICE_STALE_MS) {
109
+ await prune(device.ref);
110
+ continue;
111
+ }
112
+ if (seen.has(token) || total >= MAX_DEVICES) continue;
113
+ seen.add(token);
114
+ total += 1;
115
+ const environment = device.get('apnsEnvironment');
116
+ const target = _extends({
117
+ token,
118
+ app,
119
+ topic: MOBILE_APP_BUNDLES[app]
120
+ }, transport === 'apns' && (environment === 'sandbox' || environment === 'production') ? {
121
+ apnsEnvironment: environment
122
+ } : {});
123
+ const group = (_groups_get = groups.get(transport)) != null ? _groups_get : [];
124
+ group.push({
125
+ target,
126
+ ref: device.ref
127
+ });
128
+ groups.set(transport, group);
129
+ }
130
+ }
131
+ if (!groups.size) return result;
132
+ const message = pushMessage(payload, await pushLink(db, payload));
133
+ for (const [kind, entries] of groups){
134
+ let transport = null;
135
+ try {
136
+ transport = await deps.transport(kind);
137
+ } catch (error) {
138
+ console.error(`push transport ${kind} failed to load`, error);
139
+ }
140
+ if (!transport) {
141
+ result.skipped += entries.length;
142
+ continue;
143
+ }
144
+ let outcomes;
145
+ try {
146
+ outcomes = await transport.send(entries.map((entry)=>entry.target), message);
147
+ } catch (error) {
148
+ console.error(`push transport ${kind} failed`, error);
149
+ result.failed += entries.length;
150
+ continue;
151
+ }
152
+ for (const [index, entry] of entries.entries()){
153
+ var _outcomes_index;
154
+ const outcome = (_outcomes_index = outcomes[index]) != null ? _outcomes_index : 'failed';
155
+ if (outcome === 'sent') result.sent += 1;
156
+ else if (outcome === 'prune') await prune(entry.ref);
157
+ else result.failed += 1;
158
+ }
159
+ }
160
+ } catch (error) {
161
+ console.error('push delivery failed', error);
162
+ }
163
+ return result;
164
+ }
165
+ /** The deployment's transports, each imported the first time a device needs it. */ async function deploymentTransport(kind) {
166
+ if (kind === 'apns') return (await import("./push-apns.js")).apnsTransport();
167
+ return (await import("./push-fcm.js")).fcmTransport();
168
+ }
169
+ /** The sender `registerNativePushSenders` hands the fan-out. */ export const nativePushSender = async (uids, payload, { db })=>{
170
+ await deliverNativePush(uids, payload, {
171
+ db,
172
+ transport: deploymentTransport
173
+ });
174
+ };
175
+
176
+ //# sourceMappingURL=push-delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/push-delivery.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The native apps' push delivery (AGL-3651): a notification the fan-out\n * wrote goes to each recipient's registered devices, grouped by transport —\n * APNs for iOS and macOS (`push-apns.ts`), FCM for Android (`push-fcm.ts`).\n *\n * Server-only. `registerNativePushSenders` (`native-push.ts`) registers\n * `nativePushSender` with the fan-out and imports this file only when a\n * notification has push recipients, and this file imports each transport\n * only when a device needs it.\n *\n * A row the registry no longer accepts is pruned here: an Expo row from the\n * React Native app, a row with no transport, a token that does not fit its\n * transport, and a device not seen for `MOBILE_DEVICE_STALE_MS`. A token a\n * transport reports as gone is pruned after the send.\n */\n\nimport {\n isMobilePushToken,\n MOBILE_APP_BUNDLES,\n MOBILE_DEVICE_STALE_MS,\n MOBILE_DEVICES_COLLECTION,\n MOBILE_DEVICES_PER_USER,\n mobilePushData,\n type ApnsEnvironment,\n type MobileDeviceApp,\n type MobilePushData,\n type MobilePushTransport,\n} from '@aglyn/aglyn/app-utils/notification-push'\nimport { normalizeNotificationLink } from '@aglyn/aglyn/server'\nimport type { DocumentReference, Firestore } from 'firebase-admin/firestore'\nimport type { MobilePushPayload, MobilePushSender } from './mobile-push-switch'\n\n/** The most devices one notification is sent to, whatever the audience. */\nconst MAX_DEVICES = 1000\n\n/** One device a transport sends to. */\nexport interface PushTarget {\n token: string\n app: MobileDeviceApp\n /** APNs: the app's bundle id, which is the push topic. */\n topic: string\n apnsEnvironment?: ApnsEnvironment\n}\n\n/** What every device of one notification is sent. */\nexport interface PushMessage {\n title: string\n body?: string\n data: MobilePushData\n /** Critical and warning notifications ask the platform to deliver now. */\n urgent: boolean\n}\n\n/** A transport's verdict for each target, in order. `prune`: the token is gone; forget the row. */\nexport type PushOutcome = 'sent' | 'prune' | 'failed'\n\nexport interface PushTransport {\n send(targets: readonly PushTarget[], message: PushMessage): Promise<PushOutcome[]>\n}\n\nexport interface PushDeliveryDeps {\n db: Firestore\n /** Each transport, or null when the deployment has none configured. Resolved only when a device needs it. */\n transport: (kind: MobilePushTransport) => Promise<PushTransport | null>\n now?: () => number\n}\n\nexport interface PushDeliveryResult {\n sent: number\n /** Rows removed: stale, Expo or malformed rows, and tokens a transport reported gone. */\n pruned: number\n failed: number\n /** Devices not sent to because their transport is not configured. */\n skipped: number\n}\n\n/** A stored timestamp as epoch millis, or null when there is none. */\nfunction millis(value: unknown): number | null {\n if (!value) return null\n if (typeof value === 'number') return value\n const withMillis = value as { toMillis?: () => number }\n return typeof withMillis.toMillis === 'function' ? withMillis.toMillis() : null\n}\n\nexport function pushMessage(payload: MobilePushPayload, link: string | undefined): PushMessage {\n return {\n title: payload.title,\n ...(payload.body ? { body: payload.body } : {}),\n data: mobilePushData({ ...payload, link }),\n urgent: payload.level === 'critical' || payload.level === 'warning',\n }\n}\n\n/** The link the app opens: the stored one, rewritten onto today's console routes. */\nasync function pushLink(db: Firestore, payload: MobilePushPayload): Promise<string | undefined> {\n if (!payload.link) return undefined\n if (!payload.orgId) return normalizeNotificationLink(payload.link, {})\n try {\n const [org, host] = await Promise.all([\n db.collection('orgs').doc(payload.orgId).get(),\n payload.hostId ? db.collection('hosts').doc(payload.hostId).get() : Promise.resolve(null),\n ])\n const slug = org.get('slug')\n const subdomain = host?.get('subdomain')\n return normalizeNotificationLink(payload.link, {\n orgSlug: typeof slug === 'string' ? slug : null,\n hostId: payload.hostId ?? null,\n hostSubdomain: typeof subdomain === 'string' ? subdomain : null,\n })\n } catch {\n return normalizeNotificationLink(payload.link, {})\n }\n}\n\nconst isApp = (value: unknown): value is MobileDeviceApp => value === 'aglyn' || value === 'aglyn-pos'\n\n/**\n * Sends one notification to every live device of `uids`, each through its\n * own transport. Never throws: the console notification is the record, and a\n * push is a courtesy beside it.\n */\nexport async function deliverNativePush(\n uids: readonly string[],\n payload: MobilePushPayload,\n deps: PushDeliveryDeps,\n): Promise<PushDeliveryResult> {\n const result: PushDeliveryResult = { sent: 0, pruned: 0, failed: 0, skipped: 0 }\n if (!uids.length) return result\n const now = (deps.now ?? Date.now)()\n const prune = async (ref: DocumentReference) => {\n await ref.delete().catch(() => undefined)\n result.pruned += 1\n }\n try {\n const { db } = deps\n const groups = new Map<MobilePushTransport, Array<{ target: PushTarget; ref: DocumentReference }>>()\n const seen = new Set<string>()\n let total = 0\n for (const uid of uids) {\n const devices = await db\n .collection('users')\n .doc(uid)\n .collection(MOBILE_DEVICES_COLLECTION)\n .limit(MOBILE_DEVICES_PER_USER)\n .get()\n for (const device of devices.docs) {\n const transport = device.get('transport')\n const token = device.get('token')\n const app = device.get('app')\n if (!isMobilePushToken(transport, token) || !isApp(app)) {\n await prune(device.ref)\n continue\n }\n const lastSeen = millis(device.get('lastSeen'))\n if (lastSeen !== null && now - lastSeen > MOBILE_DEVICE_STALE_MS) {\n await prune(device.ref)\n continue\n }\n if (seen.has(token) || total >= MAX_DEVICES) continue\n seen.add(token)\n total += 1\n const environment = device.get('apnsEnvironment')\n const target: PushTarget = {\n token,\n app,\n topic: MOBILE_APP_BUNDLES[app],\n ...(transport === 'apns' && (environment === 'sandbox' || environment === 'production')\n ? { apnsEnvironment: environment }\n : {}),\n }\n const group = groups.get(transport) ?? []\n group.push({ target, ref: device.ref })\n groups.set(transport, group)\n }\n }\n if (!groups.size) return result\n const message = pushMessage(payload, await pushLink(db, payload))\n for (const [kind, entries] of groups) {\n let transport: PushTransport | null = null\n try {\n transport = await deps.transport(kind)\n } catch (error) {\n console.error(`push transport ${kind} failed to load`, error)\n }\n if (!transport) {\n result.skipped += entries.length\n continue\n }\n let outcomes: PushOutcome[]\n try {\n outcomes = await transport.send(\n entries.map((entry) => entry.target),\n message,\n )\n } catch (error) {\n console.error(`push transport ${kind} failed`, error)\n result.failed += entries.length\n continue\n }\n for (const [index, entry] of entries.entries()) {\n const outcome = outcomes[index] ?? 'failed'\n if (outcome === 'sent') result.sent += 1\n else if (outcome === 'prune') await prune(entry.ref)\n else result.failed += 1\n }\n }\n } catch (error) {\n console.error('push delivery failed', error)\n }\n return result\n}\n\n/** The deployment's transports, each imported the first time a device needs it. */\nasync function deploymentTransport(kind: MobilePushTransport): Promise<PushTransport | null> {\n if (kind === 'apns') return (await import('./push-apns')).apnsTransport()\n return (await import('./push-fcm')).fcmTransport()\n}\n\n/** The sender `registerNativePushSenders` hands the fan-out. */\nexport const nativePushSender: MobilePushSender = async (uids, payload, { db }) => {\n await deliverNativePush(uids, payload, { db, transport: deploymentTransport })\n}\n"],"names":["isMobilePushToken","MOBILE_APP_BUNDLES","MOBILE_DEVICE_STALE_MS","MOBILE_DEVICES_COLLECTION","MOBILE_DEVICES_PER_USER","mobilePushData","normalizeNotificationLink","MAX_DEVICES","millis","value","withMillis","toMillis","pushMessage","payload","link","title","body","data","urgent","level","pushLink","db","undefined","orgId","org","host","Promise","all","collection","doc","get","hostId","resolve","slug","subdomain","orgSlug","hostSubdomain","isApp","deliverNativePush","uids","deps","result","sent","pruned","failed","skipped","length","now","Date","prune","ref","delete","catch","groups","Map","seen","Set","total","uid","devices","limit","device","docs","transport","token","app","lastSeen","has","add","environment","target","topic","apnsEnvironment","group","push","set","size","message","kind","entries","error","console","outcomes","send","map","entry","index","outcome","deploymentTransport","apnsTransport","fcmTransport","nativePushSender"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type BatchResponse, type TokenMessage } from 'firebase-admin/messaging';
18
+ import './firebase-admin';
19
+ import type { PushMessage, PushTransport } from './push-delivery';
20
+ /** `sendEach` takes at most 500 messages. */
21
+ export declare const FCM_BATCH = 500;
22
+ /** The slice of firebase-admin's Messaging this transport uses; injected in tests. */
23
+ export interface FcmMessaging {
24
+ sendEach(messages: TokenMessage[]): Promise<BatchResponse>;
25
+ }
26
+ export declare function fcmMessage(token: string, message: PushMessage): TokenMessage;
27
+ export declare function createFcmTransport(messaging: FcmMessaging): PushTransport;
28
+ /** The deployment's FCM transport, on the firebase-admin default app. */
29
+ export declare function fcmTransport(): PushTransport;
@@ -0,0 +1,83 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ /**
18
+ * Firebase Cloud Messaging for the Android app (AGL-3651), through
19
+ * firebase-admin's `getMessaging().sendEach` with the project credentials
20
+ * the server already holds, so it needs no secret of its own. The
21
+ * notification is shown by the system; the tap data rides in `data`, and
22
+ * Android delivers it at high priority.
23
+ *
24
+ * A token FCM reports unregistered or invalid is gone, and the row is pruned.
25
+ *
26
+ * Server-only, imported by `push-delivery.ts` only when a device is on FCM.
27
+ */ import { getApp } from "firebase-admin/app";
28
+ import { getMessaging } from "firebase-admin/messaging";
29
+ import "./firebase-admin.js";
30
+ /** `sendEach` takes at most 500 messages. */ export const FCM_BATCH = 500;
31
+ const GONE = new Set([
32
+ 'messaging/registration-token-not-registered',
33
+ 'messaging/invalid-registration-token',
34
+ 'messaging/invalid-argument'
35
+ ]);
36
+ export function fcmMessage(token, message) {
37
+ return {
38
+ token,
39
+ notification: _extends({
40
+ title: message.title
41
+ }, message.body ? {
42
+ body: message.body
43
+ } : {}),
44
+ // FCM data values are strings; MobilePushData's are.
45
+ data: _extends({}, message.data),
46
+ android: {
47
+ priority: 'high',
48
+ notification: {
49
+ sound: 'default'
50
+ }
51
+ }
52
+ };
53
+ }
54
+ export function createFcmTransport(messaging) {
55
+ return {
56
+ async send (targets, message) {
57
+ const outcomes = [];
58
+ for(let start = 0; start < targets.length; start += FCM_BATCH){
59
+ const batch = targets.slice(start, start + FCM_BATCH);
60
+ try {
61
+ const { responses } = await messaging.sendEach(batch.map((target)=>fcmMessage(target.token, message)));
62
+ for (const [index] of batch.entries()){
63
+ var _ref;
64
+ var _response_error;
65
+ const response = responses[index];
66
+ if (response == null ? void 0 : response.success) outcomes.push('sent');
67
+ else outcomes.push(GONE.has((_ref = response == null ? void 0 : (_response_error = response.error) == null ? void 0 : _response_error.code) != null ? _ref : '') ? 'prune' : 'failed');
68
+ }
69
+ } catch (unused) {
70
+ outcomes.push(...batch.map(()=>'failed'));
71
+ }
72
+ }
73
+ return outcomes;
74
+ }
75
+ };
76
+ }
77
+ let configured = null;
78
+ /** The deployment's FCM transport, on the firebase-admin default app. */ export function fcmTransport() {
79
+ configured != null ? configured : configured = createFcmTransport(getMessaging(getApp()));
80
+ return configured;
81
+ }
82
+
83
+ //# sourceMappingURL=push-fcm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/tenant/data/admin/src/lib/server/push-fcm.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Firebase Cloud Messaging for the Android app (AGL-3651), through\n * firebase-admin's `getMessaging().sendEach` with the project credentials\n * the server already holds, so it needs no secret of its own. The\n * notification is shown by the system; the tap data rides in `data`, and\n * Android delivers it at high priority.\n *\n * A token FCM reports unregistered or invalid is gone, and the row is pruned.\n *\n * Server-only, imported by `push-delivery.ts` only when a device is on FCM.\n */\n\nimport { getApp } from 'firebase-admin/app'\nimport { getMessaging, type BatchResponse, type TokenMessage } from 'firebase-admin/messaging'\nimport './firebase-admin'\nimport type { PushMessage, PushOutcome, PushTarget, PushTransport } from './push-delivery'\n\n/** `sendEach` takes at most 500 messages. */\nexport const FCM_BATCH = 500\n\nconst GONE = new Set([\n 'messaging/registration-token-not-registered',\n 'messaging/invalid-registration-token',\n 'messaging/invalid-argument',\n])\n\n/** The slice of firebase-admin's Messaging this transport uses; injected in tests. */\nexport interface FcmMessaging {\n sendEach(messages: TokenMessage[]): Promise<BatchResponse>\n}\n\nexport function fcmMessage(token: string, message: PushMessage): TokenMessage {\n return {\n token,\n notification: { title: message.title, ...(message.body ? { body: message.body } : {}) },\n // FCM data values are strings; MobilePushData's are.\n data: { ...message.data },\n android: { priority: 'high', notification: { sound: 'default' } },\n }\n}\n\nexport function createFcmTransport(messaging: FcmMessaging): PushTransport {\n return {\n async send(targets: readonly PushTarget[], message: PushMessage): Promise<PushOutcome[]> {\n const outcomes: PushOutcome[] = []\n for (let start = 0; start < targets.length; start += FCM_BATCH) {\n const batch = targets.slice(start, start + FCM_BATCH)\n try {\n const { responses } = await messaging.sendEach(batch.map((target) => fcmMessage(target.token, message)))\n for (const [index] of batch.entries()) {\n const response = responses[index]\n if (response?.success) outcomes.push('sent')\n else outcomes.push(GONE.has(response?.error?.code ?? '') ? 'prune' : 'failed')\n }\n } catch {\n outcomes.push(...batch.map((): PushOutcome => 'failed'))\n }\n }\n return outcomes\n },\n }\n}\n\nlet configured: PushTransport | null = null\n\n/** The deployment's FCM transport, on the firebase-admin default app. */\nexport function fcmTransport(): PushTransport {\n configured ??= createFcmTransport(getMessaging(getApp()))\n return configured\n}\n"],"names":["getApp","getMessaging","FCM_BATCH","GONE","Set","fcmMessage","token","message","notification","title","body","data","android","priority","sound","createFcmTransport","messaging","send","targets","outcomes","start","length","batch","slice","responses","sendEach","map","target","index","entries","response","success","push","has","error","code","configured","fcmTransport"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;CAUC,GAED,SAASA,MAAM,QAAQ,qBAAoB;AAC3C,SAASC,YAAY,QAA+C,2BAA0B;AAC9F,OAAO,sBAAkB;AAGzB,2CAA2C,GAC3C,OAAO,MAAMC,YAAY,IAAG;AAE5B,MAAMC,OAAO,IAAIC,IAAI;IACnB;IACA;IACA;CACD;AAOD,OAAO,SAASC,WAAWC,KAAa,EAAEC,OAAoB;IAC5D,OAAO;QACLD;QACAE,cAAc;YAAEC,OAAOF,QAAQE,KAAK;WAAMF,QAAQG,IAAI,GAAG;YAAEA,MAAMH,QAAQG,IAAI;QAAC,IAAI,CAAC;QACnF,qDAAqD;QACrDC,MAAM,aAAKJ,QAAQI,IAAI;QACvBC,SAAS;YAAEC,UAAU;YAAQL,cAAc;gBAAEM,OAAO;YAAU;QAAE;IAClE;AACF;AAEA,OAAO,SAASC,mBAAmBC,SAAuB;IACxD,OAAO;QACL,MAAMC,MAAKC,OAA8B,EAAEX,OAAoB;YAC7D,MAAMY,WAA0B,EAAE;YAClC,IAAK,IAAIC,QAAQ,GAAGA,QAAQF,QAAQG,MAAM,EAAED,SAASlB,UAAW;gBAC9D,MAAMoB,QAAQJ,QAAQK,KAAK,CAACH,OAAOA,QAAQlB;gBAC3C,IAAI;oBACF,MAAM,EAAEsB,SAAS,EAAE,GAAG,MAAMR,UAAUS,QAAQ,CAACH,MAAMI,GAAG,CAAC,CAACC,SAAWtB,WAAWsB,OAAOrB,KAAK,EAAEC;oBAC9F,KAAK,MAAM,CAACqB,MAAM,IAAIN,MAAMO,OAAO,GAAI;;4BAGTC;wBAF5B,MAAMA,WAAWN,SAAS,CAACI,MAAM;wBACjC,IAAIE,4BAAAA,SAAUC,OAAO,EAAEZ,SAASa,IAAI,CAAC;6BAChCb,SAASa,IAAI,CAAC7B,KAAK8B,GAAG,SAACH,6BAAAA,kBAAAA,SAAUI,KAAK,qBAAfJ,gBAAiBK,IAAI,mBAAI,MAAM,UAAU;oBACvE;gBACF,EAAE,eAAM;oBACNhB,SAASa,IAAI,IAAIV,MAAMI,GAAG,CAAC,IAAmB;gBAChD;YACF;YACA,OAAOP;QACT;IACF;AACF;AAEA,IAAIiB,aAAmC;AAEvC,uEAAuE,GACvE,OAAO,SAASC;IACdD,qBAAAA,aAAAA,aAAerB,mBAAmBd,aAAaD;IAC/C,OAAOoC;AACT"}
@@ -180,6 +180,12 @@ export declare function mediaCdnVersionIsCurrent(options: {
180
180
  /** The document's {@link MEDIA_VARIANT_ENCODER_VERSION_FIELD}. */
181
181
  documentEncoderVersion: unknown;
182
182
  }): boolean;
183
+ /**
184
+ * Whether an asset of this type is only ever served as its own bytes — no
185
+ * `?w=` variant, display copy, poster or rendition (AGL-3656). A web font is
186
+ * the case: a versioned URL that names its hash names everything served.
187
+ */
188
+ export declare function mediaCdnHasNoDerivedForms(contentType: unknown): boolean;
183
189
  /** The immutable content-hashed URL's policy for edge-cacheable (image) types. */
184
190
  export declare const MEDIA_CDN_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
185
191
  /**
@@ -221,6 +221,13 @@ const SEGMENT = MEDIA_CDN_SEGMENT;
221
221
  const generation = typeof recorded === 'number' && Number.isInteger(recorded) && recorded > 0 ? recorded : 1;
222
222
  return token.encoderVersion === MEDIA_VARIANT_ENCODER_VERSION && generation === token.encoderVersion;
223
223
  }
224
+ /**
225
+ * Whether an asset of this type is only ever served as its own bytes — no
226
+ * `?w=` variant, display copy, poster or rendition (AGL-3656). A web font is
227
+ * the case: a versioned URL that names its hash names everything served.
228
+ */ export function mediaCdnHasNoDerivedForms(contentType) {
229
+ return String(contentType != null ? contentType : '').split(';')[0].trim().toLowerCase() === 'font/woff2';
230
+ }
224
231
  /** The immutable content-hashed URL's policy for edge-cacheable (image) types. */ export const MEDIA_CDN_IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable';
225
232
  /**
226
233
  * The immutable URL's policy for edge-bypassing types (AGL-1515): the
@@ -246,7 +253,12 @@ const SEGMENT = MEDIA_CDN_SEGMENT;
246
253
  *
247
254
  * Unknown or absent types return false: correctness over cache.
248
255
  */ export function mediaCdnEdgeCacheable(contentType) {
249
- return String(contentType != null ? contentType : '').split(';')[0].trim().toLowerCase().startsWith('image/');
256
+ const type = String(contentType != null ? contentType : '').split(';')[0].trim().toLowerCase();
257
+ // A web font (AGL-3656) is fetched the way an image is — one plain GET
258
+ // from an `@font-face` rule, never a range — and it is part of the page
259
+ // weight the band already measures, so it takes the image policy: the
260
+ // edge holds it, and a versioned URL a page names is kept for a year.
261
+ return type.startsWith('image/') || type === 'font/woff2';
250
262
  }
251
263
  /**
252
264
  * The CDN's OWN Content-Security-Policy (AGL-1474).
@@ -337,7 +349,9 @@ const SEGMENT = MEDIA_CDN_SEGMENT;
337
349
  * decision back without adding to it.
338
350
  */ function servesInline(contentType) {
339
351
  const type = String(contentType != null ? contentType : '').split(';')[0].trim().toLowerCase();
340
- return type.startsWith('image/') || type.startsWith('video/') || type.startsWith('audio/') || type === 'application/pdf';
352
+ return type.startsWith('image/') || type.startsWith('video/') || type.startsWith('audio/') || type === 'application/pdf' || // A web font (AGL-3656): an `@font-face` rule loads it, and a font is
353
+ // inert outside a stylesheet, so a top-level open has nothing to run.
354
+ type === 'font/woff2';
341
355
  }
342
356
  /** The `Content-Security-Policy` for a response serving `contentType`. */ export function mediaCdnContentSecurityPolicy(contentType) {
343
357
  const type = String(contentType != null ? contentType : '').split(';')[0].trim().toLowerCase();
@@ -1311,7 +1325,10 @@ export function parseMediaCdnRange(header, size) {
1311
1325
  widthRequested: Boolean(width),
1312
1326
  variantServed: exactVariant,
1313
1327
  otherRepresentation: usePoster || Boolean(rendition),
1314
- documentEncoderVersion: snapshot.get(MEDIA_VARIANT_ENCODER_VERSION_FIELD)
1328
+ // A web font has no derived form for an encoder to have made, so no
1329
+ // generation can be behind (AGL-3656): only its bytes are named, and
1330
+ // a library on a plan without generated variants records none.
1331
+ documentEncoderVersion: mediaCdnHasNoDerivedForms(snapshot.get('contentType')) ? MEDIA_VARIANT_ENCODER_VERSION : snapshot.get(MEDIA_VARIANT_ENCODER_VERSION_FIELD)
1315
1332
  });
1316
1333
  const stableCacheControlFor = (type)=>mediaCdnEdgeCacheable(type) ? versionCurrent ? MEDIA_CDN_VERSIONED_CACHE_CONTROL : MEDIA_CDN_STABLE_CACHE_CONTROL : MEDIA_CDN_STABLE_EDGE_BYPASS_CACHE_CONTROL;
1317
1334
  // A poster is `image/webp` and therefore edge-cacheable through the rule