@heyatom/bastion-client 0.4.0 → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  import type { BastionHttp } from './bastion-http.js';
2
2
  import { type BastionLogger } from './types.js';
3
- import type { BastionAgeVerificationResult, BastionAuthMethods, BastionClientContext, BastionEmailChangeRequest, BastionLinkTicket, BastionLoginResult, BastionMe, BastionProfile, BastionProfileUpdate, BastionRegisterInput, BastionSession, BastionSocialAccount, BastionTokenPair, BastionTwoFactorConfirmed, BastionTwoFactorSetup, BastionTwoFactorStatus, BastionUserAuditEventsPage } from './api-types.js';
3
+ import type { BastionAgeVerificationResult, BastionAuthMethods, BastionClientContext, BastionEmailChangeRequest, BastionLinkCompleted, BastionLinkTicket, BastionLoginResult, BastionMe, BastionProfile, BastionProfileUpdate, BastionRegisterInput, BastionSession, BastionSocialAccount, BastionTokenPair, BastionTwoFactorConfirmed, BastionTwoFactorSetup, BastionTwoFactorStatus, BastionUserAuditEventsPage } from './api-types.js';
4
4
  export interface BastionApiClientOptions {
5
5
  http: BastionHttp;
6
6
  /** This app's slug — sent as `appSlug` on every user-facing call. */
@@ -75,6 +75,13 @@ export declare class BastionApiClient {
75
75
  }>;
76
76
  listSocialAccounts(accessToken: string): Promise<BastionSocialAccount[]>;
77
77
  createSocialAccountLinkTicket(accessToken: string, provider: string): Promise<BastionLinkTicket>;
78
+ /**
79
+ * Second step of a link: spends the one-time `linkCode` Bastion put on the
80
+ * redirect to `/auth/callback`. Pass the signed-in user's own token — Bastion
81
+ * links only if its `sub` is the user the ticket was issued to (403
82
+ * `OAUTH_LINK_USER_MISMATCH` otherwise; the code is spent either way).
83
+ */
84
+ completeSocialAccountLink(accessToken: string, code: string): Promise<BastionLinkCompleted>;
78
85
  unlinkSocialAccount(accessToken: string, provider: string): Promise<void>;
79
86
  updateSocialAccountSyncAvatar(accessToken: string, provider: string, syncAvatar: boolean): Promise<BastionSocialAccount>;
80
87
  resendVerification(email: string, ctx?: BastionClientContext): Promise<void>;
@@ -156,6 +156,15 @@ class BastionApiClient {
156
156
  createSocialAccountLinkTicket(accessToken, provider) {
157
157
  return this.call('POST', `/auth/me/social-accounts/${encodeURIComponent(provider)}/link-ticket`, undefined, { token: accessToken });
158
158
  }
159
+ /**
160
+ * Second step of a link: spends the one-time `linkCode` Bastion put on the
161
+ * redirect to `/auth/callback`. Pass the signed-in user's own token — Bastion
162
+ * links only if its `sub` is the user the ticket was issued to (403
163
+ * `OAUTH_LINK_USER_MISMATCH` otherwise; the code is spent either way).
164
+ */
165
+ completeSocialAccountLink(accessToken, code) {
166
+ return this.call('POST', '/auth/me/social-accounts/link/complete', { code }, { token: accessToken });
167
+ }
159
168
  unlinkSocialAccount(accessToken, provider) {
160
169
  return this.call('DELETE', `/auth/me/social-accounts/${encodeURIComponent(provider)}`, undefined, { token: accessToken });
161
170
  }
@@ -96,6 +96,8 @@ export interface BastionClientContext {
96
96
  }
97
97
  export interface BastionSocialAccount {
98
98
  provider: string;
99
+ /** The provider's own id for the account (e.g. the Twitch user id), as verified by the OAuth flow. */
100
+ providerId: string;
99
101
  username: string | null;
100
102
  email: string | null;
101
103
  avatar: string | null;
@@ -119,6 +121,10 @@ export interface BastionAuthMethods {
119
121
  export interface BastionLinkTicket {
120
122
  url: string;
121
123
  }
124
+ /** `POST /auth/me/social-accounts/link/complete` — the provider now linked (lowercase slug). */
125
+ export interface BastionLinkCompleted {
126
+ provider: string;
127
+ }
122
128
  /** One row of `GET /auth/me/events` — the user-facing projection, deliberately without `ip`/`userAgent`. */
123
129
  export interface BastionUserAuditEvent {
124
130
  id: string;
@@ -29,6 +29,8 @@ export declare const BastionErrorCode: {
29
29
  readonly OAUTH_EMAIL_UNVERIFIED: "OAUTH_EMAIL_UNVERIFIED";
30
30
  readonly OAUTH_EMAIL_REQUIRED: "OAUTH_EMAIL_REQUIRED";
31
31
  readonly OAUTH_NO_ACCOUNT: "OAUTH_NO_ACCOUNT";
32
+ /** `completeSocialAccountLink` called with a token that is not the link ticket's owner. The code is spent; start the link again. */
33
+ readonly OAUTH_LINK_USER_MISMATCH: "OAUTH_LINK_USER_MISMATCH";
32
34
  readonly UNAVAILABLE: "UNAVAILABLE";
33
35
  readonly UPSTREAM: "UPSTREAM";
34
36
  };
@@ -37,6 +37,8 @@ exports.BastionErrorCode = {
37
37
  OAUTH_EMAIL_UNVERIFIED: 'OAUTH_EMAIL_UNVERIFIED',
38
38
  OAUTH_EMAIL_REQUIRED: 'OAUTH_EMAIL_REQUIRED',
39
39
  OAUTH_NO_ACCOUNT: 'OAUTH_NO_ACCOUNT',
40
+ /** `completeSocialAccountLink` called with a token that is not the link ticket's owner. The code is spent; start the link again. */
41
+ OAUTH_LINK_USER_MISMATCH: 'OAUTH_LINK_USER_MISMATCH',
40
42
  UNAVAILABLE: 'UNAVAILABLE',
41
43
  UPSTREAM: 'UPSTREAM',
42
44
  };
@@ -49,6 +51,7 @@ const CODE_MESSAGES = new Set([
49
51
  exports.BastionErrorCode.OAUTH_EMAIL_UNVERIFIED,
50
52
  exports.BastionErrorCode.OAUTH_EMAIL_REQUIRED,
51
53
  exports.BastionErrorCode.OAUTH_NO_ACCOUNT,
54
+ exports.BastionErrorCode.OAUTH_LINK_USER_MISMATCH,
52
55
  ]);
53
56
  const APP_CONTEXT_RE = /app not found|tenant not found|tenant is inactive|no tenants configured|tenantslug is required/i;
54
57
  const OAUTH_CODE_RE = /already used code|code expired|code\/app mismatch/i;
@@ -1,6 +1,6 @@
1
1
  import type { BastionHttp } from './bastion-http.js';
2
2
  import { type BastionLogger } from './types.js';
3
- import type { BastionAgeVerificationResult, BastionAuthMethods, BastionClientContext, BastionEmailChangeRequest, BastionLinkTicket, BastionLoginResult, BastionMe, BastionProfile, BastionProfileUpdate, BastionRegisterInput, BastionSession, BastionSocialAccount, BastionTokenPair, BastionTwoFactorConfirmed, BastionTwoFactorSetup, BastionTwoFactorStatus, BastionUserAuditEventsPage } from './api-types.js';
3
+ import type { BastionAgeVerificationResult, BastionAuthMethods, BastionClientContext, BastionEmailChangeRequest, BastionLinkCompleted, BastionLinkTicket, BastionLoginResult, BastionMe, BastionProfile, BastionProfileUpdate, BastionRegisterInput, BastionSession, BastionSocialAccount, BastionTokenPair, BastionTwoFactorConfirmed, BastionTwoFactorSetup, BastionTwoFactorStatus, BastionUserAuditEventsPage } from './api-types.js';
4
4
  export interface BastionApiClientOptions {
5
5
  http: BastionHttp;
6
6
  /** This app's slug — sent as `appSlug` on every user-facing call. */
@@ -75,6 +75,13 @@ export declare class BastionApiClient {
75
75
  }>;
76
76
  listSocialAccounts(accessToken: string): Promise<BastionSocialAccount[]>;
77
77
  createSocialAccountLinkTicket(accessToken: string, provider: string): Promise<BastionLinkTicket>;
78
+ /**
79
+ * Second step of a link: spends the one-time `linkCode` Bastion put on the
80
+ * redirect to `/auth/callback`. Pass the signed-in user's own token — Bastion
81
+ * links only if its `sub` is the user the ticket was issued to (403
82
+ * `OAUTH_LINK_USER_MISMATCH` otherwise; the code is spent either way).
83
+ */
84
+ completeSocialAccountLink(accessToken: string, code: string): Promise<BastionLinkCompleted>;
78
85
  unlinkSocialAccount(accessToken: string, provider: string): Promise<void>;
79
86
  updateSocialAccountSyncAvatar(accessToken: string, provider: string, syncAvatar: boolean): Promise<BastionSocialAccount>;
80
87
  resendVerification(email: string, ctx?: BastionClientContext): Promise<void>;
@@ -153,6 +153,15 @@ export class BastionApiClient {
153
153
  createSocialAccountLinkTicket(accessToken, provider) {
154
154
  return this.call('POST', `/auth/me/social-accounts/${encodeURIComponent(provider)}/link-ticket`, undefined, { token: accessToken });
155
155
  }
156
+ /**
157
+ * Second step of a link: spends the one-time `linkCode` Bastion put on the
158
+ * redirect to `/auth/callback`. Pass the signed-in user's own token — Bastion
159
+ * links only if its `sub` is the user the ticket was issued to (403
160
+ * `OAUTH_LINK_USER_MISMATCH` otherwise; the code is spent either way).
161
+ */
162
+ completeSocialAccountLink(accessToken, code) {
163
+ return this.call('POST', '/auth/me/social-accounts/link/complete', { code }, { token: accessToken });
164
+ }
156
165
  unlinkSocialAccount(accessToken, provider) {
157
166
  return this.call('DELETE', `/auth/me/social-accounts/${encodeURIComponent(provider)}`, undefined, { token: accessToken });
158
167
  }
@@ -96,6 +96,8 @@ export interface BastionClientContext {
96
96
  }
97
97
  export interface BastionSocialAccount {
98
98
  provider: string;
99
+ /** The provider's own id for the account (e.g. the Twitch user id), as verified by the OAuth flow. */
100
+ providerId: string;
99
101
  username: string | null;
100
102
  email: string | null;
101
103
  avatar: string | null;
@@ -119,6 +121,10 @@ export interface BastionAuthMethods {
119
121
  export interface BastionLinkTicket {
120
122
  url: string;
121
123
  }
124
+ /** `POST /auth/me/social-accounts/link/complete` — the provider now linked (lowercase slug). */
125
+ export interface BastionLinkCompleted {
126
+ provider: string;
127
+ }
122
128
  /** One row of `GET /auth/me/events` — the user-facing projection, deliberately without `ip`/`userAgent`. */
123
129
  export interface BastionUserAuditEvent {
124
130
  id: string;
@@ -29,6 +29,8 @@ export declare const BastionErrorCode: {
29
29
  readonly OAUTH_EMAIL_UNVERIFIED: "OAUTH_EMAIL_UNVERIFIED";
30
30
  readonly OAUTH_EMAIL_REQUIRED: "OAUTH_EMAIL_REQUIRED";
31
31
  readonly OAUTH_NO_ACCOUNT: "OAUTH_NO_ACCOUNT";
32
+ /** `completeSocialAccountLink` called with a token that is not the link ticket's owner. The code is spent; start the link again. */
33
+ readonly OAUTH_LINK_USER_MISMATCH: "OAUTH_LINK_USER_MISMATCH";
32
34
  readonly UNAVAILABLE: "UNAVAILABLE";
33
35
  readonly UPSTREAM: "UPSTREAM";
34
36
  };
@@ -32,6 +32,8 @@ export const BastionErrorCode = {
32
32
  OAUTH_EMAIL_UNVERIFIED: 'OAUTH_EMAIL_UNVERIFIED',
33
33
  OAUTH_EMAIL_REQUIRED: 'OAUTH_EMAIL_REQUIRED',
34
34
  OAUTH_NO_ACCOUNT: 'OAUTH_NO_ACCOUNT',
35
+ /** `completeSocialAccountLink` called with a token that is not the link ticket's owner. The code is spent; start the link again. */
36
+ OAUTH_LINK_USER_MISMATCH: 'OAUTH_LINK_USER_MISMATCH',
35
37
  UNAVAILABLE: 'UNAVAILABLE',
36
38
  UPSTREAM: 'UPSTREAM',
37
39
  };
@@ -44,6 +46,7 @@ const CODE_MESSAGES = new Set([
44
46
  BastionErrorCode.OAUTH_EMAIL_UNVERIFIED,
45
47
  BastionErrorCode.OAUTH_EMAIL_REQUIRED,
46
48
  BastionErrorCode.OAUTH_NO_ACCOUNT,
49
+ BastionErrorCode.OAUTH_LINK_USER_MISMATCH,
47
50
  ]);
48
51
  const APP_CONTEXT_RE = /app not found|tenant not found|tenant is inactive|no tenants configured|tenantslug is required/i;
49
52
  const OAUTH_CODE_RE = /already used code|code expired|code\/app mismatch/i;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyatom/bastion-client",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Consumer SDK for the Bastion identity provider: JWKS verification, service-client auth, audit writes, NestJS guards",
5
5
  "license": "MIT",
6
6
  "author": "Andrea Tombolato <hey@heyatom.dev> (https://heyatom.dev)",