@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 +192 -23
- package/dist/index.d.ts +2 -1
- package/dist/index.js +50 -5
- package/dist/index.mjs +50 -5
- package/dist/interfaces/Push.d.ts +54 -0
- package/dist/modules/auth/changePassword.d.ts +23 -0
- package/dist/modules/auth/signOut.d.ts +95 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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<
|
|
103
|
+
export declare function signOut(client: SublayHttpClient, data?: SignOutProps): Promise<SignOutResult>;
|