@sublay/js 7.7.1 → 7.8.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.mts CHANGED
@@ -184,14 +184,182 @@ interface SignInResponse {
184
184
  }
185
185
  declare function signIn(client: SublayHttpClient, data: SignInProps): Promise<SignInResponse>;
186
186
 
187
+ /**
188
+ * Every push event type — the full app-notification type set plus the chat
189
+ * `message` event (push-only). Mirrors the server's `PUSH_EVENT_TYPES` exactly
190
+ * (server `src/constants/push/pushEvents.ts`); these are the only names a
191
+ * `disabledTypes` set may contain.
192
+ */
193
+ declare const PUSH_EVENT_TYPES: readonly ["entity-comment", "comment-reply", "entity-mention", "comment-mention", "entity-reaction", "comment-reaction", "entity-reaction-milestone-specific", "entity-reaction-milestone-total", "comment-reaction-milestone-specific", "comment-reaction-milestone-total", "new-follow", "connection-request", "connection-accepted", "space-membership-approved", "event-invite", "event-updated", "event-cancelled", "message"];
194
+ type PushEventType = (typeof PUSH_EVENT_TYPES)[number];
195
+ /**
196
+ * The four conversation-mute duration choices. Send the CHOICE, never a raw
197
+ * timestamp — the server resolves it and represents "forever" via the explicit
198
+ * `mutedForever` signal on the returned member. Mirrors the server's
199
+ * `MUTE_DURATIONS` (server `src/helpers/push/muteDuration.ts`).
200
+ */
201
+ declare const MUTE_DURATIONS: readonly ["8h", "24h", "1w", "forever"];
202
+ type MuteDuration = (typeof MUTE_DURATIONS)[number];
203
+ /** Response of read/update notification preferences. */
204
+ interface NotificationPreferences {
205
+ disabledTypes: PushEventType[];
206
+ }
207
+ /**
208
+ * A Web Push subscription in its SERIALIZED form — the JSON the server reads,
209
+ * not the live browser object.
210
+ *
211
+ * A `PushSubscription` straight from `PushManager.subscribe()` does NOT satisfy
212
+ * this type. It exposes its keys only through `getKey(name)`; there is no `keys`
213
+ * property to read, so it is not assignable here under strict TypeScript, and
214
+ * passing one through a cast sends a body the server rejects.
215
+ *
216
+ * To produce a value of this type:
217
+ *
218
+ * 1. **Serialize** the subscription with `subscription.toJSON()`.
219
+ * 2. **Confirm** `endpoint`, `keys.p256dh` and `keys.auth` are all present —
220
+ * `PushSubscriptionJSON` types every one of them as optional, while the
221
+ * server requires all three as non-empty strings and 400s otherwise.
222
+ *
223
+ * ```ts
224
+ * const json = subscription.toJSON();
225
+ * if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
226
+ * throw new Error("Incomplete Web Push subscription");
227
+ * }
228
+ * const webPushSubscription: WebPushSubscription = {
229
+ * endpoint: json.endpoint,
230
+ * keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },
231
+ * };
232
+ * ```
233
+ *
234
+ * Mirrors the server's `webPushSubscriptionSchema`
235
+ * (server `src/v7/validation/push-notifications/push-notifications.schema.ts`).
236
+ */
237
+ interface WebPushSubscription {
238
+ endpoint: string;
239
+ keys: {
240
+ p256dh: string;
241
+ auth: string;
242
+ };
243
+ }
244
+ /**
245
+ * Identifies one physical device. Mirrors the server's device-identifier body
246
+ * exactly (server `src/v7/validation/push-notifications/push-notifications.schema.ts`):
247
+ * a provider token for `ios`/`android`, a Web Push subscription for `web`. The
248
+ * union encodes the server's own cross-check — it rejects the same
249
+ * combinations the server's `superRefine` rejects.
250
+ *
251
+ * Passed to `auth.signOut` to unbind that device's push binding in the same
252
+ * transaction as the sign-out.
253
+ */
254
+ type PushDeviceIdentifier = {
255
+ platform: "ios" | "android";
256
+ token: string;
257
+ } | {
258
+ platform: "web";
259
+ subscription: WebPushSubscription;
260
+ };
261
+
187
262
  interface SignOutProps {
188
263
  /**
189
264
  * Defaults to the SDK's stored refresh token (SDK-managed mode). Pass it
190
265
  * explicitly in host-managed mode, where the SDK holds no refresh token.
191
266
  */
192
267
  refreshToken?: string;
268
+ /**
269
+ * Optional. When supplied — and the project has the `push` bundle — the
270
+ * server deletes this user's push binding for that device in the SAME
271
+ * transaction as the token-family destroy: signing out unbinds the device's
272
+ * push, or nothing happens at all.
273
+ *
274
+ * **Needs a refresh token to have any effect.** The unbind is scoped to the
275
+ * `sub` of the refresh token, which is what stops one account removing
276
+ * another account's binding on a shared device — so with no token there is no
277
+ * user to scope it to and the server answers `204` before it ever looks at
278
+ * `pushDevice`. When none can be resolved the field is DROPPED from the
279
+ * request rather than sent into that silent no-op, the sign-out proceeds
280
+ * normally, and the returned `pushUnbindSkipped` says so. Watch for it in
281
+ * **host-managed mode**: such a client is built with `getToken` and no
282
+ * `initialTokens`, and its `setTokens` is inert, so it starts with no refresh
283
+ * token and can never acquire one. Pass `refreshToken` explicitly alongside
284
+ * `pushDevice` there.
285
+ *
286
+ * If the unbind fails the request fails (HTTP 500, code
287
+ * `auth/device-deregistration-failed` or `auth/sign-out-failed`) and NOTHING
288
+ * is committed — the session survives so the caller can retry, and this call
289
+ * rethrows WITHOUT clearing the SDK's local tokens, so the credential that
290
+ * retry needs is still there.
291
+ *
292
+ * That is the ONLY failure that leaves you signed in. Every other one — a
293
+ * `429` from the rate limiter or a quota gate, a `423`, a `503`, a `400` on
294
+ * the body, a dropped connection — never touched a push binding, so it
295
+ * clears the local tokens and RESOLVES: a user must always be able to sign
296
+ * out. See `UNBIND_FAILURE_CODES`.
297
+ *
298
+ * Omit it and the request is byte-identical to before this field existed.
299
+ */
300
+ pushDevice?: PushDeviceIdentifier;
193
301
  }
194
- declare function signOut(client: SublayHttpClient, data?: SignOutProps): Promise<void>;
302
+ /**
303
+ * Why the unbind did not happen. Present only when `pushUnbindSkipped` is
304
+ * `true`, and it exists because THE TWO SKIPS ARE RECOVERED DIFFERENTLY — a
305
+ * caller that cannot tell them apart can only ever pick the wrong remedy for
306
+ * one of them:
307
+ *
308
+ * - `no-refresh-token` — this SDK never asked. Nothing was destroyed either:
309
+ * with no token the server has no session to end, so the account, its
310
+ * credential and its binding are all still there. **Retryable**: re-issue
311
+ * the sign-out with an explicit `refreshToken` and the unbind happens.
312
+ * - `push-unbind-status-unknown` — the server was asked and could not answer.
313
+ * The sign-out itself DID commit, so the credential that would have driven a
314
+ * retry is gone; re-issuing achieves nothing. The binding clears when that
315
+ * account next registers on this device, or when the server prunes the token
316
+ * as undeliverable.
317
+ */
318
+ type PushUnbindSkipReason = "no-refresh-token" | "push-unbind-status-unknown";
319
+ interface SignOutResult {
320
+ /**
321
+ * `true` whenever a `pushDevice` was supplied and the unbind did NOT happen,
322
+ * so this device's push binding for that account may still be live. Either
323
+ * the SDK declined to ask (no refresh token to scope the unbind to) or the
324
+ * server could not answer whether a binding exists; `pushUnbindSkipReason`
325
+ * says which, and the two are recovered differently.
326
+ *
327
+ * `false` means NO SKIP WAS REPORTED — by this SDK or by the server. It is
328
+ * not a promise that no skip happened, and the difference is not academic:
329
+ * the server declines the unbind for several reasons it answers with a plain
330
+ * `204`, and every one of them reads `false` here.
331
+ *
332
+ * - The refresh token is authentic but expired beyond the server's unbind
333
+ * window (90 days past `exp`), so there is no subject to scope the unbind
334
+ * to. Reachable from this SDK by passing an old `refreshToken` explicitly
335
+ * alongside `pushDevice`.
336
+ * - The refresh token is malformed or fails signature verification.
337
+ * - The project has no `push` bundle, so no binding can exist.
338
+ * - The unbind ran and matched zero rows — the binding was already gone, or
339
+ * was never registered under this device key.
340
+ *
341
+ * The server logs each of those under its own reason; only the case where it
342
+ * could not determine whether a binding exists is reported in the response,
343
+ * because that is the one where the binding may still be LIVE and nobody
344
+ * looked. The rest either prove there was nothing to remove or leave a device
345
+ * key the server cannot attribute to a user, and neither gives the caller
346
+ * anything to act on beyond what it already knows.
347
+ *
348
+ * A request that failed short of an unbind attempt reads `false` too — a
349
+ * gate's `429` or `423`, a `503`, a `400`, a dropped connection. Those clear
350
+ * the local tokens and resolve rather than reject (see `UNBIND_FAILURE_CODES`
351
+ * for why), and none of them is a REPORTED skip; they are logged with a
352
+ * warning instead.
353
+ *
354
+ * The sign-out itself is unaffected by the flag in both directions: a user
355
+ * must always be able to sign out, so a skipped unbind never fails the call
356
+ * and never leaves the local tokens in place.
357
+ */
358
+ pushUnbindSkipped: boolean;
359
+ /** Set only when `pushUnbindSkipped` is `true`. See `PushUnbindSkipReason`. */
360
+ pushUnbindSkipReason?: PushUnbindSkipReason;
361
+ }
362
+ declare function signOut(client: SublayHttpClient, data?: SignOutProps): Promise<SignOutResult>;
195
363
 
196
364
  interface RequestNewAccessTokenProps {
197
365
  /** Defaults to the SDK's stored refresh token (SDK-managed mode). */
@@ -234,11 +402,33 @@ interface ChangePasswordProps {
234
402
  /** The user's current password (verified before the change). */
235
403
  password: string;
236
404
  newPassword: string;
405
+ /**
406
+ * Optional. The physical device making this call.
407
+ *
408
+ * A password change also deletes every push binding the user holds, so an
409
+ * intruder's device stops receiving notification content. Naming this device
410
+ * keeps ITS binding — nothing re-binds a device from its live session, so
411
+ * without this the handset the user just changed their password on goes
412
+ * quiet until the app cold-starts or switches accounts.
413
+ *
414
+ * Same value passed to `push.register` / `auth.signOut`. Omit it and every
415
+ * binding for the user goes, this device included.
416
+ */
417
+ pushDevice?: PushDeviceIdentifier;
237
418
  }
238
419
  interface ChangePasswordResponse {
239
420
  success: boolean;
240
421
  message: string;
241
422
  }
423
+ /**
424
+ * Change the signed-in user's password.
425
+ *
426
+ * Ends every OTHER session for that user — every other device must sign in
427
+ * again with the new password — while the session making this call survives.
428
+ * The server identifies that session from the access token this request is
429
+ * already authenticated with, so nothing about the caller's session has to be
430
+ * sent.
431
+ */
242
432
  declare function changePassword(client: SublayHttpClient, data: ChangePasswordProps): Promise<ChangePasswordResponse>;
243
433
 
244
434
  interface SetPasswordProps {
@@ -3244,27 +3434,6 @@ declare function markAsRead(client: SublayHttpClient, data: MarkAsReadProps): Pr
3244
3434
  message: string;
3245
3435
  }>;
3246
3436
 
3247
- /**
3248
- * Every push event type — the full app-notification type set plus the chat
3249
- * `message` event (push-only). Mirrors the server's `PUSH_EVENT_TYPES` exactly
3250
- * (server `src/constants/push/pushEvents.ts`); these are the only names a
3251
- * `disabledTypes` set may contain.
3252
- */
3253
- declare const PUSH_EVENT_TYPES: readonly ["entity-comment", "comment-reply", "entity-mention", "comment-mention", "entity-reaction", "comment-reaction", "entity-reaction-milestone-specific", "entity-reaction-milestone-total", "comment-reaction-milestone-specific", "comment-reaction-milestone-total", "new-follow", "connection-request", "connection-accepted", "space-membership-approved", "event-invite", "event-updated", "event-cancelled", "message"];
3254
- type PushEventType = (typeof PUSH_EVENT_TYPES)[number];
3255
- /**
3256
- * The four conversation-mute duration choices. Send the CHOICE, never a raw
3257
- * timestamp — the server resolves it and represents "forever" via the explicit
3258
- * `mutedForever` signal on the returned member. Mirrors the server's
3259
- * `MUTE_DURATIONS` (server `src/helpers/push/muteDuration.ts`).
3260
- */
3261
- declare const MUTE_DURATIONS: readonly ["8h", "24h", "1w", "forever"];
3262
- type MuteDuration = (typeof MUTE_DURATIONS)[number];
3263
- /** Response of read/update notification preferences. */
3264
- interface NotificationPreferences {
3265
- disabledTypes: PushEventType[];
3266
- }
3267
-
3268
3437
  interface MuteConversationProps {
3269
3438
  conversationId: string;
3270
3439
  /**
@@ -3873,4 +4042,4 @@ declare class SublayClient {
3873
4042
  clearTokens(): void;
3874
4043
  }
3875
4044
 
3876
- export { type AuthTokens, type BulkDeleteProps, type BulkDeleteResult, type ClientConfig, type DbFilter, type DbFilterOperator, type DeleteResult, MUTE_DURATIONS, type MuteConversationProps, type MuteConversationResponse, type MuteDuration, type NotificationPreferences, PUSH_EVENT_TYPES, type PaginatedResponse, type PaginationMetadata, type PushEventType, type RemoveWorkspaceMemberFromSubtreeResponse, type SkippedWorkspace, type SpaceReputationContextParams, type SpaceReputationUserParams, SublayClient, type TableAccessor, type TableQuery, type TableRow, type UpdateNotificationPreferencesProps, type UserSearchParams, type Workspace, type WorkspaceAuthority, type WorkspaceAuthorityReason, type WorkspaceAuthorityReasonDetail, type WorkspaceCapability, type WorkspaceInvitation, type WorkspaceInvitationStatus, type WorkspaceMember, type WorkspaceMemberStanding, type WorkspaceRosterCountsResponse, type WorkspaceRosterEntry, type WorkspaceRosterReason, type WorkspaceRosterResponse, type WorkspaceStandingUser };
4045
+ export { type AuthTokens, type BulkDeleteProps, type BulkDeleteResult, type ClientConfig, type DbFilter, type DbFilterOperator, type DeleteResult, MUTE_DURATIONS, type MuteConversationProps, type MuteConversationResponse, type MuteDuration, type NotificationPreferences, PUSH_EVENT_TYPES, type PaginatedResponse, type PaginationMetadata, type PushDeviceIdentifier, type PushEventType, type PushUnbindSkipReason, type RemoveWorkspaceMemberFromSubtreeResponse, type SignOutResult, type SkippedWorkspace, type SpaceReputationContextParams, type SpaceReputationUserParams, SublayClient, type TableAccessor, type TableQuery, type TableRow, type UpdateNotificationPreferencesProps, type UserSearchParams, type WebPushSubscription, type Workspace, type WorkspaceAuthority, type WorkspaceAuthorityReason, type WorkspaceAuthorityReasonDetail, type WorkspaceCapability, type WorkspaceInvitation, type WorkspaceInvitationStatus, type WorkspaceMember, type WorkspaceMemberStanding, type WorkspaceRosterCountsResponse, type WorkspaceRosterEntry, type WorkspaceRosterReason, type WorkspaceRosterResponse, type WorkspaceStandingUser };
package/dist/index.d.ts CHANGED
@@ -58,7 +58,8 @@ export type { SpaceReputationContextParams, SpaceReputationUserParams, } from ".
58
58
  export type { UserSearchParams } from "./interfaces/UserSearch";
59
59
  export type { Workspace, WorkspaceMember, WorkspaceInvitation, WorkspaceCapability, WorkspaceInvitationStatus, WorkspaceAuthorityReason, WorkspaceAuthorityReasonDetail, WorkspaceRosterReason, WorkspaceRosterEntry, WorkspaceRosterResponse, WorkspaceRosterCountsResponse, WorkspaceStandingUser, WorkspaceMemberStanding, WorkspaceAuthority, RemoveWorkspaceMemberFromSubtreeResponse, SkippedWorkspace, } from "./interfaces/Workspace";
60
60
  export { PUSH_EVENT_TYPES, MUTE_DURATIONS } from "./interfaces/Push";
61
- export type { PushEventType, MuteDuration, NotificationPreferences, } from "./interfaces/Push";
61
+ export type { PushEventType, MuteDuration, NotificationPreferences, PushDeviceIdentifier, WebPushSubscription, } from "./interfaces/Push";
62
+ export type { SignOutResult, PushUnbindSkipReason, } from "./modules/auth/signOut";
62
63
  export type { UpdateNotificationPreferencesProps } from "./modules/push/updateNotificationPreferences";
63
64
  export type { MuteConversationProps, MuteConversationResponse, } from "./modules/chat/muteConversation";
64
65
  export type { TableAccessor, TableRow, TableQuery, DbFilter, DbFilterOperator, BulkDeleteProps, DeleteResult, BulkDeleteResult, } from "./interfaces/Table";
package/dist/index.js CHANGED
@@ -212,13 +212,54 @@ async function signIn(client, data) {
212
212
  }
213
213
 
214
214
  // src/modules/auth/signOut.ts
215
+ var PUSH_UNBIND_STATUS_UNKNOWN_CODE = "auth/push-unbind-status-unknown";
216
+ var UNBIND_FAILURE_CODES = [
217
+ "auth/device-deregistration-failed",
218
+ "auth/sign-out-failed"
219
+ ];
220
+ function isUnbindFailure(error) {
221
+ const code = error?.response?.data?.code;
222
+ return typeof code === "string" && UNBIND_FAILURE_CODES.includes(code);
223
+ }
215
224
  async function signOut(client, data) {
216
225
  const refreshToken = data?.refreshToken ?? client.getRefreshToken();
217
- await client.projectInstance.post(
218
- "/auth/sign-out",
219
- refreshToken ? { refreshToken } : {}
220
- );
226
+ const localUnbindSkipped = Boolean(data?.pushDevice) && !refreshToken;
227
+ const body = refreshToken ? { refreshToken } : {};
228
+ if (data?.pushDevice && !localUnbindSkipped) body.pushDevice = data.pushDevice;
229
+ let response;
230
+ try {
231
+ response = await client.projectInstance.post(
232
+ "/auth/sign-out",
233
+ body
234
+ );
235
+ } catch (error) {
236
+ if (isUnbindFailure(error)) throw error;
237
+ console.warn(
238
+ "signOut: the server sign-out did not complete, so the session may still be live on the server until its refresh token expires. Signing out locally anyway \u2014 a user must always be able to sign out; only a refused push unbind keeps the local session, because that is the one failure that leaves a binding to retry against.",
239
+ error
240
+ );
241
+ }
221
242
  client.clearTokens();
243
+ const serverUnbindSkipped = typeof response?.data === "object" && response.data !== null && response.data.code === PUSH_UNBIND_STATUS_UNKNOWN_CODE;
244
+ if (localUnbindSkipped) {
245
+ console.warn(
246
+ "signOut: signed out, but the push unbind was SKIPPED \u2014 `pushDevice` was supplied and no refresh token could be resolved (none stored and none provided). The unbind is scoped to the refresh token's subject, so the server would have unbound nothing. This device keeps receiving that account's notifications until something else removes the binding. Pass `refreshToken` alongside `pushDevice` \u2014 required in host-managed mode, where the SDK stores no refresh token."
247
+ );
248
+ return {
249
+ pushUnbindSkipped: true,
250
+ pushUnbindSkipReason: "no-refresh-token"
251
+ };
252
+ }
253
+ if (serverUnbindSkipped) {
254
+ console.warn(
255
+ "signOut: signed out, but the push unbind was SKIPPED \u2014 the server could not determine whether this project has push devices, so it did not attempt one. This device may still be bound to that account and can keep receiving its notifications. The sign-out itself committed, so there is no credential left to retry with: the binding clears when that account next registers on this device, or when the server prunes the token as undeliverable."
256
+ );
257
+ return {
258
+ pushUnbindSkipped: true,
259
+ pushUnbindSkipReason: "push-unbind-status-unknown"
260
+ };
261
+ }
262
+ return { pushUnbindSkipped: false };
222
263
  }
223
264
 
224
265
  // src/modules/auth/requestNewAccessToken.ts
@@ -266,7 +307,11 @@ async function resetPassword(client, data) {
266
307
  async function changePassword(client, data) {
267
308
  const response = await client.projectInstance.post(
268
309
  "/auth/change-password",
269
- data
310
+ {
311
+ password: data.password,
312
+ newPassword: data.newPassword,
313
+ ...data.pushDevice ? { pushDevice: data.pushDevice } : {}
314
+ }
270
315
  );
271
316
  return response.data;
272
317
  }
package/dist/index.mjs CHANGED
@@ -180,13 +180,54 @@ async function signIn(client, data) {
180
180
  }
181
181
 
182
182
  // src/modules/auth/signOut.ts
183
+ var PUSH_UNBIND_STATUS_UNKNOWN_CODE = "auth/push-unbind-status-unknown";
184
+ var UNBIND_FAILURE_CODES = [
185
+ "auth/device-deregistration-failed",
186
+ "auth/sign-out-failed"
187
+ ];
188
+ function isUnbindFailure(error) {
189
+ const code = error?.response?.data?.code;
190
+ return typeof code === "string" && UNBIND_FAILURE_CODES.includes(code);
191
+ }
183
192
  async function signOut(client, data) {
184
193
  const refreshToken = data?.refreshToken ?? client.getRefreshToken();
185
- await client.projectInstance.post(
186
- "/auth/sign-out",
187
- refreshToken ? { refreshToken } : {}
188
- );
194
+ const localUnbindSkipped = Boolean(data?.pushDevice) && !refreshToken;
195
+ const body = refreshToken ? { refreshToken } : {};
196
+ if (data?.pushDevice && !localUnbindSkipped) body.pushDevice = data.pushDevice;
197
+ let response;
198
+ try {
199
+ response = await client.projectInstance.post(
200
+ "/auth/sign-out",
201
+ body
202
+ );
203
+ } catch (error) {
204
+ if (isUnbindFailure(error)) throw error;
205
+ console.warn(
206
+ "signOut: the server sign-out did not complete, so the session may still be live on the server until its refresh token expires. Signing out locally anyway \u2014 a user must always be able to sign out; only a refused push unbind keeps the local session, because that is the one failure that leaves a binding to retry against.",
207
+ error
208
+ );
209
+ }
189
210
  client.clearTokens();
211
+ const serverUnbindSkipped = typeof response?.data === "object" && response.data !== null && response.data.code === PUSH_UNBIND_STATUS_UNKNOWN_CODE;
212
+ if (localUnbindSkipped) {
213
+ console.warn(
214
+ "signOut: signed out, but the push unbind was SKIPPED \u2014 `pushDevice` was supplied and no refresh token could be resolved (none stored and none provided). The unbind is scoped to the refresh token's subject, so the server would have unbound nothing. This device keeps receiving that account's notifications until something else removes the binding. Pass `refreshToken` alongside `pushDevice` \u2014 required in host-managed mode, where the SDK stores no refresh token."
215
+ );
216
+ return {
217
+ pushUnbindSkipped: true,
218
+ pushUnbindSkipReason: "no-refresh-token"
219
+ };
220
+ }
221
+ if (serverUnbindSkipped) {
222
+ console.warn(
223
+ "signOut: signed out, but the push unbind was SKIPPED \u2014 the server could not determine whether this project has push devices, so it did not attempt one. This device may still be bound to that account and can keep receiving its notifications. The sign-out itself committed, so there is no credential left to retry with: the binding clears when that account next registers on this device, or when the server prunes the token as undeliverable."
224
+ );
225
+ return {
226
+ pushUnbindSkipped: true,
227
+ pushUnbindSkipReason: "push-unbind-status-unknown"
228
+ };
229
+ }
230
+ return { pushUnbindSkipped: false };
190
231
  }
191
232
 
192
233
  // src/modules/auth/requestNewAccessToken.ts
@@ -234,7 +275,11 @@ async function resetPassword(client, data) {
234
275
  async function changePassword(client, data) {
235
276
  const response = await client.projectInstance.post(
236
277
  "/auth/change-password",
237
- data
278
+ {
279
+ password: data.password,
280
+ newPassword: data.newPassword,
281
+ ...data.pushDevice ? { pushDevice: data.pushDevice } : {}
282
+ }
238
283
  );
239
284
  return response.data;
240
285
  }
@@ -18,3 +18,57 @@ export type MuteDuration = (typeof MUTE_DURATIONS)[number];
18
18
  export interface NotificationPreferences {
19
19
  disabledTypes: PushEventType[];
20
20
  }
21
+ /**
22
+ * A Web Push subscription in its SERIALIZED form — the JSON the server reads,
23
+ * not the live browser object.
24
+ *
25
+ * A `PushSubscription` straight from `PushManager.subscribe()` does NOT satisfy
26
+ * this type. It exposes its keys only through `getKey(name)`; there is no `keys`
27
+ * property to read, so it is not assignable here under strict TypeScript, and
28
+ * passing one through a cast sends a body the server rejects.
29
+ *
30
+ * To produce a value of this type:
31
+ *
32
+ * 1. **Serialize** the subscription with `subscription.toJSON()`.
33
+ * 2. **Confirm** `endpoint`, `keys.p256dh` and `keys.auth` are all present —
34
+ * `PushSubscriptionJSON` types every one of them as optional, while the
35
+ * server requires all three as non-empty strings and 400s otherwise.
36
+ *
37
+ * ```ts
38
+ * const json = subscription.toJSON();
39
+ * if (!json.endpoint || !json.keys?.p256dh || !json.keys?.auth) {
40
+ * throw new Error("Incomplete Web Push subscription");
41
+ * }
42
+ * const webPushSubscription: WebPushSubscription = {
43
+ * endpoint: json.endpoint,
44
+ * keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },
45
+ * };
46
+ * ```
47
+ *
48
+ * Mirrors the server's `webPushSubscriptionSchema`
49
+ * (server `src/v7/validation/push-notifications/push-notifications.schema.ts`).
50
+ */
51
+ export interface WebPushSubscription {
52
+ endpoint: string;
53
+ keys: {
54
+ p256dh: string;
55
+ auth: string;
56
+ };
57
+ }
58
+ /**
59
+ * Identifies one physical device. Mirrors the server's device-identifier body
60
+ * exactly (server `src/v7/validation/push-notifications/push-notifications.schema.ts`):
61
+ * a provider token for `ios`/`android`, a Web Push subscription for `web`. The
62
+ * union encodes the server's own cross-check — it rejects the same
63
+ * combinations the server's `superRefine` rejects.
64
+ *
65
+ * Passed to `auth.signOut` to unbind that device's push binding in the same
66
+ * transaction as the sign-out.
67
+ */
68
+ export type PushDeviceIdentifier = {
69
+ platform: "ios" | "android";
70
+ token: string;
71
+ } | {
72
+ platform: "web";
73
+ subscription: WebPushSubscription;
74
+ };
@@ -1,11 +1,34 @@
1
1
  import { SublayHttpClient } from "../../core/client";
2
+ import { PushDeviceIdentifier } from "../../interfaces/Push";
2
3
  export interface ChangePasswordProps {
3
4
  /** The user's current password (verified before the change). */
4
5
  password: string;
5
6
  newPassword: string;
7
+ /**
8
+ * Optional. The physical device making this call.
9
+ *
10
+ * A password change also deletes every push binding the user holds, so an
11
+ * intruder's device stops receiving notification content. Naming this device
12
+ * keeps ITS binding — nothing re-binds a device from its live session, so
13
+ * without this the handset the user just changed their password on goes
14
+ * quiet until the app cold-starts or switches accounts.
15
+ *
16
+ * Same value passed to `push.register` / `auth.signOut`. Omit it and every
17
+ * binding for the user goes, this device included.
18
+ */
19
+ pushDevice?: PushDeviceIdentifier;
6
20
  }
7
21
  export interface ChangePasswordResponse {
8
22
  success: boolean;
9
23
  message: string;
10
24
  }
25
+ /**
26
+ * Change the signed-in user's password.
27
+ *
28
+ * Ends every OTHER session for that user — every other device must sign in
29
+ * again with the new password — while the session making this call survives.
30
+ * The server identifies that session from the access token this request is
31
+ * already authenticated with, so nothing about the caller's session has to be
32
+ * sent.
33
+ */
11
34
  export declare function changePassword(client: SublayHttpClient, data: ChangePasswordProps): Promise<ChangePasswordResponse>;
@@ -1,9 +1,103 @@
1
1
  import { SublayHttpClient } from "../../core/client";
2
+ import { PushDeviceIdentifier } from "../../interfaces/Push";
2
3
  export interface SignOutProps {
3
4
  /**
4
5
  * Defaults to the SDK's stored refresh token (SDK-managed mode). Pass it
5
6
  * explicitly in host-managed mode, where the SDK holds no refresh token.
6
7
  */
7
8
  refreshToken?: string;
9
+ /**
10
+ * Optional. When supplied — and the project has the `push` bundle — the
11
+ * server deletes this user's push binding for that device in the SAME
12
+ * transaction as the token-family destroy: signing out unbinds the device's
13
+ * push, or nothing happens at all.
14
+ *
15
+ * **Needs a refresh token to have any effect.** The unbind is scoped to the
16
+ * `sub` of the refresh token, which is what stops one account removing
17
+ * another account's binding on a shared device — so with no token there is no
18
+ * user to scope it to and the server answers `204` before it ever looks at
19
+ * `pushDevice`. When none can be resolved the field is DROPPED from the
20
+ * request rather than sent into that silent no-op, the sign-out proceeds
21
+ * normally, and the returned `pushUnbindSkipped` says so. Watch for it in
22
+ * **host-managed mode**: such a client is built with `getToken` and no
23
+ * `initialTokens`, and its `setTokens` is inert, so it starts with no refresh
24
+ * token and can never acquire one. Pass `refreshToken` explicitly alongside
25
+ * `pushDevice` there.
26
+ *
27
+ * If the unbind fails the request fails (HTTP 500, code
28
+ * `auth/device-deregistration-failed` or `auth/sign-out-failed`) and NOTHING
29
+ * is committed — the session survives so the caller can retry, and this call
30
+ * rethrows WITHOUT clearing the SDK's local tokens, so the credential that
31
+ * retry needs is still there.
32
+ *
33
+ * That is the ONLY failure that leaves you signed in. Every other one — a
34
+ * `429` from the rate limiter or a quota gate, a `423`, a `503`, a `400` on
35
+ * the body, a dropped connection — never touched a push binding, so it
36
+ * clears the local tokens and RESOLVES: a user must always be able to sign
37
+ * out. See `UNBIND_FAILURE_CODES`.
38
+ *
39
+ * Omit it and the request is byte-identical to before this field existed.
40
+ */
41
+ pushDevice?: PushDeviceIdentifier;
42
+ }
43
+ /**
44
+ * Why the unbind did not happen. Present only when `pushUnbindSkipped` is
45
+ * `true`, and it exists because THE TWO SKIPS ARE RECOVERED DIFFERENTLY — a
46
+ * caller that cannot tell them apart can only ever pick the wrong remedy for
47
+ * one of them:
48
+ *
49
+ * - `no-refresh-token` — this SDK never asked. Nothing was destroyed either:
50
+ * with no token the server has no session to end, so the account, its
51
+ * credential and its binding are all still there. **Retryable**: re-issue
52
+ * the sign-out with an explicit `refreshToken` and the unbind happens.
53
+ * - `push-unbind-status-unknown` — the server was asked and could not answer.
54
+ * The sign-out itself DID commit, so the credential that would have driven a
55
+ * retry is gone; re-issuing achieves nothing. The binding clears when that
56
+ * account next registers on this device, or when the server prunes the token
57
+ * as undeliverable.
58
+ */
59
+ export type PushUnbindSkipReason = "no-refresh-token" | "push-unbind-status-unknown";
60
+ export interface SignOutResult {
61
+ /**
62
+ * `true` whenever a `pushDevice` was supplied and the unbind did NOT happen,
63
+ * so this device's push binding for that account may still be live. Either
64
+ * the SDK declined to ask (no refresh token to scope the unbind to) or the
65
+ * server could not answer whether a binding exists; `pushUnbindSkipReason`
66
+ * says which, and the two are recovered differently.
67
+ *
68
+ * `false` means NO SKIP WAS REPORTED — by this SDK or by the server. It is
69
+ * not a promise that no skip happened, and the difference is not academic:
70
+ * the server declines the unbind for several reasons it answers with a plain
71
+ * `204`, and every one of them reads `false` here.
72
+ *
73
+ * - The refresh token is authentic but expired beyond the server's unbind
74
+ * window (90 days past `exp`), so there is no subject to scope the unbind
75
+ * to. Reachable from this SDK by passing an old `refreshToken` explicitly
76
+ * alongside `pushDevice`.
77
+ * - The refresh token is malformed or fails signature verification.
78
+ * - The project has no `push` bundle, so no binding can exist.
79
+ * - The unbind ran and matched zero rows — the binding was already gone, or
80
+ * was never registered under this device key.
81
+ *
82
+ * The server logs each of those under its own reason; only the case where it
83
+ * could not determine whether a binding exists is reported in the response,
84
+ * because that is the one where the binding may still be LIVE and nobody
85
+ * looked. The rest either prove there was nothing to remove or leave a device
86
+ * key the server cannot attribute to a user, and neither gives the caller
87
+ * anything to act on beyond what it already knows.
88
+ *
89
+ * A request that failed short of an unbind attempt reads `false` too — a
90
+ * gate's `429` or `423`, a `503`, a `400`, a dropped connection. Those clear
91
+ * the local tokens and resolve rather than reject (see `UNBIND_FAILURE_CODES`
92
+ * for why), and none of them is a REPORTED skip; they are logged with a
93
+ * warning instead.
94
+ *
95
+ * The sign-out itself is unaffected by the flag in both directions: a user
96
+ * must always be able to sign out, so a skipped unbind never fails the call
97
+ * and never leaves the local tokens in place.
98
+ */
99
+ pushUnbindSkipped: boolean;
100
+ /** Set only when `pushUnbindSkipped` is `true`. See `PushUnbindSkipReason`. */
101
+ pushUnbindSkipReason?: PushUnbindSkipReason;
8
102
  }
9
- export declare function signOut(client: SublayHttpClient, data?: SignOutProps): Promise<void>;
103
+ export declare function signOut(client: SublayHttpClient, data?: SignOutProps): Promise<SignOutResult>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sublay/js",
3
- "version": "7.7.1",
3
+ "version": "7.8.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "files": [