slauthjs 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.js CHANGED
@@ -18,11 +18,20 @@ export class SlauthError extends Error {
18
18
  }
19
19
  }
20
20
 
21
- export function createClient({ basePath = '/auth', fetch: doFetch = (...args) => fetch(...args) } = {}) {
21
+ export function createClient({ basePath = '/auth', fetch: doFetch = (...args) => fetch(...args), token: bearer } = {}) {
22
22
  const base = basePath.replace(/\/+$/, '')
23
+ // Apps that can't keep cookies (React Native, CLIs) pass `token: { get, set }` and get bearer tokens instead.
24
+ const extra = () => {
25
+ if (!bearer) return {}
26
+ const value = bearer.get()
27
+ return { 'X-Slauth-Transport': 'bearer', ...(value ? { Authorization: `Bearer ${value}` } : {}) }
28
+ }
23
29
 
24
30
  async function call(path, body) {
25
- const init = body === undefined ? { credentials: 'same-origin' } : { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
31
+ const init =
32
+ body === undefined
33
+ ? { credentials: 'same-origin', headers: extra() }
34
+ : { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', ...extra() }, body: JSON.stringify(body) }
26
35
  let res
27
36
  try {
28
37
  res = await doFetch(`${base}/${path}`, init)
@@ -34,9 +43,21 @@ export function createClient({ basePath = '/auth', fetch: doFetch = (...args) =>
34
43
  const { error, code, ...rest } = data
35
44
  throw new SlauthError(error ?? `HTTP ${res.status}`, code ?? 'server-error', res.status, rest)
36
45
  }
46
+ if (bearer && typeof data.token === 'string' && path !== 'token') await bearer.set(data.token)
47
+ if (bearer && path === 'signout') await bearer.set(null)
48
+ return data
49
+ }
50
+
51
+ /** Remembers the pending two-factor sign-in for bearer apps. */
52
+ let pendingToken = null
53
+ const signIn = async (path, body) => {
54
+ const data = await call(path, body)
55
+ if (data.pendingToken) pendingToken = data.pendingToken
37
56
  return data
38
57
  }
39
58
 
59
+ let cachedToken = null
60
+
40
61
  const passkey = {
41
62
  /** True when this browser can use passkeys. */
42
63
  supported: () => typeof window !== 'undefined' && typeof window.PublicKeyCredential === 'function',
@@ -65,18 +86,90 @@ export function createClient({ basePath = '/auth', fetch: doFetch = (...args) =>
65
86
  },
66
87
 
67
88
  list: () => call('passkeys'),
89
+ rename: (id, name) => call('passkey/rename', { id, name }),
68
90
  remove: (id) => call('passkey/delete', { id }),
69
91
  }
70
92
 
93
+ const go = (url) => {
94
+ if (typeof window !== 'undefined') window.location.assign(url)
95
+ return url
96
+ }
97
+ const providerUrl = (id, params) => `${base}/oauth/${encodeURIComponent(id)}?${new URLSearchParams(params)}`
98
+
71
99
  return {
72
- /** `{ user }` (null when signed out), plus `lock` when the app lock is on. */
100
+ /** `{ user }` (null when signed out), `twoFactor: 'pending'` mid sign-in, and `lock` when the app lock is on. */
73
101
  session: () => call('session'),
102
+ /** What this app offers: providers, passwords, magic links, two-factor, organizations. */
103
+ config: () => call('config'),
74
104
  email: {
75
- start: (email) => call('email/start', { email }),
76
- verify: (email, code) => call('email/verify', { email, code }),
105
+ start: (email, options = {}) => call('email/start', { email, ...options }),
106
+ verify: (email, code) => signIn('email/verify', { email, code }),
107
+ /** Finishes a magic link (the page the link opens does this for you). */
108
+ link: (token) => signIn('email/link', { token }),
109
+ change: {
110
+ start: (email) => call('email/change/start', { email }),
111
+ verify: (email, code) => call('email/change/verify', { email, code }),
112
+ },
113
+ },
114
+ password: {
115
+ signIn: (email, password) => signIn('password/signin', { email, password }),
116
+ /** Emails a code; once it's used (email.verify) the password is saved. */
117
+ signUp: (email, password, options = {}) => call('password/signup', { email, password, ...options }),
118
+ set: (password, current) => call('password/set', { password, current }),
119
+ remove: () => call('password/remove', {}),
120
+ },
121
+ /** "Sign in with …": goes to the provider and comes back to `redirect`. */
122
+ oauth: {
123
+ signIn: (provider, { redirect } = {}) => go(providerUrl(provider, redirect ? { redirect } : {})),
124
+ link: (provider, { redirect } = {}) => go(providerUrl(provider, { link: '1', ...(redirect ? { redirect } : {}) })),
125
+ url: (provider, { redirect } = {}) => providerUrl(provider, redirect ? { redirect } : {}),
126
+ },
127
+ twoFactor: {
128
+ /** The second step of signing in. Pass { recoveryCode } instead of a code if the phone is lost. */
129
+ verify: (code) => call('two-factor/verify', { ...(typeof code === 'string' ? { code } : code), ...(pendingToken ? { pendingToken } : {}) }),
130
+ setup: () => call('two-factor/setup', {}),
131
+ enable: (code) => call('two-factor/enable', { code }),
132
+ disable: (code) => call('two-factor/disable', typeof code === 'string' ? { code } : code),
133
+ recoveryCodes: (code) => call('two-factor/recovery-codes', { code }),
77
134
  },
78
135
  passkey,
79
- signOut: () => call('signout', {}),
136
+ signOut: async () => {
137
+ cachedToken = null
138
+ return call('signout', {})
139
+ },
140
+ user: {
141
+ update: (patch) => call('user', patch),
142
+ delete: () => call('user/delete', {}),
143
+ },
144
+ sessions: {
145
+ list: () => call('sessions'),
146
+ revoke: (id) => call('sessions/revoke', { id }),
147
+ revokeOthers: () => call('sessions/revoke-others', {}),
148
+ },
149
+ accounts: {
150
+ list: () => call('accounts'),
151
+ unlink: (provider) => call('accounts/unlink', { provider }),
152
+ },
153
+ /** A JWT for your other services, reused until a minute before it expires. */
154
+ async getToken() {
155
+ if (cachedToken && cachedToken.expiresAt - 60_000 > Date.now()) return cachedToken.token
156
+ cachedToken = await call('token')
157
+ return cachedToken.token
158
+ },
159
+ orgs: {
160
+ list: () => call('orgs'),
161
+ create: (name) => call('orgs', { name }),
162
+ get: (id) => call(`orgs/${encodeURIComponent(id)}`),
163
+ rename: (id, name) => call(`orgs/${encodeURIComponent(id)}`, { name }),
164
+ delete: (id) => call(`orgs/${encodeURIComponent(id)}/delete`, {}),
165
+ invite: (id, email, role) => call(`orgs/${encodeURIComponent(id)}/invite`, { email, role }),
166
+ revokeInvite: (id, inviteId) => call(`orgs/${encodeURIComponent(id)}/invites/revoke`, { id: inviteId }),
167
+ setRole: (id, userId, role) => call(`orgs/${encodeURIComponent(id)}/members/role`, { userId, role }),
168
+ removeMember: (id, userId) => call(`orgs/${encodeURIComponent(id)}/members/remove`, { userId }),
169
+ invites: () => call('invites'),
170
+ accept: (inviteId) => call('invites/accept', { id: inviteId }),
171
+ decline: (inviteId) => call('invites/decline', { id: inviteId }),
172
+ },
80
173
  lock: {
81
174
  unlock: (pin) => call('lock/login', { pin }),
82
175
  setup: (pin, policy) => call('lock/setup', { pin, policy }),