@proappstore/sdk 1.7.0 → 1.9.0

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.
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Web Push notifications — subscribe users, send targeted or broadcast pushes.
3
+ * Backed by the PAS API + VAPID + W3C Push API.
4
+ */
5
+ export class Notifications {
6
+ appId;
7
+ apiBase;
8
+ auth;
9
+ vapidKeyCache = null;
10
+ constructor(appId, apiBase, auth) {
11
+ this.appId = appId;
12
+ this.apiBase = apiBase;
13
+ this.auth = auth;
14
+ }
15
+ /** Fetch the server's VAPID public key (cached after first call). */
16
+ async getVapidKey() {
17
+ if (this.vapidKeyCache)
18
+ return this.vapidKeyCache;
19
+ const res = await fetch(`${this.apiBase}/v1/notifications/vapid-key`);
20
+ if (!res.ok)
21
+ throw new Error(`Failed to fetch VAPID key: ${res.status}`);
22
+ const data = (await res.json());
23
+ this.vapidKeyCache = data.publicKey;
24
+ return data.publicKey;
25
+ }
26
+ /** Request permission, register the service worker, and subscribe to push. */
27
+ async subscribe(swPath = '/sw.js') {
28
+ const token = this.auth.token;
29
+ if (!token)
30
+ throw new Error('Not signed in.');
31
+ const permission = await Notification.requestPermission();
32
+ if (permission !== 'granted')
33
+ throw new Error('Notification permission denied.');
34
+ const vapidKey = await this.getVapidKey();
35
+ await navigator.serviceWorker.register(swPath);
36
+ const registration = await navigator.serviceWorker.ready;
37
+ const subscription = await registration.pushManager.subscribe({
38
+ userVisibleOnly: true,
39
+ applicationServerKey: urlBase64ToUint8Array(vapidKey),
40
+ });
41
+ const keys = subscription.toJSON().keys;
42
+ const res = await fetch(`${this.apiBase}/v1/notifications/subscribe`, {
43
+ method: 'POST',
44
+ headers: {
45
+ Authorization: `Bearer ${token}`,
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ body: JSON.stringify({
49
+ appId: this.appId,
50
+ endpoint: subscription.endpoint,
51
+ p256dh: keys.p256dh,
52
+ auth: keys.auth,
53
+ }),
54
+ });
55
+ if (!res.ok) {
56
+ // Roll back browser subscription so isSubscribed() stays consistent
57
+ await subscription.unsubscribe();
58
+ if (res.status === 401) {
59
+ this.auth.handleUnauthorized();
60
+ throw new Error('Not signed in.');
61
+ }
62
+ throw new Error(`subscribe failed: ${res.status}`);
63
+ }
64
+ }
65
+ /** Unsubscribe from push notifications (browser + backend). */
66
+ async unsubscribe() {
67
+ const token = this.auth.token;
68
+ if (!token)
69
+ throw new Error('Not signed in.');
70
+ const registration = await navigator.serviceWorker.getRegistration();
71
+ const subscription = await registration?.pushManager.getSubscription();
72
+ if (subscription) {
73
+ const endpoint = subscription.endpoint;
74
+ // Backend first — if it fails, browser sub stays active so user can retry
75
+ await fetch(`${this.apiBase}/v1/notifications/unsubscribe`, {
76
+ method: 'POST',
77
+ headers: {
78
+ Authorization: `Bearer ${token}`,
79
+ 'Content-Type': 'application/json',
80
+ },
81
+ body: JSON.stringify({ endpoint }),
82
+ });
83
+ await subscription.unsubscribe();
84
+ }
85
+ }
86
+ /** Check if the user is currently subscribed to push. */
87
+ async isSubscribed() {
88
+ const registration = await navigator.serviceWorker.getRegistration();
89
+ if (!registration)
90
+ return false;
91
+ const subscription = await registration.pushManager.getSubscription();
92
+ return subscription !== null;
93
+ }
94
+ /** Return the current Notification permission state. */
95
+ getPermission() {
96
+ return typeof Notification !== 'undefined' ? Notification.permission : 'denied';
97
+ }
98
+ /** Send a push notification to a specific user (must be app creator). */
99
+ async send(userId, payload) {
100
+ return this._send({ ...payload, userId });
101
+ }
102
+ /** Broadcast a push notification to all subscribers (must be app creator). */
103
+ async broadcast(payload) {
104
+ return this._send(payload);
105
+ }
106
+ async _send(payload) {
107
+ const token = this.auth.token;
108
+ if (!token)
109
+ throw new Error('Not signed in.');
110
+ const res = await fetch(`${this.apiBase}/v1/notifications/send`, {
111
+ method: 'POST',
112
+ headers: {
113
+ Authorization: `Bearer ${token}`,
114
+ 'Content-Type': 'application/json',
115
+ },
116
+ body: JSON.stringify({
117
+ appId: this.appId,
118
+ userId: payload.userId,
119
+ title: payload.title,
120
+ body: payload.body,
121
+ url: payload.url,
122
+ icon: payload.icon,
123
+ tag: payload.tag,
124
+ }),
125
+ });
126
+ if (res.status === 401) {
127
+ this.auth.handleUnauthorized();
128
+ throw new Error('Not signed in.');
129
+ }
130
+ if (res.status === 403)
131
+ throw new Error('Only the app creator can send notifications.');
132
+ if (!res.ok)
133
+ throw new Error(`send failed: ${res.status}`);
134
+ return (await res.json());
135
+ }
136
+ /**
137
+ * Returns the service worker push event handler script as a string.
138
+ * Apps should save this as their sw.js (or append to an existing one).
139
+ */
140
+ static getServiceWorkerScript() {
141
+ return `
142
+ self.addEventListener('push', (event) => {
143
+ const data = event.data ? event.data.json() : {};
144
+ event.waitUntil(
145
+ self.registration.showNotification(data.title || 'Notification', {
146
+ body: data.body || '',
147
+ icon: data.icon || '/icon-192.png',
148
+ tag: data.tag || undefined,
149
+ data: { url: data.url },
150
+ })
151
+ );
152
+ });
153
+
154
+ self.addEventListener('notificationclick', (event) => {
155
+ event.notification.close();
156
+ const url = event.notification.data?.url;
157
+ if (url) {
158
+ event.waitUntil(clients.openWindow(url));
159
+ }
160
+ });
161
+ `.trim();
162
+ }
163
+ }
164
+ /** Convert a URL-safe base64 VAPID key to Uint8Array for PushManager. */
165
+ function urlBase64ToUint8Array(base64String) {
166
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
167
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
168
+ const raw = atob(base64);
169
+ const arr = new Uint8Array(raw.length);
170
+ for (let i = 0; i < raw.length; i++)
171
+ arr[i] = raw.charCodeAt(i);
172
+ return arr;
173
+ }
174
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AACH,MAAM,OAAO,aAAa;IAIL;IACA;IACA;IALX,aAAa,GAAkB,IAAI,CAAC;IAE5C,YACmB,KAAa,EACb,OAAe,EACf,IAAc;QAFd,UAAK,GAAL,KAAK,CAAQ;QACb,YAAO,GAAP,OAAO,CAAQ;QACf,SAAI,GAAJ,IAAI,CAAU;IAC9B,CAAC;IAEJ,qEAAqE;IACrE,KAAK,CAAC,WAAW;QACf,IAAI,IAAI,CAAC,aAAa;YAAE,OAAO,IAAI,CAAC,aAAa,CAAC;QAClD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,6BAA6B,CAAC,CAAC;QACtE,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACzE,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA0B,CAAC;QACzD,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC;QACpC,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,QAAQ;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAE9C,MAAM,UAAU,GAAG,MAAM,YAAY,CAAC,iBAAiB,EAAE,CAAC;QAC1D,IAAI,UAAU,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QAEjF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,SAAS,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC/C,MAAM,YAAY,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC;QAEzD,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,SAAS,CAAC;YAC5D,eAAe,EAAE,IAAI;YACrB,oBAAoB,EAAE,qBAAqB,CAAC,QAAQ,CAAiB;SACtE,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,IAAK,CAAC;QACzC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,6BAA6B,EAAE;YACpE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,KAAK,EAAE;gBAChC,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,YAAY,CAAC,QAAQ;gBAC/B,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;aAChB,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,oEAAoE;YACpE,MAAM,YAAY,CAAC,WAAW,EAAE,CAAC;YACjC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBACvB,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;gBAC/B,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;YACpC,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,qBAAqB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,WAAW;QACf,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAE9C,MAAM,YAAY,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,eAAe,EAAE,CAAC;QACrE,MAAM,YAAY,GAAG,MAAM,YAAY,EAAE,WAAW,CAAC,eAAe,EAAE,CAAC;QAEvE,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,QAAQ,GAAG,YAAY,CAAC,QAAQ,CAAC;YAEvC,0EAA0E;YAC1E,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,+BAA+B,EAAE;gBAC1D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,aAAa,EAAE,UAAU,KAAK,EAAE;oBAChC,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC;aACnC,CAAC,CAAC;YAEH,MAAM,YAAY,CAAC,WAAW,EAAE,CAAC;QACnC,CAAC;IACH,CAAC;IAED,yDAAyD;IACzD,KAAK,CAAC,YAAY;QAChB,MAAM,YAAY,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,eAAe,EAAE,CAAC;QACrE,IAAI,CAAC,YAAY;YAAE,OAAO,KAAK,CAAC;QAChC,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC;QACtE,OAAO,YAAY,KAAK,IAAI,CAAC;IAC/B,CAAC;IAED,wDAAwD;IACxD,aAAa;QACX,OAAO,OAAO,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClF,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,OAA4B;QACrD,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAAC,SAAS,CAAC,OAA4B;QAC1C,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,OAAkD;QACpE,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAE9C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,wBAAwB,EAAE;YAC/D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,KAAK,EAAE;gBAChC,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;gBACnB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,EAAE,OAAO,CAAC,GAAG;aACjB,CAAC;SACH,CAAC,CAAC;QAEH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACpC,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QACxF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAE3D,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAe,CAAC;IAC1C,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,sBAAsB;QAC3B,OAAO;;;;;;;;;;;;;;;;;;;;CAoBV,CAAC,IAAI,EAAE,CAAC;IACP,CAAC;CACF;AAED,yEAAyE;AACzE,SAAS,qBAAqB,CAAC,YAAoB;IACjD,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,CAAC,YAAY,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC9E,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAChE,OAAO,GAAG,CAAC;AACb,CAAC"}
package/dist/sms.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ interface AuthLike {
2
+ token: string | null;
3
+ handleUnauthorized(): void;
4
+ }
5
+ export interface SmsSendResult {
6
+ sent: number;
7
+ failed: number;
8
+ }
9
+ /**
10
+ * SMS — send text messages via the PAS API (Twilio-backed server-side).
11
+ *
12
+ * The platform owns the Twilio credentials; the app never sees them.
13
+ * Only the app creator can send. Rate-limiting / abuse protection is
14
+ * server-side. Numbers must be E.164 ("+15551234567").
15
+ *
16
+ * Use case: class reminders, OTP codes, no-show notifications. Pair with
17
+ * `app.notifications` (Web Push) for in-browser delivery on the same event.
18
+ */
19
+ export declare class SMS {
20
+ private readonly appId;
21
+ private readonly apiBase;
22
+ private readonly auth;
23
+ constructor(appId: string, apiBase: string, auth: AuthLike);
24
+ /** Send an SMS to a single phone number (E.164). Caller must be app creator. */
25
+ send(to: string, message: string): Promise<SmsSendResult>;
26
+ /** Send the same SMS to many recipients. Caller must be app creator. */
27
+ broadcast(numbers: string[], message: string): Promise<SmsSendResult>;
28
+ private _send;
29
+ }
30
+ export {};
31
+ //# sourceMappingURL=sms.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sms.d.ts","sourceRoot":"","sources":["../src/sms.ts"],"names":[],"mappings":"AAAA,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,kBAAkB,IAAI,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,qBAAa,GAAG;IAEZ,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAFJ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,QAAQ;IAGjC,gFAAgF;IAC1E,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAI/D,wEAAwE;IAClE,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;YAI7D,KAAK;CAuBpB"}
package/dist/sms.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * SMS — send text messages via the PAS API (Twilio-backed server-side).
3
+ *
4
+ * The platform owns the Twilio credentials; the app never sees them.
5
+ * Only the app creator can send. Rate-limiting / abuse protection is
6
+ * server-side. Numbers must be E.164 ("+15551234567").
7
+ *
8
+ * Use case: class reminders, OTP codes, no-show notifications. Pair with
9
+ * `app.notifications` (Web Push) for in-browser delivery on the same event.
10
+ */
11
+ export class SMS {
12
+ appId;
13
+ apiBase;
14
+ auth;
15
+ constructor(appId, apiBase, auth) {
16
+ this.appId = appId;
17
+ this.apiBase = apiBase;
18
+ this.auth = auth;
19
+ }
20
+ /** Send an SMS to a single phone number (E.164). Caller must be app creator. */
21
+ async send(to, message) {
22
+ return this._send([to], message);
23
+ }
24
+ /** Send the same SMS to many recipients. Caller must be app creator. */
25
+ async broadcast(numbers, message) {
26
+ return this._send(numbers, message);
27
+ }
28
+ async _send(numbers, message) {
29
+ const token = this.auth.token;
30
+ if (!token)
31
+ throw new Error('Not signed in.');
32
+ const res = await fetch(`${this.apiBase}/v1/sms/send`, {
33
+ method: 'POST',
34
+ headers: {
35
+ Authorization: `Bearer ${token}`,
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ body: JSON.stringify({ appId: this.appId, to: numbers, message }),
39
+ });
40
+ if (res.status === 401) {
41
+ this.auth.handleUnauthorized();
42
+ throw new Error('Not signed in.');
43
+ }
44
+ if (res.status === 403)
45
+ throw new Error('Only the app creator can send SMS.');
46
+ if (res.status === 503)
47
+ throw new Error('SMS is not configured on this platform.');
48
+ if (!res.ok)
49
+ throw new Error(`SMS send failed: ${res.status}`);
50
+ return (await res.json());
51
+ }
52
+ }
53
+ //# sourceMappingURL=sms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sms.js","sourceRoot":"","sources":["../src/sms.ts"],"names":[],"mappings":"AAUA;;;;;;;;;GASG;AACH,MAAM,OAAO,GAAG;IAEK;IACA;IACA;IAHnB,YACmB,KAAa,EACb,OAAe,EACf,IAAc;QAFd,UAAK,GAAL,KAAK,CAAQ;QACb,YAAO,GAAP,OAAO,CAAQ;QACf,SAAI,GAAJ,IAAI,CAAU;IAC9B,CAAC;IAEJ,gFAAgF;IAChF,KAAK,CAAC,IAAI,CAAC,EAAU,EAAE,OAAe;QACpC,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,wEAAwE;IACxE,KAAK,CAAC,SAAS,CAAC,OAAiB,EAAE,OAAe;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACtC,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,OAAiB,EAAE,OAAe;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAE9C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,cAAc,EAAE;YACrD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,aAAa,EAAE,UAAU,KAAK,EAAE;gBAChC,cAAc,EAAE,kBAAkB;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;SAClE,CAAC,CAAC;QAEH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;QACpC,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QAC9E,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QACnF,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAE/D,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAkB,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,52 @@
1
+ import type { Database, ExecuteResult } from './db.js';
2
+ /**
3
+ * TenantScope — safe-by-default helpers for multi-tenant tables.
4
+ *
5
+ * Every multi-tenant table in your app must have a `tenant_id` column.
6
+ * The helpers here auto-inject `tenant_id` on inserts and auto-scope
7
+ * reads/writes by `tenant_id` — so you can't accidentally leak a row
8
+ * across tenants by forgetting a `WHERE` clause.
9
+ *
10
+ * Where this lives architecturally: a thin wrapper around `app.db`.
11
+ * It does NOT replace `db.query` / `db.execute` — those are still raw.
12
+ * Use the scope helpers for normal CRUD; drop down to `db.query` when
13
+ * you need joins, aggregates, or anything beyond single-table operations.
14
+ *
15
+ * @example
16
+ * const tx = app.db.tenant('studio-123');
17
+ *
18
+ * await tx.insert('clients', { id: 'c-1', name: 'Alice' });
19
+ * const alice = await tx.find('clients', { id: 'c-1' });
20
+ * await tx.update('clients', { id: 'c-1' }, { name: 'Alicia' });
21
+ * await tx.delete('clients', { id: 'c-1' });
22
+ *
23
+ * // Raw escape hatch — tenant_id available as tx.tenantId; bind it yourself.
24
+ * const rows = await tx.db.query(
25
+ * 'SELECT * FROM clients WHERE name LIKE ? AND tenant_id = ?',
26
+ * ['A%', tx.tenantId],
27
+ * );
28
+ */
29
+ export declare class TenantScope {
30
+ /** The Database instance this scope wraps. Exposed for raw escape-hatch queries. */
31
+ readonly db: Database;
32
+ /** The tenant_id all scope operations bind to. */
33
+ readonly tenantId: string;
34
+ constructor(
35
+ /** The Database instance this scope wraps. Exposed for raw escape-hatch queries. */
36
+ db: Database,
37
+ /** The tenant_id all scope operations bind to. */
38
+ tenantId: string);
39
+ /** Find a single row matching the filter (tenant_id automatically appended). Returns null when nothing matches. */
40
+ find<T = Record<string, unknown>>(table: string, filter?: Record<string, unknown>): Promise<T | null>;
41
+ /** Find all rows matching the filter (tenant_id automatically appended). */
42
+ findMany<T = Record<string, unknown>>(table: string, filter?: Record<string, unknown>): Promise<T[]>;
43
+ /** Insert a row. `tenant_id` is set automatically — don't include it in `values`. */
44
+ insert(table: string, values: Record<string, unknown>): Promise<ExecuteResult>;
45
+ /** Update rows matching `filter`. `tenant_id` is auto-appended to the WHERE. */
46
+ update(table: string, filter: Record<string, unknown>, values: Record<string, unknown>): Promise<ExecuteResult>;
47
+ /** Delete rows matching `filter`. `tenant_id` is auto-appended. */
48
+ delete(table: string, filter: Record<string, unknown>): Promise<ExecuteResult>;
49
+ /** Count rows matching `filter` (or all in the tenant). */
50
+ count(table: string, filter?: Record<string, unknown>): Promise<number>;
51
+ }
52
+ //# sourceMappingURL=tenant.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAe,MAAM,SAAS,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,WAAW;IAEpB,oFAAoF;IACpF,QAAQ,CAAC,EAAE,EAAE,QAAQ;IACrB,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM;;IAHzB,oFAAoF;IAC3E,EAAE,EAAE,QAAQ;IACrB,kDAAkD;IACzC,QAAQ,EAAE,MAAM;IAK3B,mHAAmH;IAC7G,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAQpB,4EAA4E;IACtE,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxC,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,CAAC,EAAE,CAAC;IAQf,qFAAqF;IAC/E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAapF,gFAAgF;IAC1E,MAAM,CACV,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,aAAa,CAAC;IAczB,mEAAmE;IAC7D,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC;IAOpF,2DAA2D;IACrD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAOlF"}
package/dist/tenant.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * TenantScope — safe-by-default helpers for multi-tenant tables.
3
+ *
4
+ * Every multi-tenant table in your app must have a `tenant_id` column.
5
+ * The helpers here auto-inject `tenant_id` on inserts and auto-scope
6
+ * reads/writes by `tenant_id` — so you can't accidentally leak a row
7
+ * across tenants by forgetting a `WHERE` clause.
8
+ *
9
+ * Where this lives architecturally: a thin wrapper around `app.db`.
10
+ * It does NOT replace `db.query` / `db.execute` — those are still raw.
11
+ * Use the scope helpers for normal CRUD; drop down to `db.query` when
12
+ * you need joins, aggregates, or anything beyond single-table operations.
13
+ *
14
+ * @example
15
+ * const tx = app.db.tenant('studio-123');
16
+ *
17
+ * await tx.insert('clients', { id: 'c-1', name: 'Alice' });
18
+ * const alice = await tx.find('clients', { id: 'c-1' });
19
+ * await tx.update('clients', { id: 'c-1' }, { name: 'Alicia' });
20
+ * await tx.delete('clients', { id: 'c-1' });
21
+ *
22
+ * // Raw escape hatch — tenant_id available as tx.tenantId; bind it yourself.
23
+ * const rows = await tx.db.query(
24
+ * 'SELECT * FROM clients WHERE name LIKE ? AND tenant_id = ?',
25
+ * ['A%', tx.tenantId],
26
+ * );
27
+ */
28
+ export class TenantScope {
29
+ db;
30
+ tenantId;
31
+ constructor(
32
+ /** The Database instance this scope wraps. Exposed for raw escape-hatch queries. */
33
+ db,
34
+ /** The tenant_id all scope operations bind to. */
35
+ tenantId) {
36
+ this.db = db;
37
+ this.tenantId = tenantId;
38
+ if (!tenantId)
39
+ throw new Error('TenantScope requires a non-empty tenantId.');
40
+ }
41
+ /** Find a single row matching the filter (tenant_id automatically appended). Returns null when nothing matches. */
42
+ async find(table, filter = {}) {
43
+ assertIdent(table);
44
+ const { whereSql, params } = whereClause(filter, this.tenantId);
45
+ const sql = `SELECT * FROM ${table} ${whereSql} LIMIT 1`;
46
+ const result = await this.db.query(sql, params);
47
+ return result.rows[0] ?? null;
48
+ }
49
+ /** Find all rows matching the filter (tenant_id automatically appended). */
50
+ async findMany(table, filter = {}) {
51
+ assertIdent(table);
52
+ const { whereSql, params } = whereClause(filter, this.tenantId);
53
+ const sql = `SELECT * FROM ${table} ${whereSql}`;
54
+ const result = await this.db.query(sql, params);
55
+ return result.rows;
56
+ }
57
+ /** Insert a row. `tenant_id` is set automatically — don't include it in `values`. */
58
+ async insert(table, values) {
59
+ assertIdent(table);
60
+ if ('tenant_id' in values) {
61
+ throw new Error('Do not pass tenant_id to TenantScope.insert — it is set automatically.');
62
+ }
63
+ const fullValues = { ...values, tenant_id: this.tenantId };
64
+ const cols = Object.keys(fullValues);
65
+ for (const c of cols)
66
+ assertIdent(c);
67
+ const placeholders = cols.map(() => '?').join(', ');
68
+ const sql = `INSERT INTO ${table} (${cols.join(', ')}) VALUES (${placeholders})`;
69
+ return this.db.execute(sql, Object.values(fullValues));
70
+ }
71
+ /** Update rows matching `filter`. `tenant_id` is auto-appended to the WHERE. */
72
+ async update(table, filter, values) {
73
+ assertIdent(table);
74
+ if ('tenant_id' in values) {
75
+ throw new Error('Do not pass tenant_id to TenantScope.update — it is preserved automatically.');
76
+ }
77
+ const setCols = Object.keys(values);
78
+ if (setCols.length === 0)
79
+ throw new Error('TenantScope.update requires at least one column to set.');
80
+ for (const c of setCols)
81
+ assertIdent(c);
82
+ const setSql = setCols.map((c) => `${c} = ?`).join(', ');
83
+ const { whereSql, params: whereParams } = whereClause(filter, this.tenantId);
84
+ const sql = `UPDATE ${table} SET ${setSql} ${whereSql}`;
85
+ return this.db.execute(sql, [...Object.values(values), ...whereParams]);
86
+ }
87
+ /** Delete rows matching `filter`. `tenant_id` is auto-appended. */
88
+ async delete(table, filter) {
89
+ assertIdent(table);
90
+ const { whereSql, params } = whereClause(filter, this.tenantId);
91
+ const sql = `DELETE FROM ${table} ${whereSql}`;
92
+ return this.db.execute(sql, params);
93
+ }
94
+ /** Count rows matching `filter` (or all in the tenant). */
95
+ async count(table, filter = {}) {
96
+ assertIdent(table);
97
+ const { whereSql, params } = whereClause(filter, this.tenantId);
98
+ const sql = `SELECT COUNT(*) AS n FROM ${table} ${whereSql}`;
99
+ const result = await this.db.query(sql, params);
100
+ return Number(result.rows[0]?.n ?? 0);
101
+ }
102
+ }
103
+ /** Build a WHERE clause from an equality filter plus the auto-injected tenant_id. */
104
+ function whereClause(filter, tenantId) {
105
+ const keys = Object.keys(filter);
106
+ for (const k of keys)
107
+ assertIdent(k);
108
+ const conds = keys.map((k) => `${k} = ?`);
109
+ conds.push('tenant_id = ?');
110
+ const params = [...keys.map((k) => filter[k]), tenantId];
111
+ return { whereSql: `WHERE ${conds.join(' AND ')}`, params };
112
+ }
113
+ /**
114
+ * Identifiers (table + column names) cannot be parameterized in SQL, so we
115
+ * interpolate them directly. Reject anything that isn't a safe SQL identifier
116
+ * to keep this from becoming an SQL-injection vector.
117
+ *
118
+ * Allowed: ASCII letter or underscore, followed by letters/digits/underscores.
119
+ * Reject anything with quotes, spaces, semicolons, dashes, dots, etc.
120
+ */
121
+ function assertIdent(name) {
122
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
123
+ throw new Error(`Unsafe SQL identifier: ${JSON.stringify(name)}`);
124
+ }
125
+ }
126
+ //# sourceMappingURL=tenant.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tenant.js","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -6,6 +6,11 @@ export interface ProInitOptions {
6
6
  proApiBase?: string;
7
7
  /** Defaults to https://data-{appId}.proappstore.online (per-app data worker). */
8
8
  dataApiBase?: string;
9
+ /** Usage telemetry options. Auto-heartbeat is on by default. */
10
+ usage?: {
11
+ /** Default true. Set false to disable the auto-heartbeat in this app. */
12
+ auto?: boolean;
13
+ };
9
14
  }
10
15
  export type SubscriptionStatus = 'active' | 'past_due' | 'canceled' | 'incomplete';
11
16
  export interface Subscription {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,YAAY,CAAC;AAEnF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,KAAK,CAAC,EAAE;QACN,yEAAyE;QACzE,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,YAAY,CAAC;AAEnF,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sDAAsD;IACtD,gBAAgB,EAAE,MAAM,CAAC;IACzB,2DAA2D;IAC3D,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Usage telemetry — heartbeats `POST /v1/usage/ping` while the tab is visible.
3
+ *
4
+ * Auto-started by `initPro()` unless `usage.auto === false` in the options.
5
+ * The collected (app, user, day) counts drive the usage-proportional creator
6
+ * payouts described in proappstore.online/pricing.
7
+ *
8
+ * Design notes:
9
+ *
10
+ * - **Visible-time only.** We start a stopwatch when the tab is visible and
11
+ * pause on `visibilitychange`. A tab that's been hidden for 90s contributes
12
+ * 0 seconds for that interval.
13
+ * - **Browser-only.** All methods no-op when `document` or `window` is
14
+ * undefined so the import is SSR-safe.
15
+ * - **Silent failures.** Telemetry must never break an app. Every network
16
+ * path catches + ignores errors.
17
+ * - **Page-close flush.** On `pagehide` we send a final ping with the
18
+ * residual elapsed seconds via `navigator.sendBeacon` (survives unload)
19
+ * falling back to `fetch(..., { keepalive: true })`.
20
+ */
21
+ interface AuthLike {
22
+ token: string | null;
23
+ }
24
+ export interface UsageOptions {
25
+ /** Default true. Set false to disable auto-heartbeat in this app. */
26
+ auto?: boolean;
27
+ }
28
+ export declare class Usage {
29
+ private readonly appId;
30
+ private readonly apiBase;
31
+ private readonly auth;
32
+ private running;
33
+ private timer;
34
+ private visibleSince;
35
+ /** Accumulated visible-time since the last successful ping, in milliseconds. */
36
+ private accruedMs;
37
+ private pendingApiCalls;
38
+ private onVisibility;
39
+ private onPageHide;
40
+ constructor(appId: string, apiBase: string, auth: AuthLike);
41
+ /** Begin heartbeat reporting. Idempotent — calling twice is a no-op. */
42
+ start(): void;
43
+ /** Stop heartbeats. Idempotent. Doesn't flush; call `flush()` if you need to. */
44
+ stop(): void;
45
+ /**
46
+ * Record API calls. Bumps a local counter that piggybacks on the next
47
+ * heartbeat. Cheap to call from hot paths — no network until the next tick.
48
+ */
49
+ recordApiCall(n?: number): void;
50
+ /**
51
+ * Send a final ping (intended for page-close paths). Best-effort,
52
+ * fire-and-forget. Uses sendBeacon when available so the request survives
53
+ * unload; falls back to keepalive fetch otherwise.
54
+ */
55
+ flush(): void;
56
+ /** Add any in-progress visible-time to the running accrual. */
57
+ private bankVisible;
58
+ /** Round accrued ms to whole seconds, clamp to MAX_DELTA_SECONDS, return + reset. */
59
+ private drainSeconds;
60
+ private drainApiCalls;
61
+ private tick;
62
+ private send;
63
+ }
64
+ export {};
65
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,UAAU,QAAQ;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AASD,qBAAa,KAAK;IAWd,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAZvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,YAAY,CAAuB;IAC3C,gFAAgF;IAChF,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,UAAU,CAA6B;gBAG5B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,QAAQ;IAGjC,wEAAwE;IACxE,KAAK,IAAI,IAAI;IA6Bb,iFAAiF;IACjF,IAAI,IAAI,IAAI;IAkBZ;;;OAGG;IACH,aAAa,CAAC,CAAC,GAAE,MAAU,GAAG,IAAI;IAKlC;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAWb,+DAA+D;IAC/D,OAAO,CAAC,WAAW;IAOnB,qFAAqF;IACrF,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,aAAa;YAMP,IAAI;YAoBJ,IAAI;CAsCnB"}