@authyon/auth 0.1.4 → 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({ tokens: data.tokens }, "signed_in")).then((session) => this.hydrateUser(session))
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,7 +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((data) => this.setSession({ tokens: data.tokens }, "signed_in")).then((session) => this.hydrateUser(session))
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))
138
154
  };
139
155
  // ── User ─────────────────────────────────────────────────────────────────
140
156
  this.user = {
@@ -192,11 +208,16 @@ var AuthyonClient = class {
192
208
  method: "POST",
193
209
  bearer: true,
194
210
  body: { tenantSlug: organizationSlug }
195
- }).then((data) => this.setSession({ tokens: data.tokens }, "refreshed")).then((session) => this.hydrateUser(session)),
211
+ }).then((data) => this.setSession({ tokens: readTokens(data) }, "refreshed")).then((session) => this.hydrateUser(session)),
196
212
  /** The organization the current session is scoped to, from the cached session — no network call. */
197
213
  current: () => this.getSession()?.user?.activeOrganization ?? null,
198
214
  members: {
199
- /** 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
+ */
200
221
  list: (organizationId, params = {}) => this.request(
201
222
  `/auth/tenants/${encodeURIComponent(organizationId)}/members?${toQuery(params)}`,
202
223
  { bearer: true }
@@ -414,7 +435,9 @@ var AuthyonClient = class {
414
435
  if (data.twoFactor) {
415
436
  return { twoFactorRequired: true, ...data.twoFactor };
416
437
  }
417
- const session = await this.hydrateUser(this.setSession({ tokens: data.tokens }, "signed_in"));
438
+ const session = await this.hydrateUser(
439
+ this.setSession({ tokens: readTokens(data) }, "signed_in")
440
+ );
418
441
  return { twoFactorRequired: false, session };
419
442
  }
420
443
  /** POST /auth/2fa/verify — redeems a 2FA challenge from `login()` and stores the session. */
@@ -423,7 +446,7 @@ var AuthyonClient = class {
423
446
  method: "POST",
424
447
  body: params
425
448
  });
426
- return this.hydrateUser(this.setSession({ tokens: data.tokens }, "signed_in"));
449
+ return this.hydrateUser(this.setSession({ tokens: readTokens(data) }, "signed_in"));
427
450
  }
428
451
  /** POST /auth/refresh — rotates the single-use refresh token (single-flight). */
429
452
  async refresh() {
@@ -436,7 +459,7 @@ var AuthyonClient = class {
436
459
  body: { refreshToken: current.refreshToken }
437
460
  }).then(
438
461
  (data) => this.setSession(
439
- { tokens: data.tokens, user: current.user },
462
+ { tokens: readTokens(data), user: current.user },
440
463
  "refreshed"
441
464
  )
442
465
  ).catch((error) => {
@@ -471,21 +494,32 @@ var AuthyonClient = class {
471
494
  this.clearSession();
472
495
  }
473
496
  // ── Token verification ───────────────────────────────────────────────────
474
- /** 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
+ */
475
507
  async introspect(token) {
476
508
  const accessToken = token ?? await this.getAccessToken();
477
509
  return this.request("/auth/introspect", { method: "POST", body: { token: accessToken } });
478
510
  }
479
- /** 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
+ */
480
516
  async validate(token) {
481
517
  const accessToken = token ?? await this.getAccessToken();
482
- const raw = await this.request("/auth/validate", {
483
- method: "POST",
484
- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0
485
- });
518
+ const raw = await this.request("/auth/validate", { method: "POST", body: { token: accessToken } });
486
519
  return {
487
- user: normalizeUser(raw.user),
488
- organization: raw.organization ?? raw.tenant ?? null
520
+ valid: raw.valid,
521
+ reason: raw.reason ?? null,
522
+ user: raw.profile ? normalizeUser(raw.profile) : null
489
523
  };
490
524
  }
491
525
  };
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 {
@@ -181,17 +185,32 @@ interface SessionInfo {
181
185
  lastUsedAt?: string | null;
182
186
  lastUsedFromIp?: string | null;
183
187
  }
188
+ /** POST /auth/introspect (RFC 7662) — confirmed against the live API. */
184
189
  interface IntrospectResult {
185
190
  active: boolean;
186
191
  sub?: string;
192
+ username?: string | null;
193
+ email?: string | null;
194
+ roles?: string[] | null;
195
+ permissions?: string[];
187
196
  client_id?: string;
188
197
  scope?: string;
189
198
  exp?: number;
199
+ iat?: number;
200
+ jti?: string;
190
201
  token_type?: string;
191
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
+ */
192
210
  interface ValidateResult {
193
- user: User;
194
- organization?: Organization | null;
211
+ valid: boolean;
212
+ reason?: string | null;
213
+ user: User | null;
195
214
  }
196
215
  type AuthEvent = {
197
216
  type: "signed_in";
@@ -349,8 +368,13 @@ declare class AuthyonClient {
349
368
  /** The organization the current session is scoped to, from the cached session — no network call. */
350
369
  current: () => Organization | null;
351
370
  members: {
352
- /** GET /auth/tenants/{organizationId}/members — list an organization's members. */
353
- 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>>;
354
378
  /** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
355
379
  invite: (organizationId: string, params: InviteMemberParams) => Promise<void>;
356
380
  /** DELETE /auth/tenants/{organizationId}/members/{userId} — remove a member. */
@@ -405,9 +429,22 @@ declare class AuthyonClient {
405
429
  assertionStart: (challengeToken: string) => Promise<WebAuthnCeremonyStart>;
406
430
  };
407
431
  };
408
- /** 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
+ */
409
442
  introspect(token?: string): Promise<IntrospectResult>;
410
- /** 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
+ */
411
448
  validate(token?: string): Promise<ValidateResult>;
412
449
  }
413
450
  /** Convenience factory: `const authyon = createClient({ envKey: "pk_live_..." })`. */
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 {
@@ -181,17 +185,32 @@ interface SessionInfo {
181
185
  lastUsedAt?: string | null;
182
186
  lastUsedFromIp?: string | null;
183
187
  }
188
+ /** POST /auth/introspect (RFC 7662) — confirmed against the live API. */
184
189
  interface IntrospectResult {
185
190
  active: boolean;
186
191
  sub?: string;
192
+ username?: string | null;
193
+ email?: string | null;
194
+ roles?: string[] | null;
195
+ permissions?: string[];
187
196
  client_id?: string;
188
197
  scope?: string;
189
198
  exp?: number;
199
+ iat?: number;
200
+ jti?: string;
190
201
  token_type?: string;
191
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
+ */
192
210
  interface ValidateResult {
193
- user: User;
194
- organization?: Organization | null;
211
+ valid: boolean;
212
+ reason?: string | null;
213
+ user: User | null;
195
214
  }
196
215
  type AuthEvent = {
197
216
  type: "signed_in";
@@ -349,8 +368,13 @@ declare class AuthyonClient {
349
368
  /** The organization the current session is scoped to, from the cached session — no network call. */
350
369
  current: () => Organization | null;
351
370
  members: {
352
- /** GET /auth/tenants/{organizationId}/members — list an organization's members. */
353
- 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>>;
354
378
  /** POST /auth/tenants/{organizationId}/members — invite a member by e-mail. */
355
379
  invite: (organizationId: string, params: InviteMemberParams) => Promise<void>;
356
380
  /** DELETE /auth/tenants/{organizationId}/members/{userId} — remove a member. */
@@ -405,9 +429,22 @@ declare class AuthyonClient {
405
429
  assertionStart: (challengeToken: string) => Promise<WebAuthnCeremonyStart>;
406
430
  };
407
431
  };
408
- /** 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
+ */
409
442
  introspect(token?: string): Promise<IntrospectResult>;
410
- /** 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
+ */
411
448
  validate(token?: string): Promise<ValidateResult>;
412
449
  }
413
450
  /** Convenience factory: `const authyon = createClient({ envKey: "pk_live_..." })`. */
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({ tokens: data.tokens }, "signed_in")).then((session) => this.hydrateUser(session))
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,7 +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((data) => this.setSession({ tokens: data.tokens }, "signed_in")).then((session) => this.hydrateUser(session))
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))
106
122
  };
107
123
  // ── User ─────────────────────────────────────────────────────────────────
108
124
  this.user = {
@@ -160,11 +176,16 @@ var AuthyonClient = class {
160
176
  method: "POST",
161
177
  bearer: true,
162
178
  body: { tenantSlug: organizationSlug }
163
- }).then((data) => this.setSession({ tokens: data.tokens }, "refreshed")).then((session) => this.hydrateUser(session)),
179
+ }).then((data) => this.setSession({ tokens: readTokens(data) }, "refreshed")).then((session) => this.hydrateUser(session)),
164
180
  /** The organization the current session is scoped to, from the cached session — no network call. */
165
181
  current: () => this.getSession()?.user?.activeOrganization ?? null,
166
182
  members: {
167
- /** 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
+ */
168
189
  list: (organizationId, params = {}) => this.request(
169
190
  `/auth/tenants/${encodeURIComponent(organizationId)}/members?${toQuery(params)}`,
170
191
  { bearer: true }
@@ -382,7 +403,9 @@ var AuthyonClient = class {
382
403
  if (data.twoFactor) {
383
404
  return { twoFactorRequired: true, ...data.twoFactor };
384
405
  }
385
- const session = await this.hydrateUser(this.setSession({ tokens: data.tokens }, "signed_in"));
406
+ const session = await this.hydrateUser(
407
+ this.setSession({ tokens: readTokens(data) }, "signed_in")
408
+ );
386
409
  return { twoFactorRequired: false, session };
387
410
  }
388
411
  /** POST /auth/2fa/verify — redeems a 2FA challenge from `login()` and stores the session. */
@@ -391,7 +414,7 @@ var AuthyonClient = class {
391
414
  method: "POST",
392
415
  body: params
393
416
  });
394
- return this.hydrateUser(this.setSession({ tokens: data.tokens }, "signed_in"));
417
+ return this.hydrateUser(this.setSession({ tokens: readTokens(data) }, "signed_in"));
395
418
  }
396
419
  /** POST /auth/refresh — rotates the single-use refresh token (single-flight). */
397
420
  async refresh() {
@@ -404,7 +427,7 @@ var AuthyonClient = class {
404
427
  body: { refreshToken: current.refreshToken }
405
428
  }).then(
406
429
  (data) => this.setSession(
407
- { tokens: data.tokens, user: current.user },
430
+ { tokens: readTokens(data), user: current.user },
408
431
  "refreshed"
409
432
  )
410
433
  ).catch((error) => {
@@ -439,21 +462,32 @@ var AuthyonClient = class {
439
462
  this.clearSession();
440
463
  }
441
464
  // ── Token verification ───────────────────────────────────────────────────
442
- /** 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
+ */
443
475
  async introspect(token) {
444
476
  const accessToken = token ?? await this.getAccessToken();
445
477
  return this.request("/auth/introspect", { method: "POST", body: { token: accessToken } });
446
478
  }
447
- /** 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
+ */
448
484
  async validate(token) {
449
485
  const accessToken = token ?? await this.getAccessToken();
450
- const raw = await this.request("/auth/validate", {
451
- method: "POST",
452
- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : void 0
453
- });
486
+ const raw = await this.request("/auth/validate", { method: "POST", body: { token: accessToken } });
454
487
  return {
455
- user: normalizeUser(raw.user),
456
- organization: raw.organization ?? raw.tenant ?? null
488
+ valid: raw.valid,
489
+ reason: raw.reason ?? null,
490
+ user: raw.profile ? normalizeUser(raw.profile) : null
457
491
  };
458
492
  }
459
493
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authyon/auth",
3
- "version": "0.1.4",
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",