@uln/auth 0.6.0 → 0.6.1

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/client.js CHANGED
@@ -50,7 +50,9 @@ async function createToken(config, user, options = {}) {
50
50
  kid: config.kid,
51
51
  iat: now,
52
52
  exp: now + config.maxAgeSeconds,
53
- ...user.audience === undefined ? {} : { aud: user.audience }
53
+ ...user.audience === undefined ? {} : { aud: user.audience },
54
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
55
+ ...user.deviceBound ? { dbsc: true } : {}
54
56
  };
55
57
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
56
58
  const payloadBytes = encoder.encode(payloadB64);
@@ -73,7 +75,7 @@ function isTokenPayload(value) {
73
75
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
74
76
  return false;
75
77
  }
76
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
78
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
77
79
  }
78
80
  async function validateToken(config, token) {
79
81
  const parts = token.split(".");
@@ -276,12 +278,16 @@ function extractCookie(cookieHeader, name) {
276
278
  }
277
279
  return null;
278
280
  }
281
+ // src/version.ts
282
+ var LIBRARY_VERSION = "0.6.1";
283
+
279
284
  // src/flow.ts
280
285
  var DEFAULT_AUTH_URL = "https://auth.uln.industries";
281
286
  var STATE_COOKIE = "__Host-uln_auth_state";
282
287
  var INSECURE_STATE_COOKIE = "uln_auth_state";
283
288
  var STATE_PARAM = "uln_state";
284
289
  var CHALLENGE_PARAM = "uln_challenge";
290
+ var VERSION_PARAM = "uln_v";
285
291
  var STATE_MAX_AGE_SECONDS = 600;
286
292
  function base64url(bytes) {
287
293
  let binary = "";
@@ -311,6 +317,7 @@ function stripCallbackParams(url) {
311
317
  url.searchParams.delete("code");
312
318
  url.searchParams.delete(STATE_PARAM);
313
319
  url.searchParams.delete(CHALLENGE_PARAM);
320
+ url.searchParams.delete(VERSION_PARAM);
314
321
  }
315
322
  async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
316
323
  const returnUrl = new URL(currentUrl);
@@ -319,6 +326,7 @@ async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
319
326
  const verifier = randomToken();
320
327
  returnUrl.searchParams.set(STATE_PARAM, state);
321
328
  returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
329
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
322
330
  const loginUrl = new URL("/login", authUrl);
323
331
  loginUrl.searchParams.set("redirect", returnUrl.toString());
324
332
  loginUrl.searchParams.set("response_type", "code");
package/dist/flow.d.ts CHANGED
@@ -17,6 +17,11 @@ export declare const STATE_PARAM = "uln_state";
17
17
  * code that leaks (history, logs, a shared link) is useless on its own.
18
18
  */
19
19
  export declare const CHALLENGE_PARAM = "uln_challenge";
20
+ /**
21
+ * This library's version on the return URL (0.6.1+), so the auth service can track which version each site runs.
22
+ * The auth service strips it from the URL it sends the browser back to.
23
+ */
24
+ export declare const VERSION_PARAM = "uln_v";
20
25
  /** S256 code challenge for a PKCE verifier: base64url(SHA-256(verifier)). */
21
26
  export declare function pkceChallenge(verifier: string): Promise<string>;
22
27
  /**
package/dist/flow.js CHANGED
@@ -74,7 +74,9 @@ async function createToken(config, user, options = {}) {
74
74
  kid: config.kid,
75
75
  iat: now,
76
76
  exp: now + config.maxAgeSeconds,
77
- ...user.audience === undefined ? {} : { aud: user.audience }
77
+ ...user.audience === undefined ? {} : { aud: user.audience },
78
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
79
+ ...user.deviceBound ? { dbsc: true } : {}
78
80
  };
79
81
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
80
82
  const payloadBytes = encoder.encode(payloadB64);
@@ -97,7 +99,7 @@ function isTokenPayload(value) {
97
99
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
98
100
  return false;
99
101
  }
100
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
102
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
101
103
  }
102
104
  async function validateToken(config, token) {
103
105
  const parts = token.split(".");
@@ -277,12 +279,16 @@ async function fetchJwks(auth) {
277
279
  return entries;
278
280
  }
279
281
 
282
+ // src/version.ts
283
+ var LIBRARY_VERSION = "0.6.1";
284
+
280
285
  // src/flow.ts
281
286
  var DEFAULT_AUTH_URL = "https://auth.uln.industries";
282
287
  var STATE_COOKIE = "__Host-uln_auth_state";
283
288
  var INSECURE_STATE_COOKIE = "uln_auth_state";
284
289
  var STATE_PARAM = "uln_state";
285
290
  var CHALLENGE_PARAM = "uln_challenge";
291
+ var VERSION_PARAM = "uln_v";
286
292
  var STATE_MAX_AGE_SECONDS = 600;
287
293
  function base64url(bytes) {
288
294
  let binary = "";
@@ -312,6 +318,7 @@ function stripCallbackParams(url) {
312
318
  url.searchParams.delete("code");
313
319
  url.searchParams.delete(STATE_PARAM);
314
320
  url.searchParams.delete(CHALLENGE_PARAM);
321
+ url.searchParams.delete(VERSION_PARAM);
315
322
  }
316
323
  async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
317
324
  const returnUrl = new URL(currentUrl);
@@ -320,6 +327,7 @@ async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
320
327
  const verifier = randomToken();
321
328
  returnUrl.searchParams.set(STATE_PARAM, state);
322
329
  returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
330
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
323
331
  const loginUrl = new URL("/login", authUrl);
324
332
  loginUrl.searchParams.set("redirect", returnUrl.toString());
325
333
  loginUrl.searchParams.set("response_type", "code");
@@ -385,6 +393,7 @@ export {
385
393
  INSECURE_STATE_COOKIE,
386
394
  STATE_COOKIE,
387
395
  STATE_PARAM,
396
+ VERSION_PARAM,
388
397
  clearRevalidationCache,
389
398
  handleLoginCallback,
390
399
  pkceChallenge,
package/dist/hono.js CHANGED
@@ -74,7 +74,9 @@ async function createToken(config, user, options = {}) {
74
74
  kid: config.kid,
75
75
  iat: now,
76
76
  exp: now + config.maxAgeSeconds,
77
- ...user.audience === undefined ? {} : { aud: user.audience }
77
+ ...user.audience === undefined ? {} : { aud: user.audience },
78
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
79
+ ...user.deviceBound ? { dbsc: true } : {}
78
80
  };
79
81
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
80
82
  const payloadBytes = encoder.encode(payloadB64);
@@ -97,7 +99,7 @@ function isTokenPayload(value) {
97
99
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
98
100
  return false;
99
101
  }
100
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
102
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
101
103
  }
102
104
  async function validateToken(config, token) {
103
105
  const parts = token.split(".");
@@ -277,12 +279,16 @@ async function fetchJwks(auth) {
277
279
  return entries;
278
280
  }
279
281
 
282
+ // src/version.ts
283
+ var LIBRARY_VERSION = "0.6.1";
284
+
280
285
  // src/flow.ts
281
286
  var DEFAULT_AUTH_URL = "https://auth.uln.industries";
282
287
  var STATE_COOKIE = "__Host-uln_auth_state";
283
288
  var INSECURE_STATE_COOKIE = "uln_auth_state";
284
289
  var STATE_PARAM = "uln_state";
285
290
  var CHALLENGE_PARAM = "uln_challenge";
291
+ var VERSION_PARAM = "uln_v";
286
292
  var STATE_MAX_AGE_SECONDS = 600;
287
293
  function base64url(bytes) {
288
294
  let binary = "";
@@ -312,6 +318,7 @@ function stripCallbackParams(url) {
312
318
  url.searchParams.delete("code");
313
319
  url.searchParams.delete(STATE_PARAM);
314
320
  url.searchParams.delete(CHALLENGE_PARAM);
321
+ url.searchParams.delete(VERSION_PARAM);
315
322
  }
316
323
  async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
317
324
  const returnUrl = new URL(currentUrl);
@@ -320,6 +327,7 @@ async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
320
327
  const verifier = randomToken();
321
328
  returnUrl.searchParams.set(STATE_PARAM, state);
322
329
  returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
330
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
323
331
  const loginUrl = new URL("/login", authUrl);
324
332
  loginUrl.searchParams.set("redirect", returnUrl.toString());
325
333
  loginUrl.searchParams.set("response_type", "code");
package/dist/index.js CHANGED
@@ -73,7 +73,9 @@ async function createToken(config, user, options = {}) {
73
73
  kid: config.kid,
74
74
  iat: now,
75
75
  exp: now + config.maxAgeSeconds,
76
- ...user.audience === undefined ? {} : { aud: user.audience }
76
+ ...user.audience === undefined ? {} : { aud: user.audience },
77
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
78
+ ...user.deviceBound ? { dbsc: true } : {}
77
79
  };
78
80
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
79
81
  const payloadBytes = encoder.encode(payloadB64);
@@ -96,7 +98,7 @@ function isTokenPayload(value) {
96
98
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
97
99
  return false;
98
100
  }
99
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
101
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
100
102
  }
101
103
  async function validateToken(config, token) {
102
104
  const parts = token.split(".");
@@ -276,12 +278,16 @@ async function fetchJwks(auth) {
276
278
  return entries;
277
279
  }
278
280
 
281
+ // src/version.ts
282
+ var LIBRARY_VERSION = "0.6.1";
283
+
279
284
  // src/flow.ts
280
285
  var DEFAULT_AUTH_URL = "https://auth.uln.industries";
281
286
  var STATE_COOKIE = "__Host-uln_auth_state";
282
287
  var INSECURE_STATE_COOKIE = "uln_auth_state";
283
288
  var STATE_PARAM = "uln_state";
284
289
  var CHALLENGE_PARAM = "uln_challenge";
290
+ var VERSION_PARAM = "uln_v";
285
291
  var STATE_MAX_AGE_SECONDS = 600;
286
292
  function base64url(bytes) {
287
293
  let binary = "";
@@ -311,6 +317,7 @@ function stripCallbackParams(url) {
311
317
  url.searchParams.delete("code");
312
318
  url.searchParams.delete(STATE_PARAM);
313
319
  url.searchParams.delete(CHALLENGE_PARAM);
320
+ url.searchParams.delete(VERSION_PARAM);
314
321
  }
315
322
  async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
316
323
  const returnUrl = new URL(currentUrl);
@@ -319,6 +326,7 @@ async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
319
326
  const verifier = randomToken();
320
327
  returnUrl.searchParams.set(STATE_PARAM, state);
321
328
  returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
329
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
322
330
  const loginUrl = new URL("/login", authUrl);
323
331
  loginUrl.searchParams.set("redirect", returnUrl.toString());
324
332
  loginUrl.searchParams.set("response_type", "code");
@@ -74,7 +74,9 @@ async function createToken(config, user, options = {}) {
74
74
  kid: config.kid,
75
75
  iat: now,
76
76
  exp: now + config.maxAgeSeconds,
77
- ...user.audience === undefined ? {} : { aud: user.audience }
77
+ ...user.audience === undefined ? {} : { aud: user.audience },
78
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
79
+ ...user.deviceBound ? { dbsc: true } : {}
78
80
  };
79
81
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
80
82
  const payloadBytes = encoder.encode(payloadB64);
@@ -97,7 +99,7 @@ function isTokenPayload(value) {
97
99
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
98
100
  return false;
99
101
  }
100
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
102
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
101
103
  }
102
104
  async function validateToken(config, token) {
103
105
  const parts = token.split(".");
@@ -277,12 +279,16 @@ async function fetchJwks(auth) {
277
279
  return entries;
278
280
  }
279
281
 
282
+ // src/version.ts
283
+ var LIBRARY_VERSION = "0.6.1";
284
+
280
285
  // src/flow.ts
281
286
  var DEFAULT_AUTH_URL = "https://auth.uln.industries";
282
287
  var STATE_COOKIE = "__Host-uln_auth_state";
283
288
  var INSECURE_STATE_COOKIE = "uln_auth_state";
284
289
  var STATE_PARAM = "uln_state";
285
290
  var CHALLENGE_PARAM = "uln_challenge";
291
+ var VERSION_PARAM = "uln_v";
286
292
  var STATE_MAX_AGE_SECONDS = 600;
287
293
  function base64url(bytes) {
288
294
  let binary = "";
@@ -312,6 +318,7 @@ function stripCallbackParams(url) {
312
318
  url.searchParams.delete("code");
313
319
  url.searchParams.delete(STATE_PARAM);
314
320
  url.searchParams.delete(CHALLENGE_PARAM);
321
+ url.searchParams.delete(VERSION_PARAM);
315
322
  }
316
323
  async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
317
324
  const returnUrl = new URL(currentUrl);
@@ -320,6 +327,7 @@ async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
320
327
  const verifier = randomToken();
321
328
  returnUrl.searchParams.set(STATE_PARAM, state);
322
329
  returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
330
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
323
331
  const loginUrl = new URL("/login", authUrl);
324
332
  loginUrl.searchParams.set("redirect", returnUrl.toString());
325
333
  loginUrl.searchParams.set("response_type", "code");
package/dist/remote.js CHANGED
@@ -50,7 +50,9 @@ async function createToken(config, user, options = {}) {
50
50
  kid: config.kid,
51
51
  iat: now,
52
52
  exp: now + config.maxAgeSeconds,
53
- ...user.audience === undefined ? {} : { aud: user.audience }
53
+ ...user.audience === undefined ? {} : { aud: user.audience },
54
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
55
+ ...user.deviceBound ? { dbsc: true } : {}
54
56
  };
55
57
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
56
58
  const payloadBytes = encoder.encode(payloadB64);
@@ -73,7 +75,7 @@ function isTokenPayload(value) {
73
75
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
74
76
  return false;
75
77
  }
76
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
78
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
77
79
  }
78
80
  async function validateToken(config, token) {
79
81
  const parts = token.split(".");
package/dist/service.js CHANGED
@@ -74,7 +74,9 @@ async function createToken(config, user, options = {}) {
74
74
  kid: config.kid,
75
75
  iat: now,
76
76
  exp: now + config.maxAgeSeconds,
77
- ...user.audience === undefined ? {} : { aud: user.audience }
77
+ ...user.audience === undefined ? {} : { aud: user.audience },
78
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
79
+ ...user.deviceBound ? { dbsc: true } : {}
78
80
  };
79
81
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
80
82
  const payloadBytes = encoder.encode(payloadB64);
@@ -97,7 +99,7 @@ function isTokenPayload(value) {
97
99
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
98
100
  return false;
99
101
  }
100
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
102
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
101
103
  }
102
104
  async function validateToken(config, token) {
103
105
  const parts = token.split(".");
@@ -277,12 +279,16 @@ async function fetchJwks(auth) {
277
279
  return entries;
278
280
  }
279
281
 
282
+ // src/version.ts
283
+ var LIBRARY_VERSION = "0.6.1";
284
+
280
285
  // src/flow.ts
281
286
  var DEFAULT_AUTH_URL = "https://auth.uln.industries";
282
287
  var STATE_COOKIE = "__Host-uln_auth_state";
283
288
  var INSECURE_STATE_COOKIE = "uln_auth_state";
284
289
  var STATE_PARAM = "uln_state";
285
290
  var CHALLENGE_PARAM = "uln_challenge";
291
+ var VERSION_PARAM = "uln_v";
286
292
  var STATE_MAX_AGE_SECONDS = 600;
287
293
  function base64url(bytes) {
288
294
  let binary = "";
@@ -312,6 +318,7 @@ function stripCallbackParams(url) {
312
318
  url.searchParams.delete("code");
313
319
  url.searchParams.delete(STATE_PARAM);
314
320
  url.searchParams.delete(CHALLENGE_PARAM);
321
+ url.searchParams.delete(VERSION_PARAM);
315
322
  }
316
323
  async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
317
324
  const returnUrl = new URL(currentUrl);
@@ -320,6 +327,7 @@ async function redirectToLogin(currentUrl, authUrl = DEFAULT_AUTH_URL) {
320
327
  const verifier = randomToken();
321
328
  returnUrl.searchParams.set(STATE_PARAM, state);
322
329
  returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
330
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
323
331
  const loginUrl = new URL("/login", authUrl);
324
332
  loginUrl.searchParams.set("redirect", returnUrl.toString());
325
333
  loginUrl.searchParams.set("response_type", "code");
package/dist/token.d.ts CHANGED
@@ -18,6 +18,10 @@ export interface CreateTokenInput {
18
18
  flags: Record<string, string[]>;
19
19
  /** The origin the token is for (see `TokenPayload.aud`). */
20
20
  audience?: string;
21
+ /** See `TokenPayload.sid`. */
22
+ sessionId?: string;
23
+ /** See `TokenPayload.dbsc`. */
24
+ deviceBound?: boolean;
21
25
  }
22
26
  export interface CreateTokenOptions {
23
27
  /** Unix seconds to use as `iat` instead of now (e.g. to land just after a session revocation cut-off). */
package/dist/token.js CHANGED
@@ -50,7 +50,9 @@ async function createToken(config, user, options = {}) {
50
50
  kid: config.kid,
51
51
  iat: now,
52
52
  exp: now + config.maxAgeSeconds,
53
- ...user.audience === undefined ? {} : { aud: user.audience }
53
+ ...user.audience === undefined ? {} : { aud: user.audience },
54
+ ...user.sessionId === undefined ? {} : { sid: user.sessionId },
55
+ ...user.deviceBound ? { dbsc: true } : {}
54
56
  };
55
57
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
56
58
  const payloadBytes = encoder.encode(payloadB64);
@@ -73,7 +75,7 @@ function isTokenPayload(value) {
73
75
  if (!("sub" in value) || !("email" in value) || !("name" in value) || !("admin" in value) || !("scopes" in value) || !("flags" in value) || !("kid" in value) || !("iat" in value) || !("exp" in value)) {
74
76
  return false;
75
77
  }
76
- return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string");
78
+ return typeof value.sub === "string" && typeof value.email === "string" && typeof value.name === "string" && typeof value.admin === "boolean" && isStringArray(value.scopes) && isFlags(value.flags) && typeof value.kid === "string" && Number.isSafeInteger(value.iat) && Number.isSafeInteger(value.exp) && (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") && (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") && (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean");
77
79
  }
78
80
  async function validateToken(config, token) {
79
81
  const parts = token.split(".");
package/dist/types.d.ts CHANGED
@@ -51,6 +51,13 @@ export interface TokenPayload {
51
51
  * (library < 0.5); current verifiers reject those.
52
52
  */
53
53
  aud?: string;
54
+ /** Auth-service sessions only: the server-side session this token belongs to (worker `sessions` table). */
55
+ sid?: string;
56
+ /**
57
+ * Auth-service sessions only: a short-lived token for a Device Bound Session (DBSC), renewed by a proof from a key
58
+ * held by the user's device. Once a session is bound, only these tokens are honoured for it.
59
+ */
60
+ dbsc?: boolean;
54
61
  }
55
62
  export interface PublicKeyEntry {
56
63
  kid: string;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * This library's version. Sent to the auth service on every login (`uln_v` on the return URL) so Admin → Sites can show
3
+ * which version each site runs and open upgrade PRs for the ones behind. `src/version.test.ts` keeps it equal to
4
+ * `package.json`.
5
+ */
6
+ export declare const LIBRARY_VERSION = "0.6.1";
@@ -0,0 +1,6 @@
1
+ // @bun
2
+ // src/version.ts
3
+ var LIBRARY_VERSION = "0.6.1";
4
+ export {
5
+ LIBRARY_VERSION
6
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uln/auth",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ulnd/auth.git"
package/src/flow.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { extractCookie, makeSessionCookie } from "./cookie.ts";
2
2
  import { checkTokenRemote, exchangeAuthCode, type FetchLike, hasSiteAccessFor } from "./remote.ts";
3
3
  import type { CookieConfig, TokenPayload } from "./types.ts";
4
+ import { LIBRARY_VERSION } from "./version.ts";
4
5
 
5
6
  // The one implementation of the site side of the login flow, shared by `@uln/auth/client`, `/service` and `/hono`.
6
7
 
@@ -22,6 +23,11 @@ export const STATE_PARAM = "uln_state";
22
23
  * code that leaks (history, logs, a shared link) is useless on its own.
23
24
  */
24
25
  export const CHALLENGE_PARAM = "uln_challenge";
26
+ /**
27
+ * This library's version on the return URL (0.6.1+), so the auth service can track which version each site runs.
28
+ * The auth service strips it from the URL it sends the browser back to.
29
+ */
30
+ export const VERSION_PARAM = "uln_v";
25
31
  const STATE_MAX_AGE_SECONDS = 600;
26
32
 
27
33
  function base64url(bytes: Uint8Array): string {
@@ -58,6 +64,7 @@ function stripCallbackParams(url: URL): void {
58
64
  url.searchParams.delete("code");
59
65
  url.searchParams.delete(STATE_PARAM);
60
66
  url.searchParams.delete(CHALLENGE_PARAM);
67
+ url.searchParams.delete(VERSION_PARAM);
61
68
  }
62
69
 
63
70
  /**
@@ -72,6 +79,7 @@ export async function redirectToLogin(currentUrl: string, authUrl: string = DEFA
72
79
  const verifier = randomToken();
73
80
  returnUrl.searchParams.set(STATE_PARAM, state);
74
81
  returnUrl.searchParams.set(CHALLENGE_PARAM, await pkceChallenge(verifier));
82
+ returnUrl.searchParams.set(VERSION_PARAM, LIBRARY_VERSION);
75
83
 
76
84
  const loginUrl = new URL("/login", authUrl);
77
85
  loginUrl.searchParams.set("redirect", returnUrl.toString());
package/src/token.ts CHANGED
@@ -55,6 +55,10 @@ export interface CreateTokenInput {
55
55
  flags: Record<string, string[]>;
56
56
  /** The origin the token is for (see `TokenPayload.aud`). */
57
57
  audience?: string;
58
+ /** See `TokenPayload.sid`. */
59
+ sessionId?: string;
60
+ /** See `TokenPayload.dbsc`. */
61
+ deviceBound?: boolean;
58
62
  }
59
63
 
60
64
  export interface CreateTokenOptions {
@@ -80,6 +84,8 @@ export async function createToken(
80
84
  iat: now,
81
85
  exp: now + config.maxAgeSeconds,
82
86
  ...(user.audience === undefined ? {} : { aud: user.audience }),
87
+ ...(user.sessionId === undefined ? {} : { sid: user.sessionId }),
88
+ ...(user.deviceBound ? { dbsc: true } : {}),
83
89
  };
84
90
  const payloadB64 = base64urlEncode(encoder.encode(JSON.stringify(payload)));
85
91
  const payloadBytes = encoder.encode(payloadB64);
@@ -126,7 +132,9 @@ export function isTokenPayload(value: unknown): value is TokenPayload {
126
132
  typeof value.kid === "string" &&
127
133
  Number.isSafeInteger(value.iat) &&
128
134
  Number.isSafeInteger(value.exp) &&
129
- (!("aud" in value) || value.aud === undefined || typeof value.aud === "string")
135
+ (!("aud" in value) || value.aud === undefined || typeof value.aud === "string") &&
136
+ (!("sid" in value) || value.sid === undefined || typeof value.sid === "string") &&
137
+ (!("dbsc" in value) || value.dbsc === undefined || typeof value.dbsc === "boolean")
130
138
  );
131
139
  }
132
140
 
package/src/types.ts CHANGED
@@ -56,6 +56,13 @@ export interface TokenPayload {
56
56
  * (library < 0.5); current verifiers reject those.
57
57
  */
58
58
  aud?: string;
59
+ /** Auth-service sessions only: the server-side session this token belongs to (worker `sessions` table). */
60
+ sid?: string;
61
+ /**
62
+ * Auth-service sessions only: a short-lived token for a Device Bound Session (DBSC), renewed by a proof from a key
63
+ * held by the user's device. Once a session is bound, only these tokens are honoured for it.
64
+ */
65
+ dbsc?: boolean;
59
66
  }
60
67
 
61
68
  export interface PublicKeyEntry {
package/src/version.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * This library's version. Sent to the auth service on every login (`uln_v` on the return URL) so Admin → Sites can show
3
+ * which version each site runs and open upgrade PRs for the ones behind. `src/version.test.ts` keeps it equal to
4
+ * `package.json`.
5
+ */
6
+ export const LIBRARY_VERSION = "0.6.1";