@sparkvault/sdk-mobile 5.1.0 → 5.2.1

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
@@ -16,8 +16,10 @@ export interface NotificationRow {
16
16
  created_at: number;
17
17
  title: string;
18
18
  /**
19
- * The short cleartext body of a plaintext send, shown natively. Absent on a
20
- * sealed row (its content lives behind the pointer).
19
+ * The row's visible cleartext line, shown natively: a plaintext send's inline
20
+ * body, or a SEALED send's non-secret `instructions`. A sealed row can and
21
+ * normally should carry one — it is the readable line above the reveal. Never
22
+ * the sealed content itself. Absent when the sender set neither.
21
23
  */
22
24
  body?: string;
23
25
  category: string;
@@ -66,6 +68,33 @@ export interface MarkNotificationStateResponse {
66
68
  state: NotificationMarkState;
67
69
  updated_at: number;
68
70
  }
71
+ /**
72
+ * States a BULK drain can apply. `archived` is per-row only — it has no GSI for
73
+ * the server to page over, so the bulk endpoint rejects it.
74
+ */
75
+ export type NotificationBulkMarkState = 'seen' | 'read';
76
+ export interface MarkAllNotificationsStateOptions {
77
+ state: NotificationBulkMarkState;
78
+ /** Cursor from the prior page's response; omit to start at the newest page. */
79
+ cursor?: string;
80
+ }
81
+ export interface MarkAllNotificationsStateResponse {
82
+ /** Rows marked by THIS page — the server marks one bounded page per call. */
83
+ updated: number;
84
+ /** Cursor for the next page, or null once the inbox is drained. */
85
+ cursor: string | null;
86
+ }
87
+ /** Total across a full `markAllRead()` drain. */
88
+ export interface MarkAllReadResult {
89
+ updated: number;
90
+ /**
91
+ * True when the drain reached the end of the inbox (the server returned a null
92
+ * cursor). False when it stopped at the page bound with rows still unmarked —
93
+ * call again to continue. Without this a truncated drain is indistinguishable
94
+ * from a completed one.
95
+ */
96
+ complete: boolean;
97
+ }
69
98
  /** Response to registering/refreshing this device's push token. */
70
99
  export interface RegisterDeviceResponse {
71
100
  success: boolean;
@@ -90,8 +119,39 @@ export declare class MobileNotifyClient {
90
119
  * `createdAt` MUST be the row's own `created_at` — the id alone is insufficient.
91
120
  */
92
121
  markState(notificationId: string, options: MarkNotificationStateOptions): Promise<MarkNotificationStateResponse>;
122
+ /**
123
+ * Bulk-mark ONE bounded page of the caller's inbox and return the server's
124
+ * payload verbatim. `cursor` comes back non-null while rows remain — thread it
125
+ * into the next call. `archived` is per-row only (no GSI to page over), so the
126
+ * bulk states are `seen` and `read`.
127
+ *
128
+ * Prefer `markAllRead()` unless the caller needs to own the paging.
129
+ */
130
+ markAllState(options: MarkAllNotificationsStateOptions): Promise<MarkAllNotificationsStateResponse>;
131
+ /**
132
+ * Drain the caller's whole inbox to read: loop `markAllState` on the returned
133
+ * cursor until it comes back null, and return the total marked. Marking read
134
+ * clears the unseen marker too, so the badge zeroes with it. Idempotent.
135
+ *
136
+ * `complete` is false when the loop stopped at the page bound with rows still
137
+ * unmarked — an inbox deeper than the bound finishes on a second call. Without
138
+ * it a truncated drain looks exactly like a finished one.
139
+ *
140
+ * An error mid-drain propagates — the pages already marked stay marked, and a
141
+ * re-run picks up the remainder.
142
+ */
143
+ markAllRead(): Promise<MarkAllReadResult>;
93
144
  /** Register/refresh this device's Expo push token under the session user. */
94
145
  registerDevice(pushToken: string): Promise<RegisterDeviceResponse>;
95
- /** Deregister a device token (logout / disable notifications). */
146
+ /**
147
+ * Deregister this device by the raw push token it already holds (logout /
148
+ * disable notifications). The server derives the storage hash of the token
149
+ * itself, so the caller never reimplements that derivation.
150
+ */
151
+ unregisterDeviceToken(pushToken: string): Promise<void>;
152
+ /**
153
+ * Deregister a device addressed by the server's storage hash of its token.
154
+ * Use `unregisterDeviceToken` when the caller holds the token itself.
155
+ */
96
156
  unregisterDevice(tokenHash: string): Promise<void>;
97
157
  }
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.1",
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
@@ -19,8 +19,10 @@ export interface NotificationRow {
19
19
  created_at: number;
20
20
  title: string;
21
21
  /**
22
- * The short cleartext body of a plaintext send, shown natively. Absent on a
23
- * sealed row (its content lives behind the pointer).
22
+ * The row's visible cleartext line, shown natively: a plaintext send's inline
23
+ * body, or a SEALED send's non-secret `instructions`. A sealed row can and
24
+ * normally should carry one — it is the readable line above the reveal. Never
25
+ * the sealed content itself. Absent when the sender set neither.
24
26
  */
25
27
  body?: string;
26
28
  category: string;
@@ -75,6 +77,37 @@ export interface MarkNotificationStateResponse {
75
77
  updated_at: number;
76
78
  }
77
79
 
80
+ /**
81
+ * States a BULK drain can apply. `archived` is per-row only — it has no GSI for
82
+ * the server to page over, so the bulk endpoint rejects it.
83
+ */
84
+ export type NotificationBulkMarkState = 'seen' | 'read';
85
+
86
+ export interface MarkAllNotificationsStateOptions {
87
+ state: NotificationBulkMarkState;
88
+ /** Cursor from the prior page's response; omit to start at the newest page. */
89
+ cursor?: string;
90
+ }
91
+
92
+ export interface MarkAllNotificationsStateResponse {
93
+ /** Rows marked by THIS page — the server marks one bounded page per call. */
94
+ updated: number;
95
+ /** Cursor for the next page, or null once the inbox is drained. */
96
+ cursor: string | null;
97
+ }
98
+
99
+ /** Total across a full `markAllRead()` drain. */
100
+ export interface MarkAllReadResult {
101
+ updated: number;
102
+ /**
103
+ * True when the drain reached the end of the inbox (the server returned a null
104
+ * cursor). False when it stopped at the page bound with rows still unmarked —
105
+ * call again to continue. Without this a truncated drain is indistinguishable
106
+ * from a completed one.
107
+ */
108
+ complete: boolean;
109
+ }
110
+
78
111
  /** Response to registering/refreshing this device's push token. */
79
112
  export interface RegisterDeviceResponse {
80
113
  success: boolean;
@@ -82,6 +115,13 @@ export interface RegisterDeviceResponse {
82
115
  refreshed: boolean;
83
116
  }
84
117
 
118
+ /**
119
+ * Hard bound on `markAllRead()`'s drain loop: 100 server pages of 100 rows. A
120
+ * cursor that never resolves to null can therefore never spin forever, and an
121
+ * inbox deeper than the bound finishes on a second call (the drain is idempotent).
122
+ */
123
+ const MARK_ALL_MAX_PAGES = 100;
124
+
85
125
  /**
86
126
  * The signed-in user's first-party Notify surface: their own metadata-only inbox
87
127
  * plus this device's push registration. The recipient is ALWAYS derived from the
@@ -130,6 +170,53 @@ export class MobileNotifyClient {
130
170
  return response.data;
131
171
  }
132
172
 
173
+ /**
174
+ * Bulk-mark ONE bounded page of the caller's inbox and return the server's
175
+ * payload verbatim. `cursor` comes back non-null while rows remain — thread it
176
+ * into the next call. `archived` is per-row only (no GSI to page over), so the
177
+ * bulk states are `seen` and `read`.
178
+ *
179
+ * Prefer `markAllRead()` unless the caller needs to own the paging.
180
+ */
181
+ async markAllState(
182
+ options: MarkAllNotificationsStateOptions
183
+ ): Promise<MarkAllNotificationsStateResponse> {
184
+ const body: { state: NotificationBulkMarkState; cursor?: string } = { state: options.state };
185
+ if (options.cursor) body.cursor = options.cursor;
186
+
187
+ const response = await this.http.post<MarkAllNotificationsStateResponse>(
188
+ '/me/notifications/state',
189
+ body
190
+ );
191
+ return response.data;
192
+ }
193
+
194
+ /**
195
+ * Drain the caller's whole inbox to read: loop `markAllState` on the returned
196
+ * cursor until it comes back null, and return the total marked. Marking read
197
+ * clears the unseen marker too, so the badge zeroes with it. Idempotent.
198
+ *
199
+ * `complete` is false when the loop stopped at the page bound with rows still
200
+ * unmarked — an inbox deeper than the bound finishes on a second call. Without
201
+ * it a truncated drain looks exactly like a finished one.
202
+ *
203
+ * An error mid-drain propagates — the pages already marked stay marked, and a
204
+ * re-run picks up the remainder.
205
+ */
206
+ async markAllRead(): Promise<MarkAllReadResult> {
207
+ let cursor: string | undefined;
208
+ let updated = 0;
209
+
210
+ for (let page = 0; page < MARK_ALL_MAX_PAGES; page += 1) {
211
+ const result = await this.markAllState(cursor ? { state: 'read', cursor } : { state: 'read' });
212
+ updated += result?.updated ?? 0;
213
+ if (!result?.cursor) return { updated, complete: true };
214
+ cursor = result.cursor;
215
+ }
216
+
217
+ return { updated, complete: false };
218
+ }
219
+
133
220
  /** Register/refresh this device's Expo push token under the session user. */
134
221
  async registerDevice(pushToken: string): Promise<RegisterDeviceResponse> {
135
222
  const response = await this.http.post<RegisterDeviceResponse>('/me/notifications/devices', {
@@ -138,7 +225,19 @@ export class MobileNotifyClient {
138
225
  return response.data;
139
226
  }
140
227
 
141
- /** Deregister a device token (logout / disable notifications). */
228
+ /**
229
+ * Deregister this device by the raw push token it already holds (logout /
230
+ * disable notifications). The server derives the storage hash of the token
231
+ * itself, so the caller never reimplements that derivation.
232
+ */
233
+ async unregisterDeviceToken(pushToken: string): Promise<void> {
234
+ await this.http.delete('/me/notifications/devices', { body: { token: pushToken } });
235
+ }
236
+
237
+ /**
238
+ * Deregister a device addressed by the server's storage hash of its token.
239
+ * Use `unregisterDeviceToken` when the caller holds the token itself.
240
+ */
142
241
  async unregisterDevice(tokenHash: string): Promise<void> {
143
242
  await this.http.delete(`/me/notifications/devices/${encodeURIComponent(tokenHash)}`);
144
243
  }