@authyon/auth 0.1.3 → 0.1.5

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.cjs CHANGED
@@ -98,6 +98,22 @@ function defaultStorage() {
98
98
  // src/client.ts
99
99
  var DEFAULT_BASE_URL = "https://api.authyon.com";
100
100
  var EXPIRY_SKEW_MS = 3e4;
101
+ var FALLBACK_EXPIRES_IN = 1800;
102
+ function readTokens(raw) {
103
+ const tokens = raw.tokens ?? raw;
104
+ if (!tokens.accessToken || !tokens.refreshToken) {
105
+ throw new AuthyonError(502, {
106
+ code: "session.malformed",
107
+ title: "Malformed session response",
108
+ detail: "The session response carried no access/refresh token pair."
109
+ });
110
+ }
111
+ return {
112
+ accessToken: tokens.accessToken,
113
+ refreshToken: tokens.refreshToken,
114
+ expiresIn: tokens.expiresIn ?? FALLBACK_EXPIRES_IN
115
+ };
116
+ }
101
117
  var AuthyonClient = class {
102
118
  constructor(options) {
103
119
  this.listeners = /* @__PURE__ */ new Set();
@@ -113,7 +129,7 @@ var AuthyonClient = class {
113
129
  loginFinish: (assertion) => this.request("/auth/webauthn/login/finish", {
114
130
  method: "POST",
115
131
  body: assertion
116
- }).then((data) => this.setSession(data, "signed_in"))
132
+ }).then((data) => this.setSession({ tokens: readTokens(data) }, "signed_in")).then((session) => this.hydrateUser(session))
117
133
  };
118
134
  // ── Social sign-in (SSO) ─────────────────────────────────────────────────
119
135
  this.sso = {
@@ -134,9 +150,7 @@ var AuthyonClient = class {
134
150
  * POST /auth/sso/exchange — swaps the one-time code from the provider
135
151
  * callback for tokens and stores the session.
136
152
  */
137
- exchange: (code) => this.request("/auth/sso/exchange", { method: "POST", body: { code } }).then(
138
- (data) => this.setSession(data, "signed_in")
139
- )
153
+ exchange: (code) => this.request("/auth/sso/exchange", { method: "POST", body: { code } }).then((data) => this.setSession({ tokens: readTokens(data) }, "signed_in")).then((session) => this.hydrateUser(session))
140
154
  };
141
155
  // ── User ─────────────────────────────────────────────────────────────────
142
156
  this.user = {
@@ -144,7 +158,7 @@ var AuthyonClient = class {
144
158
  me: () => this.request("/auth/me", { bearer: true }).then(normalizeUser),
145
159
  /** GET /auth/sessions — active refresh-token sessions with device/IP data. */
146
160
  sessions: () => this.request("/auth/sessions", { bearer: true }),
147
- /** GET /auth/me/activities — recent account activity for the current user. */
161
+ /** GET /auth/me/activities — paginated recent account activity for the current user. */
148
162
  activities: (params = {}) => this.request(`/auth/me/activities?${toQuery(params)}`, { bearer: true }),
149
163
  /**
150
164
  * Revokes a single session by id (e.g. one entry from `sessions()`),
@@ -194,11 +208,16 @@ var AuthyonClient = class {
194
208
  method: "POST",
195
209
  bearer: true,
196
210
  body: { tenantSlug: organizationSlug }
197
- }).then((data) => this.setSession(data, "refreshed")),
211
+ }).then((data) => this.setSession({ tokens: readTokens(data) }, "refreshed")).then((session) => this.hydrateUser(session)),
198
212
  /** The organization the current session is scoped to, from the cached session — no network call. */
199
213
  current: () => this.getSession()?.user?.activeOrganization ?? null,
200
214
  members: {
201
- /** GET /auth/tenants/{organizationId}/members — list an organization's members. */
215
+ /**
216
+ * GET /auth/tenants/{organizationId}/members — paginated list of an
217
+ * organization's members. Consistent with the confirmed-live
218
+ * `Page<T>` envelope every other `skip`/`take` endpoint returns
219
+ * (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
220
+ */
202
221
  list: (organizationId, params = {}) => this.request(
203
222
  `/auth/tenants/${encodeURIComponent(organizationId)}/members?${toQuery(params)}`,
204
223
  { bearer: true }
@@ -332,9 +351,9 @@ var AuthyonClient = class {
332
351
  }
333
352
  setSession(raw, event) {
334
353
  const session = {
335
- ...raw,
354
+ ...raw.tokens,
336
355
  user: raw.user ? normalizeUser(raw.user) : void 0,
337
- expiresAt: Date.now() + raw.expiresIn * 1e3
356
+ expiresAt: Date.now() + raw.tokens.expiresIn * 1e3
338
357
  };
339
358
  this.storage.set(session);
340
359
  this.emit(event === "signed_out" ? { type: "signed_out" } : { type: event, session });
@@ -344,6 +363,23 @@ var AuthyonClient = class {
344
363
  this.storage.clear();
345
364
  this.emit({ type: "signed_out" });
346
365
  }
366
+ /**
367
+ * `/auth/login` and the other endpoints that mint a session don't return
368
+ * a `user` object on the wire — only `tokens` (plus `twoFactor`, when a
369
+ * challenge is required). Fetch the profile right after so callers get a
370
+ * fully-populated `session.user` without an extra manual round trip.
371
+ * Best-effort: keeps the session usable even if this fetch fails.
372
+ */
373
+ async hydrateUser(session) {
374
+ try {
375
+ const user = await this.user.me();
376
+ const hydrated = { ...session, user };
377
+ this.storage.set(hydrated);
378
+ return hydrated;
379
+ } catch {
380
+ return session;
381
+ }
382
+ }
347
383
  // ── HTTP core ────────────────────────────────────────────────────────────
348
384
  async request(path, options = {}, isRetry = false) {
349
385
  const headers = {
@@ -395,20 +431,22 @@ var AuthyonClient = class {
395
431
  async login(params) {
396
432
  const { organizationSlug, ...rest } = params;
397
433
  const body = organizationSlug ? { ...rest, tenantSlug: organizationSlug } : rest;
398
- const data = await this.request("/auth/login", {
399
- method: "POST",
400
- body
401
- });
402
- if (data.twoFactorRequired) {
403
- return data;
434
+ const data = await this.request("/auth/login", { method: "POST", body });
435
+ if (data.twoFactor) {
436
+ return { twoFactorRequired: true, ...data.twoFactor };
404
437
  }
405
- const session = this.setSession(data, "signed_in");
438
+ const session = await this.hydrateUser(
439
+ this.setSession({ tokens: readTokens(data) }, "signed_in")
440
+ );
406
441
  return { twoFactorRequired: false, session };
407
442
  }
408
443
  /** POST /auth/2fa/verify — redeems a 2FA challenge from `login()` and stores the session. */
409
444
  async verifyTwoFactor(params) {
410
- const data = await this.request("/auth/2fa/verify", { method: "POST", body: params });
411
- return this.setSession(data, "signed_in");
445
+ const data = await this.request("/auth/2fa/verify", {
446
+ method: "POST",
447
+ body: params
448
+ });
449
+ return this.hydrateUser(this.setSession({ tokens: readTokens(data) }, "signed_in"));
412
450
  }
413
451
  /** POST /auth/refresh — rotates the single-use refresh token (single-flight). */
414
452
  async refresh() {
@@ -420,7 +458,10 @@ var AuthyonClient = class {
420
458
  method: "POST",
421
459
  body: { refreshToken: current.refreshToken }
422
460
  }).then(
423
- (data) => this.setSession({ user: current.user, ...data }, "refreshed")
461
+ (data) => this.setSession(
462
+ { tokens: readTokens(data), user: current.user },
463
+ "refreshed"
464
+ )
424
465
  ).catch((error) => {
425
466
  if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
426
467
  this.clearSession();
@@ -453,30 +494,41 @@ var AuthyonClient = class {
453
494
  this.clearSession();
454
495
  }
455
496
  // ── Token verification ───────────────────────────────────────────────────
456
- /** POST /auth/introspect — lightweight token introspection. */
497
+ /**
498
+ * POST /auth/introspect — lightweight token introspection (RFC 7662).
499
+ *
500
+ * ⚠️ Confirmed live: this endpoint requires the CALLER to also
501
+ * authenticate, with an environment or tenant client-credentials bearer
502
+ * token — the end user's own access token doesn't satisfy that (401).
503
+ * A browser app has no client secret to present, so this will fail from
504
+ * `@authyon/auth` in practice; call it from your backend via
505
+ * `@authyon/server` instead.
506
+ */
457
507
  async introspect(token) {
458
508
  const accessToken = token ?? await this.getAccessToken();
459
509
  return this.request("/auth/introspect", { method: "POST", body: { token: accessToken } });
460
510
  }
461
- /** POST /auth/validate — recommended: cross-checks DB state, returns user + organization. */
511
+ /**
512
+ * POST /auth/validate — recommended: cross-checks DB state, catches
513
+ * revocation immediately. Same caller-authentication requirement (and
514
+ * the same practical limitation from the browser) as `introspect()`.
515
+ */
462
516
  async validate(token) {
463
517
  const accessToken = token ?? await this.getAccessToken();
464
- const raw = await this.request("/auth/validate", {
465
- method: "POST",
466
- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0
467
- });
518
+ const raw = await this.request("/auth/validate", { method: "POST", body: { token: accessToken } });
468
519
  return {
469
- user: normalizeUser(raw.user),
470
- organization: raw.organization ?? raw.tenant ?? null
520
+ valid: raw.valid,
521
+ reason: raw.reason ?? null,
522
+ user: raw.profile ? normalizeUser(raw.profile) : null
471
523
  };
472
524
  }
473
525
  };
474
526
  function normalizeUser(raw) {
475
- const { tenants, activeTenant, ...rest } = raw;
527
+ const { tenant, tenants, activeTenant, ...rest } = raw;
476
528
  return {
477
529
  ...rest,
478
530
  organizations: raw.organizations ?? tenants,
479
- activeOrganization: raw.activeOrganization ?? activeTenant ?? null
531
+ activeOrganization: tenant ?? raw.activeOrganization ?? activeTenant ?? null
480
532
  };
481
533
  }
482
534
  function createClient(options) {
package/dist/index.d.cts CHANGED
@@ -15,10 +15,14 @@ interface CreateOrganizationParams {
15
15
  slug?: string;
16
16
  description?: string;
17
17
  }
18
+ /** GET /auth/tenants/{organizationId}/members — confirmed against the live API. */
18
19
  interface OrganizationMember {
19
20
  userId: string;
20
21
  email?: string;
22
+ username?: string;
21
23
  roles?: string[];
24
+ createdAt?: string;
25
+ lastLoginAt?: string | null;
22
26
  }
23
27
  /** POST /auth/tenants/{tenantId}/members — invites a member by e-mail. */
24
28
  interface InviteMemberParams {
@@ -35,9 +39,17 @@ interface User {
35
39
  id: string;
36
40
  email: string;
37
41
  username?: string;
42
+ emailConfirmed?: boolean;
43
+ firstName?: string | null;
44
+ lastName?: string | null;
45
+ roles?: string[];
46
+ permissions?: string[];
47
+ createdAt?: string;
48
+ lastLoginAt?: string;
38
49
  organizations?: Organization[];
39
50
  activeOrganization?: Organization | null;
40
- permissions?: string[];
51
+ /** Actions the user must complete before continuing (e.g. confirm e-mail). */
52
+ pendencies?: string[];
41
53
  }
42
54
  /** Token pair issued by login / refresh / tenant switch. */
43
55
  interface Session {
@@ -89,9 +101,18 @@ interface TwoFactorVerifyParams {
89
101
  /** Required when `method` is `"webauthn"`. */
90
102
  webAuthnAssertion?: WebAuthnAssertion;
91
103
  }
104
+ /** GET /auth/2fa/status — per-method enrolment flags, confirmed against the live API. */
92
105
  interface TwoFactorStatus {
93
- methods: TwoFactorMethod[];
94
- recoveryCodesRemaining?: number;
106
+ authenticatorEnabled: boolean;
107
+ authenticatorConfirmedAt?: string | null;
108
+ emailEnabled: boolean;
109
+ emailEnabledAt?: string | null;
110
+ /** Partially redacted (e.g. `"n**********@h***.com"`). */
111
+ emailHint?: string | null;
112
+ webAuthnEnabled: boolean;
113
+ webAuthnCredentialCount: number;
114
+ webAuthnCredentials: WebAuthnCredential[];
115
+ remainingRecoveryCodes: number;
95
116
  }
96
117
  interface AuthenticatorSetup {
97
118
  secret: string;
@@ -123,12 +144,27 @@ interface SsoProvider {
123
144
  /** URL to redirect the browser to in order to start this provider's flow. */
124
145
  startUrl: string;
125
146
  }
147
+ /** GET /auth/me/activities — one audit-trail entry, confirmed against the live API. */
126
148
  interface Activity {
127
149
  id: string;
128
- type: string;
129
- createdAt: string;
150
+ eventType: string;
151
+ occurredAt: string;
152
+ environmentId?: string;
130
153
  ip?: string;
131
- device?: string;
154
+ userAgent?: string;
155
+ /** JSON-encoded string — `JSON.parse` it for the event-specific payload. */
156
+ payloadJson?: string;
157
+ }
158
+ /** Paginated list envelope returned by `user.activities()`. */
159
+ interface Page<T> {
160
+ data: T[];
161
+ /** Item count actually returned for this page. */
162
+ perPage?: number;
163
+ pageSize: number;
164
+ total: number;
165
+ pages: number;
166
+ hasNext: boolean;
167
+ hasPrev: boolean;
132
168
  }
133
169
  /** A role available within an organization (tenant). */
134
170
  interface Role {
@@ -137,25 +173,44 @@ interface Role {
137
173
  description?: string;
138
174
  permissions?: string[];
139
175
  }
176
+ /** GET /auth/sessions — confirmed against the live API. */
140
177
  interface SessionInfo {
141
178
  id: string;
142
- createdAt?: string;
143
- lastUsedAt?: string;
144
- ip?: string;
145
- device?: string;
146
- current?: boolean;
179
+ createdAt: string;
180
+ expiresAt: string;
181
+ revokedAt?: string | null;
182
+ createdFromIp?: string;
183
+ isActive: boolean;
184
+ userAgent?: string;
185
+ lastUsedAt?: string | null;
186
+ lastUsedFromIp?: string | null;
147
187
  }
188
+ /** POST /auth/introspect (RFC 7662) — confirmed against the live API. */
148
189
  interface IntrospectResult {
149
190
  active: boolean;
150
191
  sub?: string;
192
+ username?: string | null;
193
+ email?: string | null;
194
+ roles?: string[] | null;
195
+ permissions?: string[];
151
196
  client_id?: string;
152
197
  scope?: string;
153
198
  exp?: number;
199
+ iat?: number;
200
+ jti?: string;
154
201
  token_type?: string;
155
202
  }
203
+ /**
204
+ * POST /auth/validate — confirmed against the live API. The wire shape is
205
+ * `{ valid, reason, profile }`, not `{ user, organization }` as the
206
+ * OpenAPI schema (which didn't document response bodies) suggested.
207
+ * `profile` is `null` for machine tokens (there's no user behind them) and
208
+ * for tokens that fail validation.
209
+ */
156
210
  interface ValidateResult {
157
- user: User;
158
- organization?: Organization | null;
211
+ valid: boolean;
212
+ reason?: string | null;
213
+ user: User | null;
159
214
  }
160
215
  type AuthEvent = {
161
216
  type: "signed_in";
@@ -211,6 +266,14 @@ declare class AuthyonClient {
211
266
  private emit;
212
267
  private setSession;
213
268
  private clearSession;
269
+ /**
270
+ * `/auth/login` and the other endpoints that mint a session don't return
271
+ * a `user` object on the wire — only `tokens` (plus `twoFactor`, when a
272
+ * challenge is required). Fetch the profile right after so callers get a
273
+ * fully-populated `session.user` without an extra manual round trip.
274
+ * Best-effort: keeps the session usable even if this fetch fails.
275
+ */
276
+ private hydrateUser;
214
277
  private request;
215
278
  private toError;
216
279
  /** POST /auth/register — creates a new user (rate-limited to 20/hour per IP). */
@@ -266,8 +329,8 @@ declare class AuthyonClient {
266
329
  me: () => Promise<User>;
267
330
  /** GET /auth/sessions — active refresh-token sessions with device/IP data. */
268
331
  sessions: () => Promise<SessionInfo[]>;
269
- /** GET /auth/me/activities — recent account activity for the current user. */
270
- activities: (params?: PageParams) => Promise<Activity[]>;
332
+ /** GET /auth/me/activities — paginated recent account activity for the current user. */
333
+ activities: (params?: PageParams) => Promise<Page<Activity>>;
271
334
  /**
272
335
  * Revokes a single session by id (e.g. one entry from `sessions()`),
273
336
  * signing that device out without affecting the current one.
@@ -305,8 +368,13 @@ declare class AuthyonClient {
305
368
  /** The organization the current session is scoped to, from the cached session — no network call. */
306
369
  current: () => Organization | null;
307
370
  members: {
308
- /** GET /auth/tenants/{organizationId}/members — list an organization's members. */
309
- list: (organizationId: string, params?: PageParams) => Promise<OrganizationMember[]>;
371
+ /**
372
+ * GET /auth/tenants/{organizationId}/members paginated list of an
373
+ * organization's members. Consistent with the confirmed-live
374
+ * `Page<T>` envelope every other `skip`/`take` endpoint returns
375
+ * (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
376
+ */
377
+ list: (organizationId: string, params?: PageParams) => Promise<Page<OrganizationMember>>;
310
378
  /** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
311
379
  invite: (organizationId: string, params: InviteMemberParams) => Promise<void>;
312
380
  /** DELETE /auth/tenants/{organizationId}/members/{userId} — remove a member. */
@@ -361,9 +429,22 @@ declare class AuthyonClient {
361
429
  assertionStart: (challengeToken: string) => Promise<WebAuthnCeremonyStart>;
362
430
  };
363
431
  };
364
- /** POST /auth/introspect — lightweight token introspection. */
432
+ /**
433
+ * POST /auth/introspect — lightweight token introspection (RFC 7662).
434
+ *
435
+ * ⚠️ Confirmed live: this endpoint requires the CALLER to also
436
+ * authenticate, with an environment or tenant client-credentials bearer
437
+ * token — the end user's own access token doesn't satisfy that (401).
438
+ * A browser app has no client secret to present, so this will fail from
439
+ * `@authyon/auth` in practice; call it from your backend via
440
+ * `@authyon/server` instead.
441
+ */
365
442
  introspect(token?: string): Promise<IntrospectResult>;
366
- /** POST /auth/validate — recommended: cross-checks DB state, returns user + organization. */
443
+ /**
444
+ * POST /auth/validate — recommended: cross-checks DB state, catches
445
+ * revocation immediately. Same caller-authentication requirement (and
446
+ * the same practical limitation from the browser) as `introspect()`.
447
+ */
367
448
  validate(token?: string): Promise<ValidateResult>;
368
449
  }
369
450
  /** Convenience factory: `const authyon = createClient({ envKey: "pk_live_..." })`. */
@@ -400,4 +481,4 @@ declare function memoryStorage(): TokenStorage;
400
481
  declare function localStorageAdapter(key?: string): TokenStorage;
401
482
  declare function defaultStorage(): TokenStorage;
402
483
 
403
- export { type Activity, type AuthEvent, type AuthStateListener, type AuthenticatorSetup, AuthyonClient, type AuthyonClientOptions, AuthyonError, type CreateOrganizationParams, ErrorCodes, type IntrospectResult, type InviteMemberParams, type LoginParams, type LoginResult, type Organization, type OrganizationMember, type PageParams, type RegisterParams, type Role, type Session, type SessionInfo, type SsoProvider, type TokenStorage, type TwoFactorChallenge, type TwoFactorMethod, type TwoFactorStatus, type TwoFactorVerifyParams, type User, type ValidateResult, type WebAuthnAssertion, type WebAuthnCeremonyStart, type WebAuthnCredential, createClient, defaultStorage, localStorageAdapter, memoryStorage };
484
+ export { type Activity, type AuthEvent, type AuthStateListener, type AuthenticatorSetup, AuthyonClient, type AuthyonClientOptions, AuthyonError, type CreateOrganizationParams, ErrorCodes, type IntrospectResult, type InviteMemberParams, type LoginParams, type LoginResult, type Organization, type OrganizationMember, type Page, type PageParams, type RegisterParams, type Role, type Session, type SessionInfo, type SsoProvider, type TokenStorage, type TwoFactorChallenge, type TwoFactorMethod, type TwoFactorStatus, type TwoFactorVerifyParams, type User, type ValidateResult, type WebAuthnAssertion, type WebAuthnCeremonyStart, type WebAuthnCredential, createClient, defaultStorage, localStorageAdapter, memoryStorage };
package/dist/index.d.ts CHANGED
@@ -15,10 +15,14 @@ interface CreateOrganizationParams {
15
15
  slug?: string;
16
16
  description?: string;
17
17
  }
18
+ /** GET /auth/tenants/{organizationId}/members — confirmed against the live API. */
18
19
  interface OrganizationMember {
19
20
  userId: string;
20
21
  email?: string;
22
+ username?: string;
21
23
  roles?: string[];
24
+ createdAt?: string;
25
+ lastLoginAt?: string | null;
22
26
  }
23
27
  /** POST /auth/tenants/{tenantId}/members — invites a member by e-mail. */
24
28
  interface InviteMemberParams {
@@ -35,9 +39,17 @@ interface User {
35
39
  id: string;
36
40
  email: string;
37
41
  username?: string;
42
+ emailConfirmed?: boolean;
43
+ firstName?: string | null;
44
+ lastName?: string | null;
45
+ roles?: string[];
46
+ permissions?: string[];
47
+ createdAt?: string;
48
+ lastLoginAt?: string;
38
49
  organizations?: Organization[];
39
50
  activeOrganization?: Organization | null;
40
- permissions?: string[];
51
+ /** Actions the user must complete before continuing (e.g. confirm e-mail). */
52
+ pendencies?: string[];
41
53
  }
42
54
  /** Token pair issued by login / refresh / tenant switch. */
43
55
  interface Session {
@@ -89,9 +101,18 @@ interface TwoFactorVerifyParams {
89
101
  /** Required when `method` is `"webauthn"`. */
90
102
  webAuthnAssertion?: WebAuthnAssertion;
91
103
  }
104
+ /** GET /auth/2fa/status — per-method enrolment flags, confirmed against the live API. */
92
105
  interface TwoFactorStatus {
93
- methods: TwoFactorMethod[];
94
- recoveryCodesRemaining?: number;
106
+ authenticatorEnabled: boolean;
107
+ authenticatorConfirmedAt?: string | null;
108
+ emailEnabled: boolean;
109
+ emailEnabledAt?: string | null;
110
+ /** Partially redacted (e.g. `"n**********@h***.com"`). */
111
+ emailHint?: string | null;
112
+ webAuthnEnabled: boolean;
113
+ webAuthnCredentialCount: number;
114
+ webAuthnCredentials: WebAuthnCredential[];
115
+ remainingRecoveryCodes: number;
95
116
  }
96
117
  interface AuthenticatorSetup {
97
118
  secret: string;
@@ -123,12 +144,27 @@ interface SsoProvider {
123
144
  /** URL to redirect the browser to in order to start this provider's flow. */
124
145
  startUrl: string;
125
146
  }
147
+ /** GET /auth/me/activities — one audit-trail entry, confirmed against the live API. */
126
148
  interface Activity {
127
149
  id: string;
128
- type: string;
129
- createdAt: string;
150
+ eventType: string;
151
+ occurredAt: string;
152
+ environmentId?: string;
130
153
  ip?: string;
131
- device?: string;
154
+ userAgent?: string;
155
+ /** JSON-encoded string — `JSON.parse` it for the event-specific payload. */
156
+ payloadJson?: string;
157
+ }
158
+ /** Paginated list envelope returned by `user.activities()`. */
159
+ interface Page<T> {
160
+ data: T[];
161
+ /** Item count actually returned for this page. */
162
+ perPage?: number;
163
+ pageSize: number;
164
+ total: number;
165
+ pages: number;
166
+ hasNext: boolean;
167
+ hasPrev: boolean;
132
168
  }
133
169
  /** A role available within an organization (tenant). */
134
170
  interface Role {
@@ -137,25 +173,44 @@ interface Role {
137
173
  description?: string;
138
174
  permissions?: string[];
139
175
  }
176
+ /** GET /auth/sessions — confirmed against the live API. */
140
177
  interface SessionInfo {
141
178
  id: string;
142
- createdAt?: string;
143
- lastUsedAt?: string;
144
- ip?: string;
145
- device?: string;
146
- current?: boolean;
179
+ createdAt: string;
180
+ expiresAt: string;
181
+ revokedAt?: string | null;
182
+ createdFromIp?: string;
183
+ isActive: boolean;
184
+ userAgent?: string;
185
+ lastUsedAt?: string | null;
186
+ lastUsedFromIp?: string | null;
147
187
  }
188
+ /** POST /auth/introspect (RFC 7662) — confirmed against the live API. */
148
189
  interface IntrospectResult {
149
190
  active: boolean;
150
191
  sub?: string;
192
+ username?: string | null;
193
+ email?: string | null;
194
+ roles?: string[] | null;
195
+ permissions?: string[];
151
196
  client_id?: string;
152
197
  scope?: string;
153
198
  exp?: number;
199
+ iat?: number;
200
+ jti?: string;
154
201
  token_type?: string;
155
202
  }
203
+ /**
204
+ * POST /auth/validate — confirmed against the live API. The wire shape is
205
+ * `{ valid, reason, profile }`, not `{ user, organization }` as the
206
+ * OpenAPI schema (which didn't document response bodies) suggested.
207
+ * `profile` is `null` for machine tokens (there's no user behind them) and
208
+ * for tokens that fail validation.
209
+ */
156
210
  interface ValidateResult {
157
- user: User;
158
- organization?: Organization | null;
211
+ valid: boolean;
212
+ reason?: string | null;
213
+ user: User | null;
159
214
  }
160
215
  type AuthEvent = {
161
216
  type: "signed_in";
@@ -211,6 +266,14 @@ declare class AuthyonClient {
211
266
  private emit;
212
267
  private setSession;
213
268
  private clearSession;
269
+ /**
270
+ * `/auth/login` and the other endpoints that mint a session don't return
271
+ * a `user` object on the wire — only `tokens` (plus `twoFactor`, when a
272
+ * challenge is required). Fetch the profile right after so callers get a
273
+ * fully-populated `session.user` without an extra manual round trip.
274
+ * Best-effort: keeps the session usable even if this fetch fails.
275
+ */
276
+ private hydrateUser;
214
277
  private request;
215
278
  private toError;
216
279
  /** POST /auth/register — creates a new user (rate-limited to 20/hour per IP). */
@@ -266,8 +329,8 @@ declare class AuthyonClient {
266
329
  me: () => Promise<User>;
267
330
  /** GET /auth/sessions — active refresh-token sessions with device/IP data. */
268
331
  sessions: () => Promise<SessionInfo[]>;
269
- /** GET /auth/me/activities — recent account activity for the current user. */
270
- activities: (params?: PageParams) => Promise<Activity[]>;
332
+ /** GET /auth/me/activities — paginated recent account activity for the current user. */
333
+ activities: (params?: PageParams) => Promise<Page<Activity>>;
271
334
  /**
272
335
  * Revokes a single session by id (e.g. one entry from `sessions()`),
273
336
  * signing that device out without affecting the current one.
@@ -305,8 +368,13 @@ declare class AuthyonClient {
305
368
  /** The organization the current session is scoped to, from the cached session — no network call. */
306
369
  current: () => Organization | null;
307
370
  members: {
308
- /** GET /auth/tenants/{organizationId}/members — list an organization's members. */
309
- list: (organizationId: string, params?: PageParams) => Promise<OrganizationMember[]>;
371
+ /**
372
+ * GET /auth/tenants/{organizationId}/members paginated list of an
373
+ * organization's members. Consistent with the confirmed-live
374
+ * `Page<T>` envelope every other `skip`/`take` endpoint returns
375
+ * (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
376
+ */
377
+ list: (organizationId: string, params?: PageParams) => Promise<Page<OrganizationMember>>;
310
378
  /** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
311
379
  invite: (organizationId: string, params: InviteMemberParams) => Promise<void>;
312
380
  /** DELETE /auth/tenants/{organizationId}/members/{userId} — remove a member. */
@@ -361,9 +429,22 @@ declare class AuthyonClient {
361
429
  assertionStart: (challengeToken: string) => Promise<WebAuthnCeremonyStart>;
362
430
  };
363
431
  };
364
- /** POST /auth/introspect — lightweight token introspection. */
432
+ /**
433
+ * POST /auth/introspect — lightweight token introspection (RFC 7662).
434
+ *
435
+ * ⚠️ Confirmed live: this endpoint requires the CALLER to also
436
+ * authenticate, with an environment or tenant client-credentials bearer
437
+ * token — the end user's own access token doesn't satisfy that (401).
438
+ * A browser app has no client secret to present, so this will fail from
439
+ * `@authyon/auth` in practice; call it from your backend via
440
+ * `@authyon/server` instead.
441
+ */
365
442
  introspect(token?: string): Promise<IntrospectResult>;
366
- /** POST /auth/validate — recommended: cross-checks DB state, returns user + organization. */
443
+ /**
444
+ * POST /auth/validate — recommended: cross-checks DB state, catches
445
+ * revocation immediately. Same caller-authentication requirement (and
446
+ * the same practical limitation from the browser) as `introspect()`.
447
+ */
367
448
  validate(token?: string): Promise<ValidateResult>;
368
449
  }
369
450
  /** Convenience factory: `const authyon = createClient({ envKey: "pk_live_..." })`. */
@@ -400,4 +481,4 @@ declare function memoryStorage(): TokenStorage;
400
481
  declare function localStorageAdapter(key?: string): TokenStorage;
401
482
  declare function defaultStorage(): TokenStorage;
402
483
 
403
- export { type Activity, type AuthEvent, type AuthStateListener, type AuthenticatorSetup, AuthyonClient, type AuthyonClientOptions, AuthyonError, type CreateOrganizationParams, ErrorCodes, type IntrospectResult, type InviteMemberParams, type LoginParams, type LoginResult, type Organization, type OrganizationMember, type PageParams, type RegisterParams, type Role, type Session, type SessionInfo, type SsoProvider, type TokenStorage, type TwoFactorChallenge, type TwoFactorMethod, type TwoFactorStatus, type TwoFactorVerifyParams, type User, type ValidateResult, type WebAuthnAssertion, type WebAuthnCeremonyStart, type WebAuthnCredential, createClient, defaultStorage, localStorageAdapter, memoryStorage };
484
+ export { type Activity, type AuthEvent, type AuthStateListener, type AuthenticatorSetup, AuthyonClient, type AuthyonClientOptions, AuthyonError, type CreateOrganizationParams, ErrorCodes, type IntrospectResult, type InviteMemberParams, type LoginParams, type LoginResult, type Organization, type OrganizationMember, type Page, type PageParams, type RegisterParams, type Role, type Session, type SessionInfo, type SsoProvider, type TokenStorage, type TwoFactorChallenge, type TwoFactorMethod, type TwoFactorStatus, type TwoFactorVerifyParams, type User, type ValidateResult, type WebAuthnAssertion, type WebAuthnCeremonyStart, type WebAuthnCredential, createClient, defaultStorage, localStorageAdapter, memoryStorage };
package/dist/index.js CHANGED
@@ -66,6 +66,22 @@ function defaultStorage() {
66
66
  // src/client.ts
67
67
  var DEFAULT_BASE_URL = "https://api.authyon.com";
68
68
  var EXPIRY_SKEW_MS = 3e4;
69
+ var FALLBACK_EXPIRES_IN = 1800;
70
+ function readTokens(raw) {
71
+ const tokens = raw.tokens ?? raw;
72
+ if (!tokens.accessToken || !tokens.refreshToken) {
73
+ throw new AuthyonError(502, {
74
+ code: "session.malformed",
75
+ title: "Malformed session response",
76
+ detail: "The session response carried no access/refresh token pair."
77
+ });
78
+ }
79
+ return {
80
+ accessToken: tokens.accessToken,
81
+ refreshToken: tokens.refreshToken,
82
+ expiresIn: tokens.expiresIn ?? FALLBACK_EXPIRES_IN
83
+ };
84
+ }
69
85
  var AuthyonClient = class {
70
86
  constructor(options) {
71
87
  this.listeners = /* @__PURE__ */ new Set();
@@ -81,7 +97,7 @@ var AuthyonClient = class {
81
97
  loginFinish: (assertion) => this.request("/auth/webauthn/login/finish", {
82
98
  method: "POST",
83
99
  body: assertion
84
- }).then((data) => this.setSession(data, "signed_in"))
100
+ }).then((data) => this.setSession({ tokens: readTokens(data) }, "signed_in")).then((session) => this.hydrateUser(session))
85
101
  };
86
102
  // ── Social sign-in (SSO) ─────────────────────────────────────────────────
87
103
  this.sso = {
@@ -102,9 +118,7 @@ var AuthyonClient = class {
102
118
  * POST /auth/sso/exchange — swaps the one-time code from the provider
103
119
  * callback for tokens and stores the session.
104
120
  */
105
- exchange: (code) => this.request("/auth/sso/exchange", { method: "POST", body: { code } }).then(
106
- (data) => this.setSession(data, "signed_in")
107
- )
121
+ exchange: (code) => this.request("/auth/sso/exchange", { method: "POST", body: { code } }).then((data) => this.setSession({ tokens: readTokens(data) }, "signed_in")).then((session) => this.hydrateUser(session))
108
122
  };
109
123
  // ── User ─────────────────────────────────────────────────────────────────
110
124
  this.user = {
@@ -112,7 +126,7 @@ var AuthyonClient = class {
112
126
  me: () => this.request("/auth/me", { bearer: true }).then(normalizeUser),
113
127
  /** GET /auth/sessions — active refresh-token sessions with device/IP data. */
114
128
  sessions: () => this.request("/auth/sessions", { bearer: true }),
115
- /** GET /auth/me/activities — recent account activity for the current user. */
129
+ /** GET /auth/me/activities — paginated recent account activity for the current user. */
116
130
  activities: (params = {}) => this.request(`/auth/me/activities?${toQuery(params)}`, { bearer: true }),
117
131
  /**
118
132
  * Revokes a single session by id (e.g. one entry from `sessions()`),
@@ -162,11 +176,16 @@ var AuthyonClient = class {
162
176
  method: "POST",
163
177
  bearer: true,
164
178
  body: { tenantSlug: organizationSlug }
165
- }).then((data) => this.setSession(data, "refreshed")),
179
+ }).then((data) => this.setSession({ tokens: readTokens(data) }, "refreshed")).then((session) => this.hydrateUser(session)),
166
180
  /** The organization the current session is scoped to, from the cached session — no network call. */
167
181
  current: () => this.getSession()?.user?.activeOrganization ?? null,
168
182
  members: {
169
- /** GET /auth/tenants/{organizationId}/members — list an organization's members. */
183
+ /**
184
+ * GET /auth/tenants/{organizationId}/members — paginated list of an
185
+ * organization's members. Consistent with the confirmed-live
186
+ * `Page<T>` envelope every other `skip`/`take` endpoint returns
187
+ * (`user.activities()`, `@authyon/server`'s `environment.users.list()`).
188
+ */
170
189
  list: (organizationId, params = {}) => this.request(
171
190
  `/auth/tenants/${encodeURIComponent(organizationId)}/members?${toQuery(params)}`,
172
191
  { bearer: true }
@@ -300,9 +319,9 @@ var AuthyonClient = class {
300
319
  }
301
320
  setSession(raw, event) {
302
321
  const session = {
303
- ...raw,
322
+ ...raw.tokens,
304
323
  user: raw.user ? normalizeUser(raw.user) : void 0,
305
- expiresAt: Date.now() + raw.expiresIn * 1e3
324
+ expiresAt: Date.now() + raw.tokens.expiresIn * 1e3
306
325
  };
307
326
  this.storage.set(session);
308
327
  this.emit(event === "signed_out" ? { type: "signed_out" } : { type: event, session });
@@ -312,6 +331,23 @@ var AuthyonClient = class {
312
331
  this.storage.clear();
313
332
  this.emit({ type: "signed_out" });
314
333
  }
334
+ /**
335
+ * `/auth/login` and the other endpoints that mint a session don't return
336
+ * a `user` object on the wire — only `tokens` (plus `twoFactor`, when a
337
+ * challenge is required). Fetch the profile right after so callers get a
338
+ * fully-populated `session.user` without an extra manual round trip.
339
+ * Best-effort: keeps the session usable even if this fetch fails.
340
+ */
341
+ async hydrateUser(session) {
342
+ try {
343
+ const user = await this.user.me();
344
+ const hydrated = { ...session, user };
345
+ this.storage.set(hydrated);
346
+ return hydrated;
347
+ } catch {
348
+ return session;
349
+ }
350
+ }
315
351
  // ── HTTP core ────────────────────────────────────────────────────────────
316
352
  async request(path, options = {}, isRetry = false) {
317
353
  const headers = {
@@ -363,20 +399,22 @@ var AuthyonClient = class {
363
399
  async login(params) {
364
400
  const { organizationSlug, ...rest } = params;
365
401
  const body = organizationSlug ? { ...rest, tenantSlug: organizationSlug } : rest;
366
- const data = await this.request("/auth/login", {
367
- method: "POST",
368
- body
369
- });
370
- if (data.twoFactorRequired) {
371
- return data;
402
+ const data = await this.request("/auth/login", { method: "POST", body });
403
+ if (data.twoFactor) {
404
+ return { twoFactorRequired: true, ...data.twoFactor };
372
405
  }
373
- const session = this.setSession(data, "signed_in");
406
+ const session = await this.hydrateUser(
407
+ this.setSession({ tokens: readTokens(data) }, "signed_in")
408
+ );
374
409
  return { twoFactorRequired: false, session };
375
410
  }
376
411
  /** POST /auth/2fa/verify — redeems a 2FA challenge from `login()` and stores the session. */
377
412
  async verifyTwoFactor(params) {
378
- const data = await this.request("/auth/2fa/verify", { method: "POST", body: params });
379
- return this.setSession(data, "signed_in");
413
+ const data = await this.request("/auth/2fa/verify", {
414
+ method: "POST",
415
+ body: params
416
+ });
417
+ return this.hydrateUser(this.setSession({ tokens: readTokens(data) }, "signed_in"));
380
418
  }
381
419
  /** POST /auth/refresh — rotates the single-use refresh token (single-flight). */
382
420
  async refresh() {
@@ -388,7 +426,10 @@ var AuthyonClient = class {
388
426
  method: "POST",
389
427
  body: { refreshToken: current.refreshToken }
390
428
  }).then(
391
- (data) => this.setSession({ user: current.user, ...data }, "refreshed")
429
+ (data) => this.setSession(
430
+ { tokens: readTokens(data), user: current.user },
431
+ "refreshed"
432
+ )
392
433
  ).catch((error) => {
393
434
  if (error instanceof AuthyonError && (error.status === 401 || error.status === 403)) {
394
435
  this.clearSession();
@@ -421,30 +462,41 @@ var AuthyonClient = class {
421
462
  this.clearSession();
422
463
  }
423
464
  // ── Token verification ───────────────────────────────────────────────────
424
- /** POST /auth/introspect — lightweight token introspection. */
465
+ /**
466
+ * POST /auth/introspect — lightweight token introspection (RFC 7662).
467
+ *
468
+ * ⚠️ Confirmed live: this endpoint requires the CALLER to also
469
+ * authenticate, with an environment or tenant client-credentials bearer
470
+ * token — the end user's own access token doesn't satisfy that (401).
471
+ * A browser app has no client secret to present, so this will fail from
472
+ * `@authyon/auth` in practice; call it from your backend via
473
+ * `@authyon/server` instead.
474
+ */
425
475
  async introspect(token) {
426
476
  const accessToken = token ?? await this.getAccessToken();
427
477
  return this.request("/auth/introspect", { method: "POST", body: { token: accessToken } });
428
478
  }
429
- /** POST /auth/validate — recommended: cross-checks DB state, returns user + organization. */
479
+ /**
480
+ * POST /auth/validate — recommended: cross-checks DB state, catches
481
+ * revocation immediately. Same caller-authentication requirement (and
482
+ * the same practical limitation from the browser) as `introspect()`.
483
+ */
430
484
  async validate(token) {
431
485
  const accessToken = token ?? await this.getAccessToken();
432
- const raw = await this.request("/auth/validate", {
433
- method: "POST",
434
- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0
435
- });
486
+ const raw = await this.request("/auth/validate", { method: "POST", body: { token: accessToken } });
436
487
  return {
437
- user: normalizeUser(raw.user),
438
- organization: raw.organization ?? raw.tenant ?? null
488
+ valid: raw.valid,
489
+ reason: raw.reason ?? null,
490
+ user: raw.profile ? normalizeUser(raw.profile) : null
439
491
  };
440
492
  }
441
493
  };
442
494
  function normalizeUser(raw) {
443
- const { tenants, activeTenant, ...rest } = raw;
495
+ const { tenant, tenants, activeTenant, ...rest } = raw;
444
496
  return {
445
497
  ...rest,
446
498
  organizations: raw.organizations ?? tenants,
447
- activeOrganization: raw.activeOrganization ?? activeTenant ?? null
499
+ activeOrganization: tenant ?? raw.activeOrganization ?? activeTenant ?? null
448
500
  };
449
501
  }
450
502
  function createClient(options) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authyon/auth",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Authyon SDK for browsers — auth, sessions, multi-tenant and 2FA for vanilla JS/TS.",
5
5
  "license": "MIT",
6
6
  "type": "module",