@onjunctiondev/auth 0.2.0 → 0.2.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/index.cjs CHANGED
@@ -61,29 +61,80 @@ function localStorageStorage(key) {
61
61
  }
62
62
  };
63
63
  }
64
+ function cookieStorage(key) {
65
+ const canUse = typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
66
+ return {
67
+ get() {
68
+ if (!canUse) return null;
69
+ try {
70
+ const jar = globalThis.document.cookie;
71
+ if (!jar) return null;
72
+ const prefix = `${key}=`;
73
+ for (const part of jar.split("; ")) {
74
+ if (part.startsWith(prefix)) {
75
+ const value = part.slice(prefix.length);
76
+ if (!value) return null;
77
+ return JSON.parse(decodeURIComponent(value));
78
+ }
79
+ }
80
+ return null;
81
+ } catch {
82
+ return null;
83
+ }
84
+ },
85
+ set(session) {
86
+ if (!canUse) return;
87
+ try {
88
+ const doc = globalThis.document;
89
+ const loc = globalThis.location;
90
+ const isHttps = loc?.protocol === "https:";
91
+ const secure = isHttps ? "; Secure" : "";
92
+ if (session) {
93
+ const now = Math.floor(Date.now() / 1e3);
94
+ const maxAge = Math.max(0, session.expires_at - now);
95
+ const value = encodeURIComponent(JSON.stringify(session));
96
+ doc.cookie = `${key}=${value}; Path=/; Max-Age=${maxAge}; SameSite=Lax${secure}`;
97
+ } else {
98
+ doc.cookie = `${key}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
99
+ }
100
+ } catch {
101
+ }
102
+ }
103
+ };
104
+ }
64
105
 
65
106
  // src/client.ts
66
- var DEFAULT_STORAGE_KEY = "junction.auth.session";
67
- function pickStorage(opts) {
68
- const key = opts.storageKey ?? DEFAULT_STORAGE_KEY;
107
+ var DEFAULT_STORAGE_KEY = "__junction_session";
108
+ function pickStorage(opts, key) {
69
109
  if (opts.storage === "memory") return memoryStorage();
70
- if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
71
- return localStorageStorage(key);
72
- }
110
+ if (opts.storage === "localStorage") return localStorageStorage(key);
111
+ const hasDocument = typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
112
+ if (hasDocument) return cookieStorage(key);
73
113
  return memoryStorage();
74
114
  }
75
115
  var AuthClient = class {
76
116
  #deps;
77
117
  #listeners = /* @__PURE__ */ new Set();
118
+ #channel;
119
+ #selfId;
78
120
  // Refresh in-flight — concurrent 401s share one refresh so we don't
79
121
  // race-rotate the single-use token on the server.
80
122
  #inflightRefresh = null;
81
123
  constructor(opts) {
124
+ const key = opts.storageKey ?? DEFAULT_STORAGE_KEY;
82
125
  this.#deps = {
83
126
  url: opts.url.replace(/\/+$/, ""),
84
127
  fetchFn: opts.fetch ?? ((...args) => fetch(...args)),
85
- storage: pickStorage(opts)
128
+ storage: pickStorage(opts, key)
86
129
  };
130
+ this.#selfId = randomId();
131
+ this.#channel = openChannel(`junction.auth.${key}`);
132
+ this.#channel?.addEventListener("message", (e) => {
133
+ const payload = e.data;
134
+ if (!payload || payload.from === this.#selfId) return;
135
+ this.#deps.storage.set(payload.session);
136
+ this.#emitLocal(payload.event, payload.session);
137
+ });
87
138
  }
88
139
  // --- public API --------------------------------------------------------
89
140
  async signUp(credentials) {
@@ -202,7 +253,22 @@ var AuthClient = class {
202
253
  async #rawFetch(path, init) {
203
254
  return this.#deps.fetchFn(`${this.#deps.url}${path}`, init);
204
255
  }
256
+ // Emits locally AND broadcasts to peer tabs.
205
257
  #emit(event, session) {
258
+ this.#emitLocal(event, session);
259
+ try {
260
+ this.#channel?.postMessage({
261
+ from: this.#selfId,
262
+ event,
263
+ session
264
+ });
265
+ } catch {
266
+ }
267
+ }
268
+ // Emits to in-process subscribers only — used both by local actions
269
+ // (via #emit, which also broadcasts) and by inbound broadcast messages
270
+ // (direct, so we don't re-broadcast and loop).
271
+ #emitLocal(event, session) {
206
272
  for (const l of this.#listeners) {
207
273
  try {
208
274
  l(event, session);
@@ -211,6 +277,20 @@ var AuthClient = class {
211
277
  }
212
278
  }
213
279
  };
280
+ function openChannel(name) {
281
+ const Ctor = globalThis.BroadcastChannel;
282
+ if (!Ctor) return null;
283
+ try {
284
+ return new Ctor(name);
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+ function randomId() {
290
+ const crypto = globalThis.crypto;
291
+ if (crypto?.randomUUID) return crypto.randomUUID();
292
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
293
+ }
214
294
  function errorFrom(code, message, status) {
215
295
  return { code, message, status };
216
296
  }
package/dist/index.d.cts CHANGED
@@ -34,7 +34,7 @@ type AuthEvent = "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED"
34
34
  type AuthStateChangeHandler = (event: AuthEvent, session: AuthSession | null) => void;
35
35
  type ClientOptions = {
36
36
  url: string;
37
- storage?: "localStorage" | "memory";
37
+ storage?: "cookie" | "localStorage" | "memory";
38
38
  storageKey?: string;
39
39
  fetch?: typeof fetch;
40
40
  };
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@ type AuthEvent = "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED"
34
34
  type AuthStateChangeHandler = (event: AuthEvent, session: AuthSession | null) => void;
35
35
  type ClientOptions = {
36
36
  url: string;
37
- storage?: "localStorage" | "memory";
37
+ storage?: "cookie" | "localStorage" | "memory";
38
38
  storageKey?: string;
39
39
  fetch?: typeof fetch;
40
40
  };
package/dist/index.js CHANGED
@@ -34,29 +34,80 @@ function localStorageStorage(key) {
34
34
  }
35
35
  };
36
36
  }
37
+ function cookieStorage(key) {
38
+ const canUse = typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
39
+ return {
40
+ get() {
41
+ if (!canUse) return null;
42
+ try {
43
+ const jar = globalThis.document.cookie;
44
+ if (!jar) return null;
45
+ const prefix = `${key}=`;
46
+ for (const part of jar.split("; ")) {
47
+ if (part.startsWith(prefix)) {
48
+ const value = part.slice(prefix.length);
49
+ if (!value) return null;
50
+ return JSON.parse(decodeURIComponent(value));
51
+ }
52
+ }
53
+ return null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ },
58
+ set(session) {
59
+ if (!canUse) return;
60
+ try {
61
+ const doc = globalThis.document;
62
+ const loc = globalThis.location;
63
+ const isHttps = loc?.protocol === "https:";
64
+ const secure = isHttps ? "; Secure" : "";
65
+ if (session) {
66
+ const now = Math.floor(Date.now() / 1e3);
67
+ const maxAge = Math.max(0, session.expires_at - now);
68
+ const value = encodeURIComponent(JSON.stringify(session));
69
+ doc.cookie = `${key}=${value}; Path=/; Max-Age=${maxAge}; SameSite=Lax${secure}`;
70
+ } else {
71
+ doc.cookie = `${key}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
72
+ }
73
+ } catch {
74
+ }
75
+ }
76
+ };
77
+ }
37
78
 
38
79
  // src/client.ts
39
- var DEFAULT_STORAGE_KEY = "junction.auth.session";
40
- function pickStorage(opts) {
41
- const key = opts.storageKey ?? DEFAULT_STORAGE_KEY;
80
+ var DEFAULT_STORAGE_KEY = "__junction_session";
81
+ function pickStorage(opts, key) {
42
82
  if (opts.storage === "memory") return memoryStorage();
43
- if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
44
- return localStorageStorage(key);
45
- }
83
+ if (opts.storage === "localStorage") return localStorageStorage(key);
84
+ const hasDocument = typeof globalThis !== "undefined" && typeof globalThis.document !== "undefined";
85
+ if (hasDocument) return cookieStorage(key);
46
86
  return memoryStorage();
47
87
  }
48
88
  var AuthClient = class {
49
89
  #deps;
50
90
  #listeners = /* @__PURE__ */ new Set();
91
+ #channel;
92
+ #selfId;
51
93
  // Refresh in-flight — concurrent 401s share one refresh so we don't
52
94
  // race-rotate the single-use token on the server.
53
95
  #inflightRefresh = null;
54
96
  constructor(opts) {
97
+ const key = opts.storageKey ?? DEFAULT_STORAGE_KEY;
55
98
  this.#deps = {
56
99
  url: opts.url.replace(/\/+$/, ""),
57
100
  fetchFn: opts.fetch ?? ((...args) => fetch(...args)),
58
- storage: pickStorage(opts)
101
+ storage: pickStorage(opts, key)
59
102
  };
103
+ this.#selfId = randomId();
104
+ this.#channel = openChannel(`junction.auth.${key}`);
105
+ this.#channel?.addEventListener("message", (e) => {
106
+ const payload = e.data;
107
+ if (!payload || payload.from === this.#selfId) return;
108
+ this.#deps.storage.set(payload.session);
109
+ this.#emitLocal(payload.event, payload.session);
110
+ });
60
111
  }
61
112
  // --- public API --------------------------------------------------------
62
113
  async signUp(credentials) {
@@ -175,7 +226,22 @@ var AuthClient = class {
175
226
  async #rawFetch(path, init) {
176
227
  return this.#deps.fetchFn(`${this.#deps.url}${path}`, init);
177
228
  }
229
+ // Emits locally AND broadcasts to peer tabs.
178
230
  #emit(event, session) {
231
+ this.#emitLocal(event, session);
232
+ try {
233
+ this.#channel?.postMessage({
234
+ from: this.#selfId,
235
+ event,
236
+ session
237
+ });
238
+ } catch {
239
+ }
240
+ }
241
+ // Emits to in-process subscribers only — used both by local actions
242
+ // (via #emit, which also broadcasts) and by inbound broadcast messages
243
+ // (direct, so we don't re-broadcast and loop).
244
+ #emitLocal(event, session) {
179
245
  for (const l of this.#listeners) {
180
246
  try {
181
247
  l(event, session);
@@ -184,6 +250,20 @@ var AuthClient = class {
184
250
  }
185
251
  }
186
252
  };
253
+ function openChannel(name) {
254
+ const Ctor = globalThis.BroadcastChannel;
255
+ if (!Ctor) return null;
256
+ try {
257
+ return new Ctor(name);
258
+ } catch {
259
+ return null;
260
+ }
261
+ }
262
+ function randomId() {
263
+ const crypto = globalThis.crypto;
264
+ if (crypto?.randomUUID) return crypto.randomUUID();
265
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
266
+ }
187
267
  function errorFrom(code, message, status) {
188
268
  return { code, message, status };
189
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onjunctiondev/auth",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Headless auth SDK for Junction — email/password sessions with per-project JWTs and RLS claims.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://onjunction.dev",