@sparkvault/sdk-mobile 5.1.0 → 5.2.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.
package/dist/index.d.ts CHANGED
@@ -14,7 +14,7 @@ export type { CreateSparkOptions, CreateSparkResponse, ListSparksOptions, ListSp
14
14
  export { MobileEntropyClient, } from './entropy.js';
15
15
  export type { EntropyFormat, EntropyResponse, GenerateEntropyOptions, } from './entropy.js';
16
16
  export { MobileNotifyClient, } from './notify.js';
17
- export type { NotificationInboxState, NotificationMarkState, NotificationRow, ListNotificationsOptions, ListNotificationsResponse, UnreadCountResponse, MarkNotificationStateOptions, MarkNotificationStateResponse, RegisterDeviceResponse, } from './notify.js';
17
+ export type { NotificationInboxState, NotificationMarkState, NotificationRow, ListNotificationsOptions, ListNotificationsResponse, UnreadCountResponse, MarkNotificationStateOptions, MarkNotificationStateResponse, NotificationBulkMarkState, MarkAllNotificationsStateOptions, MarkAllNotificationsStateResponse, MarkAllReadResult, RegisterDeviceResponse, } from './notify.js';
18
18
  export { MobileBillingClient, } from './billing.js';
19
19
  export type { PortalSession, } from './billing.js';
20
20
  export { MobileUsersClient, } from './users.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AASrB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAYrB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAUrB,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAMtB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAYrB,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAItB,OAAO,EACL,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,iBAAiB,EACjB,aAAa,GACd,MAAM,UAAU,CAAC;AAKlB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,aAAa,EACb,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,EAClB,6BAA6B,EAC7B,4BAA4B,EAC5B,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,yBAAyB,EACzB,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,4BAA4B,GAC7B,MAAM,aAAa,CAAC;AASrB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAYrB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAKrB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAUrB,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAMtB,OAAO,EACL,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAgBrB,OAAO,EACL,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAItB,OAAO,EACL,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,iBAAiB,EACjB,aAAa,GACd,MAAM,UAAU,CAAC;AAKlB,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,WAAW,EACX,aAAa,EACb,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,EACxB,iBAAiB,EACjB,kBAAkB,EAClB,6BAA6B,EAC7B,4BAA4B,EAC5B,qBAAqB,EACrB,sBAAsB,EACtB,sBAAsB,EACtB,yBAAyB,EACzB,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,eAAe,GAChB,MAAM,iBAAiB,CAAC"}
package/dist/notify.d.ts CHANGED
@@ -66,6 +66,33 @@ export interface MarkNotificationStateResponse {
66
66
  state: NotificationMarkState;
67
67
  updated_at: number;
68
68
  }
69
+ /**
70
+ * States a BULK drain can apply. `archived` is per-row only — it has no GSI for
71
+ * the server to page over, so the bulk endpoint rejects it.
72
+ */
73
+ export type NotificationBulkMarkState = 'seen' | 'read';
74
+ export interface MarkAllNotificationsStateOptions {
75
+ state: NotificationBulkMarkState;
76
+ /** Cursor from the prior page's response; omit to start at the newest page. */
77
+ cursor?: string;
78
+ }
79
+ export interface MarkAllNotificationsStateResponse {
80
+ /** Rows marked by THIS page — the server marks one bounded page per call. */
81
+ updated: number;
82
+ /** Cursor for the next page, or null once the inbox is drained. */
83
+ cursor: string | null;
84
+ }
85
+ /** Total across a full `markAllRead()` drain. */
86
+ export interface MarkAllReadResult {
87
+ updated: number;
88
+ /**
89
+ * True when the drain reached the end of the inbox (the server returned a null
90
+ * cursor). False when it stopped at the page bound with rows still unmarked —
91
+ * call again to continue. Without this a truncated drain is indistinguishable
92
+ * from a completed one.
93
+ */
94
+ complete: boolean;
95
+ }
69
96
  /** Response to registering/refreshing this device's push token. */
70
97
  export interface RegisterDeviceResponse {
71
98
  success: boolean;
@@ -90,8 +117,39 @@ export declare class MobileNotifyClient {
90
117
  * `createdAt` MUST be the row's own `created_at` — the id alone is insufficient.
91
118
  */
92
119
  markState(notificationId: string, options: MarkNotificationStateOptions): Promise<MarkNotificationStateResponse>;
120
+ /**
121
+ * Bulk-mark ONE bounded page of the caller's inbox and return the server's
122
+ * payload verbatim. `cursor` comes back non-null while rows remain — thread it
123
+ * into the next call. `archived` is per-row only (no GSI to page over), so the
124
+ * bulk states are `seen` and `read`.
125
+ *
126
+ * Prefer `markAllRead()` unless the caller needs to own the paging.
127
+ */
128
+ markAllState(options: MarkAllNotificationsStateOptions): Promise<MarkAllNotificationsStateResponse>;
129
+ /**
130
+ * Drain the caller's whole inbox to read: loop `markAllState` on the returned
131
+ * cursor until it comes back null, and return the total marked. Marking read
132
+ * clears the unseen marker too, so the badge zeroes with it. Idempotent.
133
+ *
134
+ * `complete` is false when the loop stopped at the page bound with rows still
135
+ * unmarked — an inbox deeper than the bound finishes on a second call. Without
136
+ * it a truncated drain looks exactly like a finished one.
137
+ *
138
+ * An error mid-drain propagates — the pages already marked stay marked, and a
139
+ * re-run picks up the remainder.
140
+ */
141
+ markAllRead(): Promise<MarkAllReadResult>;
93
142
  /** Register/refresh this device's Expo push token under the session user. */
94
143
  registerDevice(pushToken: string): Promise<RegisterDeviceResponse>;
95
- /** Deregister a device token (logout / disable notifications). */
144
+ /**
145
+ * Deregister this device by the raw push token it already holds (logout /
146
+ * disable notifications). The server derives the storage hash of the token
147
+ * itself, so the caller never reimplements that derivation.
148
+ */
149
+ unregisterDeviceToken(pushToken: string): Promise<void>;
150
+ /**
151
+ * Deregister a device addressed by the server's storage hash of its token.
152
+ * Use `unregisterDeviceToken` when the caller holds the token itself.
153
+ */
96
154
  unregisterDevice(tokenHash: string): Promise<void>;
97
155
  }
package/dist/notify.js CHANGED
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Hard bound on `markAllRead()`'s drain loop: 100 server pages of 100 rows. A
3
+ * cursor that never resolves to null can therefore never spin forever, and an
4
+ * inbox deeper than the bound finishes on a second call (the drain is idempotent).
5
+ */
6
+ const MARK_ALL_MAX_PAGES = 100;
1
7
  /**
2
8
  * The signed-in user's first-party Notify surface: their own metadata-only inbox
3
9
  * plus this device's push registration. The recipient is ALWAYS derived from the
@@ -34,6 +40,45 @@ export class MobileNotifyClient {
34
40
  const response = await this.http.post(`/me/notifications/${encodeURIComponent(notificationId)}/state`, { created_at: options.createdAt, state: options.state });
35
41
  return response.data;
36
42
  }
43
+ /**
44
+ * Bulk-mark ONE bounded page of the caller's inbox and return the server's
45
+ * payload verbatim. `cursor` comes back non-null while rows remain — thread it
46
+ * into the next call. `archived` is per-row only (no GSI to page over), so the
47
+ * bulk states are `seen` and `read`.
48
+ *
49
+ * Prefer `markAllRead()` unless the caller needs to own the paging.
50
+ */
51
+ async markAllState(options) {
52
+ const body = { state: options.state };
53
+ if (options.cursor)
54
+ body.cursor = options.cursor;
55
+ const response = await this.http.post('/me/notifications/state', body);
56
+ return response.data;
57
+ }
58
+ /**
59
+ * Drain the caller's whole inbox to read: loop `markAllState` on the returned
60
+ * cursor until it comes back null, and return the total marked. Marking read
61
+ * clears the unseen marker too, so the badge zeroes with it. Idempotent.
62
+ *
63
+ * `complete` is false when the loop stopped at the page bound with rows still
64
+ * unmarked — an inbox deeper than the bound finishes on a second call. Without
65
+ * it a truncated drain looks exactly like a finished one.
66
+ *
67
+ * An error mid-drain propagates — the pages already marked stay marked, and a
68
+ * re-run picks up the remainder.
69
+ */
70
+ async markAllRead() {
71
+ let cursor;
72
+ let updated = 0;
73
+ for (let page = 0; page < MARK_ALL_MAX_PAGES; page += 1) {
74
+ const result = await this.markAllState(cursor ? { state: 'read', cursor } : { state: 'read' });
75
+ updated += result?.updated ?? 0;
76
+ if (!result?.cursor)
77
+ return { updated, complete: true };
78
+ cursor = result.cursor;
79
+ }
80
+ return { updated, complete: false };
81
+ }
37
82
  /** Register/refresh this device's Expo push token under the session user. */
38
83
  async registerDevice(pushToken) {
39
84
  const response = await this.http.post('/me/notifications/devices', {
@@ -41,7 +86,18 @@ export class MobileNotifyClient {
41
86
  });
42
87
  return response.data;
43
88
  }
44
- /** Deregister a device token (logout / disable notifications). */
89
+ /**
90
+ * Deregister this device by the raw push token it already holds (logout /
91
+ * disable notifications). The server derives the storage hash of the token
92
+ * itself, so the caller never reimplements that derivation.
93
+ */
94
+ async unregisterDeviceToken(pushToken) {
95
+ await this.http.delete('/me/notifications/devices', { body: { token: pushToken } });
96
+ }
97
+ /**
98
+ * Deregister a device addressed by the server's storage hash of its token.
99
+ * Use `unregisterDeviceToken` when the caller holds the token itself.
100
+ */
45
101
  async unregisterDevice(tokenHash) {
46
102
  await this.http.delete(`/me/notifications/devices/${encodeURIComponent(tokenHash)}`);
47
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"notify.js","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"AAoFA;;;;;GAKG;AACH,MAAM,OAAO,kBAAkB;IAG7B,YAAY,IAAsB;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,kDAAkD;IAClD,KAAK,CAAC,QAAQ,CAAC,UAAoC,EAAE;QACnD,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,KAAK;YAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QACtD,IAAI,OAAO,CAAC,KAAK;YAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9D,IAAI,OAAO,CAAC,MAAM;YAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAEzD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAClC,KAAK,CAAC,CAAC,CAAC,qBAAqB,KAAK,EAAE,CAAC,CAAC,CAAC,mBAAmB,CAC3D,CAAC;QACF,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,6DAA6D;IAC7D,KAAK,CAAC,cAAc;QAClB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAsB,gCAAgC,CAAC,CAAC;QAC5F,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,CACb,cAAsB,EACtB,OAAqC;QAErC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CACnC,qBAAqB,kBAAkB,CAAC,cAAc,CAAC,QAAQ,EAC/D,EAAE,UAAU,EAAE,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CACxD,CAAC;QACF,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,cAAc,CAAC,SAAiB;QACpC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAyB,2BAA2B,EAAE;YACzF,UAAU,EAAE,SAAS;SACtB,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,kEAAkE;IAClE,KAAK,CAAC,gBAAgB,CAAC,SAAiB;QACtC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,6BAA6B,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;CACF"}
1
+ {"version":3,"file":"notify.js","sourceRoot":"","sources":["../src/notify.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sparkvault/sdk-mobile",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Mobile SDK for SparkVault Identity, vault, folder, and ingot workflows",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -76,6 +76,10 @@ export type {
76
76
  UnreadCountResponse,
77
77
  MarkNotificationStateOptions,
78
78
  MarkNotificationStateResponse,
79
+ NotificationBulkMarkState,
80
+ MarkAllNotificationsStateOptions,
81
+ MarkAllNotificationsStateResponse,
82
+ MarkAllReadResult,
79
83
  RegisterDeviceResponse,
80
84
  } from './notify.js';
81
85
  export {
package/src/notify.ts CHANGED
@@ -75,6 +75,37 @@ export interface MarkNotificationStateResponse {
75
75
  updated_at: number;
76
76
  }
77
77
 
78
+ /**
79
+ * States a BULK drain can apply. `archived` is per-row only — it has no GSI for
80
+ * the server to page over, so the bulk endpoint rejects it.
81
+ */
82
+ export type NotificationBulkMarkState = 'seen' | 'read';
83
+
84
+ export interface MarkAllNotificationsStateOptions {
85
+ state: NotificationBulkMarkState;
86
+ /** Cursor from the prior page's response; omit to start at the newest page. */
87
+ cursor?: string;
88
+ }
89
+
90
+ export interface MarkAllNotificationsStateResponse {
91
+ /** Rows marked by THIS page — the server marks one bounded page per call. */
92
+ updated: number;
93
+ /** Cursor for the next page, or null once the inbox is drained. */
94
+ cursor: string | null;
95
+ }
96
+
97
+ /** Total across a full `markAllRead()` drain. */
98
+ export interface MarkAllReadResult {
99
+ updated: number;
100
+ /**
101
+ * True when the drain reached the end of the inbox (the server returned a null
102
+ * cursor). False when it stopped at the page bound with rows still unmarked —
103
+ * call again to continue. Without this a truncated drain is indistinguishable
104
+ * from a completed one.
105
+ */
106
+ complete: boolean;
107
+ }
108
+
78
109
  /** Response to registering/refreshing this device's push token. */
79
110
  export interface RegisterDeviceResponse {
80
111
  success: boolean;
@@ -82,6 +113,13 @@ export interface RegisterDeviceResponse {
82
113
  refreshed: boolean;
83
114
  }
84
115
 
116
+ /**
117
+ * Hard bound on `markAllRead()`'s drain loop: 100 server pages of 100 rows. A
118
+ * cursor that never resolves to null can therefore never spin forever, and an
119
+ * inbox deeper than the bound finishes on a second call (the drain is idempotent).
120
+ */
121
+ const MARK_ALL_MAX_PAGES = 100;
122
+
85
123
  /**
86
124
  * The signed-in user's first-party Notify surface: their own metadata-only inbox
87
125
  * plus this device's push registration. The recipient is ALWAYS derived from the
@@ -130,6 +168,53 @@ export class MobileNotifyClient {
130
168
  return response.data;
131
169
  }
132
170
 
171
+ /**
172
+ * Bulk-mark ONE bounded page of the caller's inbox and return the server's
173
+ * payload verbatim. `cursor` comes back non-null while rows remain — thread it
174
+ * into the next call. `archived` is per-row only (no GSI to page over), so the
175
+ * bulk states are `seen` and `read`.
176
+ *
177
+ * Prefer `markAllRead()` unless the caller needs to own the paging.
178
+ */
179
+ async markAllState(
180
+ options: MarkAllNotificationsStateOptions
181
+ ): Promise<MarkAllNotificationsStateResponse> {
182
+ const body: { state: NotificationBulkMarkState; cursor?: string } = { state: options.state };
183
+ if (options.cursor) body.cursor = options.cursor;
184
+
185
+ const response = await this.http.post<MarkAllNotificationsStateResponse>(
186
+ '/me/notifications/state',
187
+ body
188
+ );
189
+ return response.data;
190
+ }
191
+
192
+ /**
193
+ * Drain the caller's whole inbox to read: loop `markAllState` on the returned
194
+ * cursor until it comes back null, and return the total marked. Marking read
195
+ * clears the unseen marker too, so the badge zeroes with it. Idempotent.
196
+ *
197
+ * `complete` is false when the loop stopped at the page bound with rows still
198
+ * unmarked — an inbox deeper than the bound finishes on a second call. Without
199
+ * it a truncated drain looks exactly like a finished one.
200
+ *
201
+ * An error mid-drain propagates — the pages already marked stay marked, and a
202
+ * re-run picks up the remainder.
203
+ */
204
+ async markAllRead(): Promise<MarkAllReadResult> {
205
+ let cursor: string | undefined;
206
+ let updated = 0;
207
+
208
+ for (let page = 0; page < MARK_ALL_MAX_PAGES; page += 1) {
209
+ const result = await this.markAllState(cursor ? { state: 'read', cursor } : { state: 'read' });
210
+ updated += result?.updated ?? 0;
211
+ if (!result?.cursor) return { updated, complete: true };
212
+ cursor = result.cursor;
213
+ }
214
+
215
+ return { updated, complete: false };
216
+ }
217
+
133
218
  /** Register/refresh this device's Expo push token under the session user. */
134
219
  async registerDevice(pushToken: string): Promise<RegisterDeviceResponse> {
135
220
  const response = await this.http.post<RegisterDeviceResponse>('/me/notifications/devices', {
@@ -138,7 +223,19 @@ export class MobileNotifyClient {
138
223
  return response.data;
139
224
  }
140
225
 
141
- /** Deregister a device token (logout / disable notifications). */
226
+ /**
227
+ * Deregister this device by the raw push token it already holds (logout /
228
+ * disable notifications). The server derives the storage hash of the token
229
+ * itself, so the caller never reimplements that derivation.
230
+ */
231
+ async unregisterDeviceToken(pushToken: string): Promise<void> {
232
+ await this.http.delete('/me/notifications/devices', { body: { token: pushToken } });
233
+ }
234
+
235
+ /**
236
+ * Deregister a device addressed by the server's storage hash of its token.
237
+ * Use `unregisterDeviceToken` when the caller holds the token itself.
238
+ */
142
239
  async unregisterDevice(tokenHash: string): Promise<void> {
143
240
  await this.http.delete(`/me/notifications/devices/${encodeURIComponent(tokenHash)}`);
144
241
  }