@maravilla-labs/platform 0.1.41 → 0.1.44

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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { LocalParticipant } from 'livekit-client';
2
- export { RegisterPushOptions, RegisterPushResult, registerPush, unregisterPush } from './push.js';
2
+ export { RegisterPushOptions, RegisterPushResult, offsetBefore, registerPush, unregisterPush } from './push.js';
3
3
 
4
4
  /**
5
5
  * Media service for video/audio room management.
@@ -947,6 +947,213 @@ interface PolicyService {
947
947
  /** `true` when Layer 2 is active for this request. */
948
948
  isEnabled(): boolean;
949
949
  }
950
+ /**
951
+ * Target selector for Web Push sends. Combine fields to narrow — all
952
+ * specified conditions must match for a subscription to receive the push.
953
+ *
954
+ * @example
955
+ * ```typescript
956
+ * // Every subscription tagged with "waitlist"
957
+ * await platform.push.send({ topic: 'waitlist' }, notification);
958
+ *
959
+ * // Every device belonging to one authenticated user
960
+ * await platform.push.send({ userId: 'u_42' }, notification);
961
+ *
962
+ * // "This specific user's subscription for this specific invite"
963
+ * await platform.push.send({ userId: 'u_42', topic: 'invite:abc:rsvp' }, notification);
964
+ * ```
965
+ */
966
+ interface PushTarget {
967
+ userId?: string;
968
+ visitorId?: string;
969
+ topic?: string;
970
+ userIds?: string[];
971
+ topics?: string[];
972
+ onlyActive?: boolean;
973
+ }
974
+ /** Shape of a Web Push notification payload. */
975
+ interface NotificationPayload {
976
+ /** Required — the notification headline shown on lock screens and the notification shade. */
977
+ title: string;
978
+ body?: string;
979
+ icon?: string;
980
+ badge?: string;
981
+ image?: string;
982
+ /** Browsers dedupe notifications sharing a tag. */
983
+ tag?: string;
984
+ /** Where to navigate when the notification is clicked. */
985
+ url?: string;
986
+ /** Arbitrary JSON delivered to the service worker alongside the notification. */
987
+ data?: Record<string, unknown>;
988
+ /** Seconds the push service holds the message if the device is offline. */
989
+ ttl?: number;
990
+ urgency?: 'very-low' | 'low' | 'normal' | 'high';
991
+ }
992
+ /**
993
+ * Options for `platform.push.schedule(...)`.
994
+ *
995
+ * @example
996
+ * ```typescript
997
+ * // Remind every RSVP'd guest one hour before the event.
998
+ * await platform.push.schedule(
999
+ * { topic: `invite:${invite.id}` },
1000
+ * { title: invite.title, body: 'Your event is in one hour' },
1001
+ * {
1002
+ * at: offsetBefore(invite.event_date, '1h'),
1003
+ * key: `invite:${invite.id}:reminder-1h`,
1004
+ * }
1005
+ * );
1006
+ * ```
1007
+ */
1008
+ interface ScheduleOptions {
1009
+ /**
1010
+ * When to send. Absolute `Date` or ISO-8601 string; the server treats
1011
+ * bare (no-offset) strings as UTC.
1012
+ */
1013
+ at: Date | string;
1014
+ /**
1015
+ * Idempotency key scoped to your project. Re-calling `schedule` with the
1016
+ * same key atomically replaces the prior pending job — safe to call on
1017
+ * every save of an invite whose event date may change.
1018
+ */
1019
+ key: string;
1020
+ /** Maximum delivery attempts before the job is marked failed. Defaults to 3. */
1021
+ maxAttempts?: number;
1022
+ /**
1023
+ * If set, the job re-queues after every successful send and fires again
1024
+ * this many seconds later. Use for daily digests (`86400`), weekly
1025
+ * updates (`604800`), or any fixed-interval loop. `cancelScheduled(key)`
1026
+ * stops the loop.
1027
+ */
1028
+ everySeconds?: number;
1029
+ }
1030
+ /** A single scheduled push job as returned by `listScheduled` / `getScheduled`. */
1031
+ interface ScheduledJob {
1032
+ jobId: string;
1033
+ key?: string;
1034
+ /** Next fire time — unix seconds. Convert with `new Date(scheduledFor * 1000)`. */
1035
+ scheduledFor: number;
1036
+ status: 'pending' | 'running' | 'succeeded' | 'failed';
1037
+ attempts: number;
1038
+ maxAttempts: number;
1039
+ lastError?: string;
1040
+ createdAt: number;
1041
+ updatedAt: number;
1042
+ /** Populated once the job has fired at least once. */
1043
+ sentAt?: number;
1044
+ /** Set when the job recurs — `schedule()` was called with `everySeconds`. */
1045
+ recurringIntervalSecs?: number;
1046
+ }
1047
+ /** Filter passed to `listScheduled`. */
1048
+ interface ListScheduledFilter {
1049
+ status?: 'pending' | 'running' | 'succeeded' | 'failed';
1050
+ limit?: number;
1051
+ offset?: number;
1052
+ }
1053
+ /** Counts by status, as returned by `queueStats`. */
1054
+ interface QueueStats {
1055
+ pending: number;
1056
+ running: number;
1057
+ succeeded: number;
1058
+ failed: number;
1059
+ }
1060
+ /** Outcome of a single `platform.push.send(...)` fan-out. */
1061
+ interface SendReport {
1062
+ attempted: number;
1063
+ succeeded: number;
1064
+ gone: number;
1065
+ failed: number;
1066
+ errors?: Array<{
1067
+ subscriptionId: string;
1068
+ message: string;
1069
+ }>;
1070
+ }
1071
+ /** Shape of a stored Web Push subscription. */
1072
+ interface StoredPushSubscription {
1073
+ id: string;
1074
+ provider: 'web-push' | 'apns' | 'fcm';
1075
+ endpoint: string;
1076
+ p256dh?: string | null;
1077
+ auth?: string | null;
1078
+ userId?: string | null;
1079
+ visitorId?: string | null;
1080
+ userAgent?: string | null;
1081
+ topics: string[];
1082
+ createdAt: number;
1083
+ lastSeenAt?: number | null;
1084
+ expiresAt?: number | null;
1085
+ isActive: boolean;
1086
+ }
1087
+ /** Aggregate counts across every subscription in the project. */
1088
+ interface SubscriptionCounts {
1089
+ total: number;
1090
+ byTopic: Array<[string, number]>;
1091
+ byProvider: Array<[string, number]>;
1092
+ }
1093
+ /** Public VAPID config for the current project. */
1094
+ interface PublicPushConfig {
1095
+ vapidPublic: string;
1096
+ contactEmail: string;
1097
+ updatedAt: number;
1098
+ }
1099
+ /**
1100
+ * Server-side Web Push service. Access via `platform.push` inside your
1101
+ * runtime code.
1102
+ */
1103
+ interface PushService {
1104
+ /**
1105
+ * Fan out a notification to every active subscription matching `target`.
1106
+ * Blocks until every device has been tried.
1107
+ */
1108
+ send(target: PushTarget, notification: NotificationPayload): Promise<SendReport>;
1109
+ /**
1110
+ * Fire-and-forget variant. The request handler can return immediately;
1111
+ * the dispatch continues in the background. Best-effort — a delivery
1112
+ * restart mid-dispatch loses in-flight sends.
1113
+ */
1114
+ sendBackground(target: PushTarget, notification: NotificationPayload): Promise<void>;
1115
+ /**
1116
+ * Queue a notification for a future time. Idempotent by `key` — repeated
1117
+ * calls with the same key replace the prior pending job. Set `everySeconds`
1118
+ * for recurring digests.
1119
+ */
1120
+ schedule(target: PushTarget, notification: NotificationPayload, opts: ScheduleOptions): Promise<{
1121
+ jobId: string;
1122
+ }>;
1123
+ /** Cancel the pending scheduled job with this idempotency key. */
1124
+ cancelScheduled(key: string): Promise<{
1125
+ canceled: number;
1126
+ }>;
1127
+ /** List scheduled jobs for this project, optionally filtered by status. */
1128
+ listScheduled(filter?: ListScheduledFilter): Promise<ScheduledJob[]>;
1129
+ /** Look up a single scheduled job by idempotency key. */
1130
+ getScheduled(key: string): Promise<ScheduledJob | null>;
1131
+ /** Per-status counts for the scheduled queue. */
1132
+ queueStats(): Promise<QueueStats>;
1133
+ /** List subscriptions — useful for admin UIs and debugging. */
1134
+ list(filter?: {
1135
+ topic?: string;
1136
+ userId?: string;
1137
+ visitorId?: string;
1138
+ onlyActive?: boolean;
1139
+ limit?: number;
1140
+ offset?: number;
1141
+ }): Promise<StoredPushSubscription[]>;
1142
+ /** Aggregate counts grouped by topic and provider. */
1143
+ counts(): Promise<SubscriptionCounts>;
1144
+ /** Remove a subscription by id. */
1145
+ unsubscribe(subscriptionId: string): Promise<void>;
1146
+ /** Remove a subscription by its push service endpoint URL. */
1147
+ unsubscribeByEndpoint(endpoint: string): Promise<void>;
1148
+ /** Fetch the project's current VAPID public key + contact email. */
1149
+ getVapidConfig(): Promise<PublicPushConfig>;
1150
+ /**
1151
+ * Rotate the project's VAPID keypair. **Every existing subscription
1152
+ * silently stops working** — browsers bind subscriptions to the key they
1153
+ * saw at subscribe time. Confirm with your users before calling.
1154
+ */
1155
+ rotateVapidKeys(): Promise<PublicPushConfig>;
1156
+ }
950
1157
  interface Platform {
951
1158
  /** Environment containing all available platform services */
952
1159
  env: PlatformEnv;
@@ -958,6 +1165,13 @@ interface Platform {
958
1165
  auth: AuthService;
959
1166
  /** Per-request Layer 2 policy toggle (Layer 1 isolation always applies) */
960
1167
  policy: PolicyService;
1168
+ /**
1169
+ * Web Push — send, schedule, or query browser push notifications for
1170
+ * logged-in users and anonymous visitors. Available when Push is enabled
1171
+ * in project settings; `undefined` when running against the dev-server
1172
+ * fallback that doesn't proxy to delivery.
1173
+ */
1174
+ push?: PushService;
961
1175
  }
962
1176
 
963
1177
  interface RenEvent {
@@ -1320,4 +1534,4 @@ declare function getPlatform(options?: {
1320
1534
  */
1321
1535
  declare function clearPlatformCache(): void;
1322
1536
 
1323
- export { type AuthCaller, type AuthField, type AuthService, type AuthSession, type AuthUser, type Database, type DbFindOptions, type KvListResult, type KvNamespace, type LoginOptions, MediaLocalParticipant, type MediaParticipant, type MediaParticipantInfo, MediaRoom, MediaRoomEvent, type MediaRoomInfo, type MediaRoomInfoSettings, type MediaRoomOptions, type MediaService, type MediaTokenResult, type MediaTrackPublication, type Platform, type PlatformEnv, type PolicyService, type PresenceMember, type PresenceService, RealtimeClient, type RealtimeClientOptions, type RealtimeEvent, type RealtimeService, type RegisterOptions, RemoteMediaService, RenClient, type RenClientOptions, type RenEvent, type Storage$1 as Storage, type StoragePutStreamSource, type TrackKind, type TrackSource, type UpdateUserOptions, type UserListFilter, type UserListResponse, type VideoResolution, attachTrack, clearPlatformCache, detachTrack, getOrCreateClientId, getPlatform, renFetch, storageDelete, storageUpload };
1537
+ export { type AuthCaller, type AuthField, type AuthService, type AuthSession, type AuthUser, type Database, type DbFindOptions, type KvListResult, type KvNamespace, type ListScheduledFilter, type LoginOptions, MediaLocalParticipant, type MediaParticipant, type MediaParticipantInfo, MediaRoom, MediaRoomEvent, type MediaRoomInfo, type MediaRoomInfoSettings, type MediaRoomOptions, type MediaService, type MediaTokenResult, type MediaTrackPublication, type NotificationPayload, type Platform, type PlatformEnv, type PolicyService, type PresenceMember, type PresenceService, type PublicPushConfig, type PushService, type PushTarget, type QueueStats, RealtimeClient, type RealtimeClientOptions, type RealtimeEvent, type RealtimeService, type RegisterOptions, RemoteMediaService, RenClient, type RenClientOptions, type RenEvent, type ScheduleOptions, type ScheduledJob, type SendReport, type Storage$1 as Storage, type StoragePutStreamSource, type StoredPushSubscription, type SubscriptionCounts, type TrackKind, type TrackSource, type UpdateUserOptions, type UserListFilter, type UserListResponse, type VideoResolution, attachTrack, clearPlatformCache, detachTrack, getOrCreateClientId, getPlatform, renFetch, storageDelete, storageUpload };
package/dist/index.js CHANGED
@@ -1208,9 +1208,10 @@ var MediaRoom = class _MediaRoom {
1208
1208
  };
1209
1209
 
1210
1210
  // src/push.ts
1211
- var DEFAULT_BASE_PATH = "/_platform/push";
1212
- var DEFAULT_SW_PATH = "/_platform/push/sw.js";
1211
+ var DEFAULT_BASE_PATH = "/_rt/push";
1212
+ var DEFAULT_SW_PATH = "/_rt/push/sw.js";
1213
1213
  var VISITOR_STORAGE_KEY = "maravilla.push.visitorId";
1214
+ var REGISTER_TIMEOUT_MS = 1e4;
1214
1215
  function assertPushSupported() {
1215
1216
  if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) {
1216
1217
  throw new Error("Web Push is not supported: serviceWorker is unavailable");
@@ -1296,37 +1297,45 @@ async function registerPush(opts = {}) {
1296
1297
  const topics = opts.topics ?? [];
1297
1298
  const userId = opts.userId ?? null;
1298
1299
  const visitorId = resolveVisitorId(userId, opts.visitorId);
1299
- const publicKey = await fetchVapidPublicKey(basePath);
1300
- const registration = await navigator.serviceWorker.register(swPath);
1301
- await navigator.serviceWorker.ready;
1302
- const existing = await registration.pushManager.getSubscription();
1303
- const subscription = existing ?? await registration.pushManager.subscribe({
1304
- userVisibleOnly: true,
1305
- applicationServerKey: base64UrlToArrayBuffer(publicKey)
1306
- });
1307
- const { p256dh, auth } = extractKeys(subscription);
1308
- const res = await fetch(`${basePath}/subscribe`, {
1309
- method: "POST",
1310
- credentials: "same-origin",
1311
- headers: { "Content-Type": "application/json", Accept: "application/json" },
1312
- body: JSON.stringify({
1313
- provider: "web-push",
1314
- endpoint: subscription.endpoint,
1315
- p256dh,
1316
- auth,
1317
- userId,
1318
- visitorId,
1319
- topics
1320
- })
1321
- });
1322
- if (!res.ok) {
1323
- throw new Error(`Subscribe failed: ${res.status} ${res.statusText}`);
1324
- }
1325
- const saved = await res.json();
1326
- if (!saved || typeof saved.id !== "string" || saved.id.length === 0) {
1327
- throw new Error("Subscribe response is missing `id`");
1328
- }
1329
- return { subscription, subscriptionId: saved.id };
1300
+ const timeout = new Promise(
1301
+ (_, reject) => setTimeout(
1302
+ () => reject(new Error(`registerPush timed out after ${REGISTER_TIMEOUT_MS}ms`)),
1303
+ REGISTER_TIMEOUT_MS
1304
+ )
1305
+ );
1306
+ const flow = (async () => {
1307
+ const publicKey = await fetchVapidPublicKey(basePath);
1308
+ const registration = await navigator.serviceWorker.register(swPath);
1309
+ const existing = await registration.pushManager.getSubscription();
1310
+ const subscription = existing ?? await registration.pushManager.subscribe({
1311
+ userVisibleOnly: true,
1312
+ applicationServerKey: base64UrlToArrayBuffer(publicKey)
1313
+ });
1314
+ const { p256dh, auth } = extractKeys(subscription);
1315
+ const res = await fetch(`${basePath}/subscribe`, {
1316
+ method: "POST",
1317
+ credentials: "same-origin",
1318
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
1319
+ body: JSON.stringify({
1320
+ provider: "web-push",
1321
+ endpoint: subscription.endpoint,
1322
+ p256dh,
1323
+ auth,
1324
+ userId,
1325
+ visitorId,
1326
+ topics
1327
+ })
1328
+ });
1329
+ if (!res.ok) {
1330
+ throw new Error(`Subscribe failed: ${res.status} ${res.statusText}`);
1331
+ }
1332
+ const saved = await res.json();
1333
+ if (!saved || typeof saved.id !== "string" || saved.id.length === 0) {
1334
+ throw new Error("Subscribe response is missing `id`");
1335
+ }
1336
+ return { subscription, subscriptionId: saved.id };
1337
+ })();
1338
+ return Promise.race([flow, timeout]);
1330
1339
  }
1331
1340
  async function unregisterPush(subscriptionId, opts = {}) {
1332
1341
  if (!subscriptionId) {
@@ -1343,6 +1352,28 @@ async function unregisterPush(subscriptionId, opts = {}) {
1343
1352
  throw new Error(`Unsubscribe failed: ${res.status} ${res.statusText}`);
1344
1353
  }
1345
1354
  }
1355
+ function offsetBefore(anchor, offset) {
1356
+ const anchorDate = anchor instanceof Date ? anchor : new Date(anchor);
1357
+ if (Number.isNaN(anchorDate.getTime())) {
1358
+ throw new Error(`offsetBefore: invalid anchor "${String(anchor)}"`);
1359
+ }
1360
+ const match = /^(\d+)\s*(s|m|h|d|w)$/i.exec(offset.trim());
1361
+ if (!match) {
1362
+ throw new Error(
1363
+ `offsetBefore: invalid offset "${offset}" \u2014 expected something like "30m", "1h", "2d", "1w"`
1364
+ );
1365
+ }
1366
+ const amount = Number(match[1]);
1367
+ const unit = match[2].toLowerCase();
1368
+ const UNIT_MS = {
1369
+ s: 1e3,
1370
+ m: 6e4,
1371
+ h: 36e5,
1372
+ d: 864e5,
1373
+ w: 6048e5
1374
+ };
1375
+ return new Date(anchorDate.getTime() - amount * UNIT_MS[unit]);
1376
+ }
1346
1377
 
1347
1378
  // src/index.ts
1348
1379
  var cachedPlatform = void 0;
@@ -1389,6 +1420,7 @@ export {
1389
1420
  detachTrack,
1390
1421
  getOrCreateClientId,
1391
1422
  getPlatform,
1423
+ offsetBefore,
1392
1424
  registerPush,
1393
1425
  renFetch,
1394
1426
  storageDelete,