@voltro/plugin-auth 0.86.0 → 0.87.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/CHANGELOG.md +5321 -0
- package/README.md +83 -0
- package/THIRD-PARTY-NOTICES.md +1130 -11
- package/dist/authExtensions.js +9 -0
- package/dist/emailVerification.js +13 -11
- package/dist/guest.d.ts +99 -0
- package/dist/guest.js +3 -0
- package/dist/guestClient.js +237 -0
- package/dist/guestCredential.js +12 -0
- package/dist/guestPasswordUpgrade.js +85 -0
- package/dist/guestPlugin.js +166 -0
- package/dist/guestSchema.js +20 -0
- package/dist/guestSecrets.js +10 -0
- package/dist/guestService.js +224 -0
- package/dist/handlers.js +113 -113
- package/dist/impersonation.js +79 -78
- package/dist/index.d.ts +265 -15
- package/dist/index.js +27 -19
- package/dist/invitations.js +23 -23
- package/dist/native/guestStorage.js +41 -0
- package/dist/native.d.ts +110 -0
- package/dist/native.js +45 -43
- package/dist/nativeRoutes.js +61 -72
- package/dist/overlayPlugin.js +167 -0
- package/dist/plugin.d.ts +119 -10
- package/dist/plugin.js +99 -78
- package/dist/postgresUserStore.js +19 -7
- package/dist/revocation.js +40 -23
- package/dist/schema.d.ts +41 -1
- package/dist/schema.js +99 -98
- package/dist/sessionFamily.js +15 -0
- package/dist/sessionRegistry.js +93 -78
- package/dist/signedClaims.js +24 -0
- package/dist/strategy.d.ts +13 -9
- package/dist/strategy.js +26 -22
- package/dist/userStore.js +145 -138
- package/dist/web/guestStorage.js +47 -0
- package/dist/web.d.ts +94 -0
- package/dist/web.js +4 -2
- package/package.json +8 -3
package/dist/userStore.js
CHANGED
|
@@ -1,71 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { hasOwnedSessionFamily as e, indexSessionFamilies as t, sessionFamilyId as n } from "./sessionFamily.js";
|
|
2
|
+
import { Data as r, Effect as i } from "effect";
|
|
3
|
+
import { generateId as a } from "@voltro/database";
|
|
3
4
|
//#region src/userStore.ts
|
|
4
|
-
var
|
|
5
|
+
var o = class extends r.TaggedError("UserAlreadyExistsError") {}, s = class extends r.TaggedError("UserNotFoundError") {}, c = (e) => a({
|
|
5
6
|
kind: "typeid",
|
|
6
7
|
prefix: e
|
|
7
|
-
}, e),
|
|
8
|
-
let
|
|
9
|
-
for (let
|
|
10
|
-
let
|
|
11
|
-
let
|
|
12
|
-
if (!
|
|
13
|
-
let
|
|
14
|
-
return
|
|
8
|
+
}, e), l = (r = []) => {
|
|
9
|
+
let a = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map(), u = [], d = [], f = [], p = [], m = [], h = [], g = [], _ = /* @__PURE__ */ new Map();
|
|
10
|
+
for (let e of r) a.set(e.id, e), e.email !== null && l.set(e.email.toLowerCase(), e);
|
|
11
|
+
let v = (e, t) => i.gen(function* () {
|
|
12
|
+
let n = a.get(e);
|
|
13
|
+
if (!n) return yield* i.fail(new s({ userId: e }));
|
|
14
|
+
let r = t(n);
|
|
15
|
+
return a.set(e, r), r.email !== null && l.set(r.email.toLowerCase(), r), r;
|
|
15
16
|
});
|
|
16
17
|
return {
|
|
17
|
-
findByEmail: (e) =>
|
|
18
|
-
findById: (e) =>
|
|
19
|
-
insert: (e) =>
|
|
20
|
-
let
|
|
21
|
-
if (
|
|
22
|
-
let
|
|
18
|
+
findByEmail: (e) => i.sync(() => l.get(e.toLowerCase()) ?? null),
|
|
19
|
+
findById: (e) => i.sync(() => a.get(e) ?? null),
|
|
20
|
+
insert: (e) => i.gen(function* () {
|
|
21
|
+
let t = e.email === null ? null : e.email.toLowerCase();
|
|
22
|
+
if (t !== null && l.has(t)) return yield* i.fail(new o({ email: t }));
|
|
23
|
+
let n = {
|
|
23
24
|
...e,
|
|
24
|
-
email:
|
|
25
|
+
email: t,
|
|
25
26
|
createdAt: /* @__PURE__ */ new Date()
|
|
26
27
|
};
|
|
27
|
-
return
|
|
28
|
+
return a.set(n.id, n), t !== null && l.set(t, n), n;
|
|
28
29
|
}),
|
|
29
|
-
updatePassword: (e, t) =>
|
|
30
|
+
updatePassword: (e, t) => v(e, (e) => ({
|
|
30
31
|
...e,
|
|
31
32
|
passwordHash: t
|
|
32
33
|
})),
|
|
33
|
-
setMfaSecret: (e, t) =>
|
|
34
|
+
setMfaSecret: (e, t) => v(e, (e) => ({
|
|
34
35
|
...e,
|
|
35
36
|
mfaSecret: t,
|
|
36
37
|
mfaEnrolledAt: null
|
|
37
38
|
})),
|
|
38
|
-
markMfaEnrolled: (e) =>
|
|
39
|
+
markMfaEnrolled: (e) => v(e, (e) => ({
|
|
39
40
|
...e,
|
|
40
41
|
mfaEnrolledAt: /* @__PURE__ */ new Date()
|
|
41
42
|
})),
|
|
42
|
-
clearMfa: (e) =>
|
|
43
|
+
clearMfa: (e) => v(e, (e) => ({
|
|
43
44
|
...e,
|
|
44
45
|
mfaSecret: null,
|
|
45
46
|
mfaEnrolledAt: null
|
|
46
47
|
})),
|
|
47
|
-
markEmailVerified: (e) =>
|
|
48
|
+
markEmailVerified: (e) => v(e, (e) => e.emailVerifiedAt ? e : {
|
|
48
49
|
...e,
|
|
49
50
|
emailVerifiedAt: /* @__PURE__ */ new Date()
|
|
50
51
|
}),
|
|
51
|
-
listMemberships: (e) =>
|
|
52
|
-
addMembership: (e) =>
|
|
53
|
-
let t =
|
|
52
|
+
listMemberships: (e) => i.sync(() => u.filter((t) => t.userId === e)),
|
|
53
|
+
addMembership: (e) => i.sync(() => {
|
|
54
|
+
let t = u.findIndex((t) => t.userId === e.userId && t.tenantId === e.tenantId), n = {
|
|
54
55
|
...e,
|
|
55
56
|
joinedAt: /* @__PURE__ */ new Date()
|
|
56
57
|
};
|
|
57
58
|
if (t >= 0) {
|
|
58
|
-
let e =
|
|
59
|
+
let e = u[t], r = {
|
|
59
60
|
...n,
|
|
60
61
|
joinedAt: e.joinedAt
|
|
61
62
|
};
|
|
62
|
-
return
|
|
63
|
+
return u[t] = r, r;
|
|
63
64
|
}
|
|
64
|
-
return
|
|
65
|
+
return u.push(n), n;
|
|
66
|
+
}),
|
|
67
|
+
membershipRole: (e, t) => i.sync(() => u.find((n) => n.userId === e && n.tenantId === t)?.role ?? null),
|
|
68
|
+
listSessions: (r) => i.sync(() => {
|
|
69
|
+
let i = t(d);
|
|
70
|
+
return d.filter((t) => t.userId === r && t.rotatedAt == null && i.get(t.id) === t && e(t, i.get(n(t) ?? ""))).map((e) => structuredClone(e)).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime());
|
|
65
71
|
}),
|
|
66
|
-
|
|
67
|
-
listSessions: (e) => t.sync(() => c.filter((t) => t.userId === e && t.rotatedAt == null).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime())),
|
|
68
|
-
insertSession: (e) => t.sync(() => {
|
|
72
|
+
insertSession: (e) => i.sync(() => {
|
|
69
73
|
let t = /* @__PURE__ */ new Date(), n = {
|
|
70
74
|
...e,
|
|
71
75
|
familyId: e.familyId ?? e.id,
|
|
@@ -73,73 +77,76 @@ var r = class extends e.TaggedError("UserAlreadyExistsError") {}, i = class exte
|
|
|
73
77
|
createdAt: t,
|
|
74
78
|
lastSeenAt: t
|
|
75
79
|
};
|
|
76
|
-
return
|
|
80
|
+
return d.push(structuredClone(n)), structuredClone(n);
|
|
77
81
|
}),
|
|
78
|
-
findSession: (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
findSession: (r) => i.sync(() => {
|
|
83
|
+
let i = t(d), a = i.get(r);
|
|
84
|
+
return a && a.rotatedAt == null && e(a, i.get(n(a) ?? "")) ? structuredClone(a) : null;
|
|
85
|
+
}),
|
|
86
|
+
touchSession: (e, t) => i.sync(() => {
|
|
87
|
+
let n = d.findIndex((t) => t.id === e);
|
|
88
|
+
if (n >= 0 && d[n].rotatedAt == null) {
|
|
89
|
+
d[n] = {
|
|
90
|
+
...d[n],
|
|
91
|
+
expiresAt: t,
|
|
85
92
|
lastSeenAt: /* @__PURE__ */ new Date()
|
|
86
93
|
};
|
|
87
|
-
let r =
|
|
88
|
-
i >= 0 &&
|
|
89
|
-
...
|
|
90
|
-
expiresAt:
|
|
94
|
+
let r = d[n].familyId ?? e, i = d.findIndex((e) => e.id === r);
|
|
95
|
+
i >= 0 && d[i].expiresAt < t && (d[i] = {
|
|
96
|
+
...d[i],
|
|
97
|
+
expiresAt: t
|
|
91
98
|
});
|
|
92
99
|
}
|
|
93
100
|
}),
|
|
94
|
-
revokeSession: (e,
|
|
95
|
-
let
|
|
96
|
-
if (!
|
|
97
|
-
let r =
|
|
98
|
-
for (let t =
|
|
99
|
-
let n =
|
|
100
|
-
n.userId === e && (n.familyId ?? n.id) === r && (
|
|
101
|
+
revokeSession: (e, t) => i.sync(() => {
|
|
102
|
+
let n = d.find((n) => n.id === t && n.userId === e);
|
|
103
|
+
if (!n) return !1;
|
|
104
|
+
let r = n.familyId ?? n.id, i = !1;
|
|
105
|
+
for (let t = d.length - 1; t >= 0; t--) {
|
|
106
|
+
let n = d[t];
|
|
107
|
+
n.userId === e && (n.familyId ?? n.id) === r && (d.splice(t, 1), i = !0);
|
|
101
108
|
}
|
|
102
109
|
return i;
|
|
103
110
|
}),
|
|
104
|
-
revokeAllOtherSessions: (e,
|
|
105
|
-
let
|
|
106
|
-
for (let t =
|
|
107
|
-
let n =
|
|
108
|
-
n.userId === e && (n.familyId ?? n.id) !== r && (
|
|
111
|
+
revokeAllOtherSessions: (e, t) => i.sync(() => {
|
|
112
|
+
let n = d.find((n) => n.id === t && n.userId === e), r = n ? n.familyId ?? n.id : void 0, i = 0;
|
|
113
|
+
for (let t = d.length - 1; t >= 0; t--) {
|
|
114
|
+
let n = d[t];
|
|
115
|
+
n.userId === e && (n.familyId ?? n.id) !== r && (d.splice(t, 1), n.rotatedAt ?? (i += 1));
|
|
109
116
|
}
|
|
110
117
|
return i;
|
|
111
118
|
}),
|
|
112
|
-
revokeAllSessions: (e) =>
|
|
119
|
+
revokeAllSessions: (e) => i.sync(() => {
|
|
113
120
|
let t = 0;
|
|
114
|
-
for (let n =
|
|
115
|
-
let e =
|
|
116
|
-
|
|
121
|
+
for (let n = d.length - 1; n >= 0; n--) if (d[n].userId === e) {
|
|
122
|
+
let e = d[n];
|
|
123
|
+
d.splice(n, 1), e.rotatedAt ?? t++;
|
|
117
124
|
}
|
|
118
125
|
return t;
|
|
119
126
|
}),
|
|
120
|
-
insertToken: (e) =>
|
|
127
|
+
insertToken: (e) => i.sync(() => {
|
|
121
128
|
let t = {
|
|
122
129
|
...e,
|
|
123
130
|
consumedAt: null,
|
|
124
131
|
createdAt: /* @__PURE__ */ new Date()
|
|
125
132
|
};
|
|
126
|
-
return
|
|
133
|
+
return f.push(t), t;
|
|
127
134
|
}),
|
|
128
|
-
consumeToken: (e,
|
|
129
|
-
let
|
|
135
|
+
consumeToken: (e, t) => i.sync(() => {
|
|
136
|
+
let n = Date.now(), r = f.find((r) => r.tokenHash === e && r.purpose === t && r.consumedAt === null && r.expiresAt.getTime() > n);
|
|
130
137
|
if (!r) return null;
|
|
131
138
|
let i = {
|
|
132
139
|
...r,
|
|
133
140
|
consumedAt: /* @__PURE__ */ new Date()
|
|
134
|
-
}, a =
|
|
135
|
-
return
|
|
141
|
+
}, a = f.indexOf(r);
|
|
142
|
+
return f[a] = i, i;
|
|
136
143
|
}),
|
|
137
|
-
latestToken: (e,
|
|
138
|
-
let
|
|
139
|
-
for (let r of
|
|
140
|
-
return
|
|
144
|
+
latestToken: (e, t) => i.sync(() => {
|
|
145
|
+
let n = null;
|
|
146
|
+
for (let r of f) r.userId === e && r.purpose === t && (n === null || r.createdAt.getTime() > n.createdAt.getTime()) && (n = r);
|
|
147
|
+
return n;
|
|
141
148
|
}),
|
|
142
|
-
insertInvitation: (e) =>
|
|
149
|
+
insertInvitation: (e) => i.sync(() => {
|
|
143
150
|
let t = {
|
|
144
151
|
...e,
|
|
145
152
|
email: e.email.toLowerCase(),
|
|
@@ -149,119 +156,119 @@ var r = class extends e.TaggedError("UserAlreadyExistsError") {}, i = class exte
|
|
|
149
156
|
revokedByUserId: null,
|
|
150
157
|
createdAt: /* @__PURE__ */ new Date()
|
|
151
158
|
};
|
|
152
|
-
return
|
|
159
|
+
return h.push(t), t;
|
|
153
160
|
}),
|
|
154
|
-
findInvitationByHash: (e) =>
|
|
155
|
-
acceptInvitation: (e,
|
|
156
|
-
let
|
|
157
|
-
if (
|
|
161
|
+
findInvitationByHash: (e) => i.sync(() => h.find((t) => t.tokenHash === e) ?? null),
|
|
162
|
+
acceptInvitation: (e, t, n) => i.sync(() => {
|
|
163
|
+
let r = h.findIndex((t) => t.tokenHash === e && t.acceptedAt === null && t.revokedAt === null && t.expiresAt.getTime() > n.getTime());
|
|
164
|
+
if (r < 0) return null;
|
|
158
165
|
let i = {
|
|
159
|
-
...
|
|
160
|
-
acceptedAt:
|
|
161
|
-
acceptedByUserId:
|
|
166
|
+
...h[r],
|
|
167
|
+
acceptedAt: n,
|
|
168
|
+
acceptedByUserId: t
|
|
162
169
|
};
|
|
163
|
-
return
|
|
170
|
+
return h[r] = i, i;
|
|
164
171
|
}),
|
|
165
|
-
revokeInvitation: (e, n, r
|
|
166
|
-
let
|
|
167
|
-
return
|
|
168
|
-
...
|
|
169
|
-
revokedAt:
|
|
170
|
-
revokedByUserId:
|
|
172
|
+
revokeInvitation: (e, t, n, r) => i.sync(() => {
|
|
173
|
+
let i = h.findIndex((n) => n.id === e && n.tenantId === t && n.acceptedAt === null && n.revokedAt === null);
|
|
174
|
+
return i < 0 ? !1 : (h[i] = {
|
|
175
|
+
...h[i],
|
|
176
|
+
revokedAt: r,
|
|
177
|
+
revokedByUserId: n
|
|
171
178
|
}, !0);
|
|
172
179
|
}),
|
|
173
|
-
listInvitations: (e) =>
|
|
174
|
-
countPendingInvitations: (e,
|
|
175
|
-
supersedePendingInvitations: (e, n, r
|
|
176
|
-
let
|
|
177
|
-
for (let
|
|
178
|
-
let o =
|
|
179
|
-
o.tenantId === e && o.email ===
|
|
180
|
+
listInvitations: (e) => i.sync(() => h.filter((t) => t.tenantId === e).sort((e, t) => t.createdAt.getTime() - e.createdAt.getTime())),
|
|
181
|
+
countPendingInvitations: (e, t) => i.sync(() => h.filter((n) => n.tenantId === e && n.acceptedAt === null && n.revokedAt === null && n.expiresAt.getTime() > t.getTime()).length),
|
|
182
|
+
supersedePendingInvitations: (e, t, n, r) => i.sync(() => {
|
|
183
|
+
let i = t.toLowerCase(), a = 0;
|
|
184
|
+
for (let t = 0; t < h.length; t++) {
|
|
185
|
+
let o = h[t];
|
|
186
|
+
o.tenantId === e && o.email === i && o.acceptedAt === null && o.revokedAt === null && (h[t] = {
|
|
180
187
|
...o,
|
|
181
|
-
revokedAt:
|
|
182
|
-
revokedByUserId:
|
|
188
|
+
revokedAt: r,
|
|
189
|
+
revokedByUserId: n
|
|
183
190
|
}, a++);
|
|
184
191
|
}
|
|
185
192
|
return a;
|
|
186
193
|
}),
|
|
187
|
-
insertImpersonationGrant: (e) =>
|
|
194
|
+
insertImpersonationGrant: (e) => i.sync(() => {
|
|
188
195
|
let t = {
|
|
189
196
|
...e,
|
|
190
197
|
startedAt: /* @__PURE__ */ new Date(),
|
|
191
198
|
endedAt: null,
|
|
192
199
|
endedReason: null
|
|
193
200
|
};
|
|
194
|
-
return
|
|
201
|
+
return g.push(t), t;
|
|
195
202
|
}),
|
|
196
|
-
findImpersonationGrantBySession: (e) =>
|
|
197
|
-
endImpersonationGrant: (e,
|
|
198
|
-
let
|
|
199
|
-
return
|
|
200
|
-
...
|
|
201
|
-
endedAt:
|
|
202
|
-
endedReason:
|
|
203
|
+
findImpersonationGrantBySession: (e) => i.sync(() => g.find((t) => t.sessionId === e) ?? null),
|
|
204
|
+
endImpersonationGrant: (e, t, n) => i.sync(() => {
|
|
205
|
+
let r = g.findIndex((t) => t.sessionId === e && t.endedAt === null);
|
|
206
|
+
return r < 0 ? !1 : (g[r] = {
|
|
207
|
+
...g[r],
|
|
208
|
+
endedAt: n,
|
|
209
|
+
endedReason: t
|
|
203
210
|
}, !0);
|
|
204
211
|
}),
|
|
205
|
-
replaceRecoveryCodes: (e,
|
|
206
|
-
for (let t =
|
|
207
|
-
let
|
|
208
|
-
for (let r of
|
|
209
|
-
id:
|
|
212
|
+
replaceRecoveryCodes: (e, t) => i.sync(() => {
|
|
213
|
+
for (let t = m.length - 1; t >= 0; t--) m[t].userId === e && m.splice(t, 1);
|
|
214
|
+
let n = /* @__PURE__ */ new Date();
|
|
215
|
+
for (let r of t) m.push({
|
|
216
|
+
id: c("rc"),
|
|
210
217
|
userId: e,
|
|
211
218
|
codeHash: r,
|
|
212
219
|
consumedAt: null,
|
|
213
|
-
createdAt:
|
|
220
|
+
createdAt: n
|
|
214
221
|
});
|
|
215
222
|
}),
|
|
216
|
-
consumeRecoveryCode: (e,
|
|
217
|
-
let
|
|
218
|
-
return
|
|
219
|
-
...
|
|
223
|
+
consumeRecoveryCode: (e, t) => i.sync(() => {
|
|
224
|
+
let n = m.findIndex((n) => n.userId === e && n.codeHash === t && n.consumedAt === null);
|
|
225
|
+
return n < 0 ? !1 : (m[n] = {
|
|
226
|
+
...m[n],
|
|
220
227
|
consumedAt: /* @__PURE__ */ new Date()
|
|
221
228
|
}, !0);
|
|
222
229
|
}),
|
|
223
|
-
countRecoveryCodes: (e) =>
|
|
224
|
-
listPasskeys: (e) =>
|
|
225
|
-
findPasskey: (e) =>
|
|
226
|
-
insertPasskey: (e) =>
|
|
230
|
+
countRecoveryCodes: (e) => i.sync(() => m.filter((t) => t.userId === e && t.consumedAt === null).length),
|
|
231
|
+
listPasskeys: (e) => i.sync(() => p.filter((t) => t.userId === e)),
|
|
232
|
+
findPasskey: (e) => i.sync(() => p.find((t) => t.credentialId === e) ?? null),
|
|
233
|
+
insertPasskey: (e) => i.sync(() => {
|
|
227
234
|
let t = /* @__PURE__ */ new Date(), n = {
|
|
228
235
|
...e,
|
|
229
236
|
createdAt: t,
|
|
230
237
|
lastUsedAt: null
|
|
231
238
|
};
|
|
232
|
-
return
|
|
239
|
+
return p.push(n), n;
|
|
233
240
|
}),
|
|
234
|
-
advancePasskeyCounter: (e,
|
|
235
|
-
let
|
|
236
|
-
return
|
|
237
|
-
...
|
|
238
|
-
counter:
|
|
241
|
+
advancePasskeyCounter: (e, t) => i.sync(() => {
|
|
242
|
+
let n = p.findIndex((t) => t.credentialId === e);
|
|
243
|
+
return n < 0 || t <= p[n].counter ? !1 : (p[n] = {
|
|
244
|
+
...p[n],
|
|
245
|
+
counter: t,
|
|
239
246
|
lastUsedAt: /* @__PURE__ */ new Date()
|
|
240
247
|
}, !0);
|
|
241
248
|
}),
|
|
242
|
-
isLockedOut: (e,
|
|
243
|
-
let
|
|
244
|
-
return
|
|
249
|
+
isLockedOut: (e, t) => i.sync(() => {
|
|
250
|
+
let n = _.get(e.toLowerCase());
|
|
251
|
+
return n?.lockedUntil != null && t < n.lockedUntil ? {
|
|
245
252
|
locked: !0,
|
|
246
|
-
retryAfterSeconds: Math.ceil((
|
|
253
|
+
retryAfterSeconds: Math.ceil((n.lockedUntil - t) / 1e3)
|
|
247
254
|
} : {
|
|
248
255
|
locked: !1,
|
|
249
256
|
retryAfterSeconds: 0
|
|
250
257
|
};
|
|
251
258
|
}),
|
|
252
|
-
recordLoginFailure: (e,
|
|
253
|
-
let
|
|
254
|
-
|
|
259
|
+
recordLoginFailure: (e, t, n) => i.sync(() => {
|
|
260
|
+
let r = e.toLowerCase(), i = _.get(r), a = i?.windowStartedAt == null || t - i.windowStartedAt >= n.windowMs, o = i?.lockedUntil != null && t >= i.lockedUntil, s = !i || a || o, c = s ? 1 : i.failedCount + 1, l = s ? t : i.windowStartedAt, u = c >= n.maxAttempts ? t + n.lockMs : null;
|
|
261
|
+
_.set(r, {
|
|
255
262
|
failedCount: c,
|
|
256
263
|
windowStartedAt: l,
|
|
257
264
|
lockedUntil: u
|
|
258
265
|
});
|
|
259
266
|
}),
|
|
260
|
-
clearLoginFailures: (e) =>
|
|
261
|
-
|
|
267
|
+
clearLoginFailures: (e) => i.sync(() => {
|
|
268
|
+
_.delete(e.toLowerCase());
|
|
262
269
|
})
|
|
263
270
|
};
|
|
264
|
-
},
|
|
271
|
+
}, u = (e, t = {}) => {
|
|
265
272
|
let n = { ...t.metadata };
|
|
266
273
|
return t.memberships && (n.memberships = t.memberships.map((e) => ({
|
|
267
274
|
tenantId: e.tenantId,
|
|
@@ -272,14 +279,14 @@ var r = class extends e.TaggedError("UserAlreadyExistsError") {}, i = class exte
|
|
|
272
279
|
tenantId: t.tenantId ?? e.tenantId,
|
|
273
280
|
...Object.keys(n).length > 0 ? { metadata: n } : {}
|
|
274
281
|
};
|
|
275
|
-
},
|
|
282
|
+
}, d = (e) => {
|
|
276
283
|
if (e.type !== "user") return null;
|
|
277
284
|
let t = e.metadata?.sessionId;
|
|
278
285
|
return typeof t == "string" && t.length > 0 ? t : null;
|
|
279
|
-
},
|
|
286
|
+
}, f = (e) => {
|
|
280
287
|
if (e.type !== "user") return [];
|
|
281
288
|
let t = e.metadata?.memberships;
|
|
282
289
|
return Array.isArray(t) ? t.filter((e) => typeof e == "object" && !!e && typeof e.tenantId == "string" && typeof e.role == "string") : [];
|
|
283
290
|
};
|
|
284
291
|
//#endregion
|
|
285
|
-
export {
|
|
292
|
+
export { o as UserAlreadyExistsError, s as UserNotFoundError, l as memoryUserStore, c as newAuthId, d as sessionIdOfSubject, u as subjectFromUser, f as subjectMemberships };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/web/guestStorage.ts
|
|
2
|
+
var e = (e) => {
|
|
3
|
+
if (!e.trim()) throw Error("Guest storage requires an app namespace");
|
|
4
|
+
let t = (t, n) => new Promise((r, i) => {
|
|
5
|
+
let a = !1, o = globalThis.indexedDB.open("voltro:guest:" + e, 1);
|
|
6
|
+
o.onupgradeneeded = () => {
|
|
7
|
+
o.result.createObjectStore("credentials");
|
|
8
|
+
}, o.onerror = () => i(/* @__PURE__ */ Error("Guest IndexedDB unavailable")), o.onblocked = () => {
|
|
9
|
+
a = !0, i(/* @__PURE__ */ Error("Guest IndexedDB blocked"));
|
|
10
|
+
}, o.onsuccess = () => {
|
|
11
|
+
let e = o.result;
|
|
12
|
+
if (a) {
|
|
13
|
+
e.close();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
let s, c;
|
|
17
|
+
try {
|
|
18
|
+
s = e.transaction("credentials", t), c = n(s.objectStore("credentials"));
|
|
19
|
+
} catch {
|
|
20
|
+
e.close(), i(/* @__PURE__ */ Error("Guest IndexedDB transaction unavailable"));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
s.oncomplete = () => {
|
|
24
|
+
e.close(), r(c.result);
|
|
25
|
+
}, s.onerror = s.onabort = () => {
|
|
26
|
+
e.close(), i(/* @__PURE__ */ Error("Guest IndexedDB transaction failed"));
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
exclusive: (t) => globalThis.navigator?.locks ? globalThis.navigator.locks.request("voltro:guest:" + e, t) : Promise.reject(/* @__PURE__ */ Error("Guest cross-tab locking unavailable")),
|
|
32
|
+
read: async () => {
|
|
33
|
+
let e = await t("readonly", (e) => e.get("device"));
|
|
34
|
+
if (e === void 0) return null;
|
|
35
|
+
if (typeof e != "string" || !e) throw Error("Guest IndexedDB credential invalid");
|
|
36
|
+
return e;
|
|
37
|
+
},
|
|
38
|
+
write: async (e) => {
|
|
39
|
+
await t("readwrite", (t) => t.put(e, "device"));
|
|
40
|
+
},
|
|
41
|
+
remove: async () => {
|
|
42
|
+
await t("readwrite", (e) => e.delete("device"));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { e as indexedDbGuestStore };
|
package/dist/web.d.ts
CHANGED
|
@@ -1,5 +1,99 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
2
|
import { Subject } from '@voltro/protocol';
|
|
3
|
+
import { SubjectIdentity } from '@voltro/protocol';
|
|
4
|
+
|
|
5
|
+
/** Serializes operations in one client. A rejected existing credential never
|
|
6
|
+
* creates a replacement identity. Storage failures never enable memory fallback. */
|
|
7
|
+
export declare const createGuestClient: (options: GuestClientOptions) => {
|
|
8
|
+
connect: () => Promise<GuestClientSession>;
|
|
9
|
+
/** Prepare without replacing the active proof, persist both proofs, then
|
|
10
|
+
* commit. A later connect recovers the same identity after a lost response. */
|
|
11
|
+
rotate: () => Promise<GuestClientSession>;
|
|
12
|
+
/** Revoke on the server first. Keep the durable proof on transport failure
|
|
13
|
+
* so the caller can retry; erase locally only after confirmed revocation. */
|
|
14
|
+
revoke: () => Promise<void>;
|
|
15
|
+
/** Turn this guest into an email/password account under the same subject ID
|
|
16
|
+
* (`authRoutesPlugin({ guestUpgrade })`). The device proof is erased only after the
|
|
17
|
+
* server confirmed the account; on any refusal or transport failure it stays. */
|
|
18
|
+
upgrade: (input: {
|
|
19
|
+
readonly email: string;
|
|
20
|
+
readonly password: string;
|
|
21
|
+
}) => Promise<{
|
|
22
|
+
readonly subject: unknown;
|
|
23
|
+
}>;
|
|
24
|
+
/** The registered account signed in on this device (cookie clients), or null. */
|
|
25
|
+
account: () => Promise<GuestClientAccount | null>;
|
|
26
|
+
/** The signed-in account if there is one, else this device's guest (resumed or created once).
|
|
27
|
+
* The usual entry for a game that offers accounts: a reload after `upgrade` or `signIn` stays the account. */
|
|
28
|
+
start: () => Promise<GuestClientPlayer>;
|
|
29
|
+
/** Sign in to an existing account with email and password (`authRoutesPlugin`), e.g. on a second
|
|
30
|
+
* device. This device's guest proof stays: after `signOut`, `start` resumes that guest. */
|
|
31
|
+
signIn: (input: {
|
|
32
|
+
readonly email: string;
|
|
33
|
+
readonly password: string;
|
|
34
|
+
}) => Promise<GuestClientAccount>;
|
|
35
|
+
/** End the account session on the server and clear its cookie. */
|
|
36
|
+
signOut: () => Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** A registered account signed in on this device (by upgrade or sign-in), through the session cookie. */
|
|
40
|
+
export declare interface GuestClientAccount {
|
|
41
|
+
readonly subject: Extract<SubjectIdentity, {
|
|
42
|
+
readonly type: 'user';
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export declare class GuestClientError extends Error {
|
|
47
|
+
readonly code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response';
|
|
48
|
+
readonly status?: number | undefined;
|
|
49
|
+
constructor(code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response', status?: number | undefined);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export declare interface GuestClientOptions {
|
|
53
|
+
readonly apiBaseUrl: string;
|
|
54
|
+
readonly storage: GuestCredentialStore;
|
|
55
|
+
readonly prefix?: string;
|
|
56
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
57
|
+
/** Web clients include cookies and obtain a signed CSRF token for each post. */
|
|
58
|
+
readonly cookies?: boolean;
|
|
59
|
+
/** Prefix of the app's auth routes, for `upgrade`, `account`, `signIn` and `signOut` (default `/auth`). */
|
|
60
|
+
readonly accountPrefix?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** What `start` found: the signed-in account, else this device's guest. */
|
|
64
|
+
export declare type GuestClientPlayer = {
|
|
65
|
+
readonly kind: 'account';
|
|
66
|
+
readonly subject: GuestClientAccount['subject'];
|
|
67
|
+
} | {
|
|
68
|
+
readonly kind: 'guest';
|
|
69
|
+
readonly subject: GuestClientSession['subject'];
|
|
70
|
+
readonly session: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export declare interface GuestClientSession {
|
|
74
|
+
readonly subject: Extract<SubjectIdentity, {
|
|
75
|
+
readonly type: 'user';
|
|
76
|
+
}> & {
|
|
77
|
+
readonly kind: 'guest';
|
|
78
|
+
};
|
|
79
|
+
readonly session: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Platform-backed durable secret storage. Native implementations use the
|
|
83
|
+
* operating system's secure store; writes resolve only after persistence. */
|
|
84
|
+
export declare interface GuestCredentialStore {
|
|
85
|
+
/** Serialize the whole read/create/write operation across users of this
|
|
86
|
+
* namespace, including other browser tabs or native client instances. */
|
|
87
|
+
readonly exclusive: <A>(work: () => Promise<A>) => Promise<A>;
|
|
88
|
+
readonly read: () => Promise<string | null>;
|
|
89
|
+
/** Persist the opaque value, including an in-progress rotation journal. */
|
|
90
|
+
readonly write: (value: string) => Promise<void>;
|
|
91
|
+
readonly remove: () => Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** IndexedDB is the web device-proof store. Missing/blocked storage rejects;
|
|
95
|
+
* there is deliberately no localStorage or volatile substitute. */
|
|
96
|
+
export declare const indexedDbGuestStore: (namespace: string) => GuestCredentialStore;
|
|
3
97
|
|
|
4
98
|
/** True when this browser can do WebAuthn at all. */
|
|
5
99
|
export declare const isPasskeySupported: () => boolean;
|
package/dist/web.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SubjectProvider as e, useOptionalSubject as t, useSubject as n } from "./web/subjectContext.js";
|
|
2
2
|
import { RequireAuth as r } from "./web/requireAuth.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { GuestClientError as i, createGuestClient as a } from "./guestClient.js";
|
|
4
|
+
import { indexedDbGuestStore as o } from "./web/guestStorage.js";
|
|
5
|
+
import { isPasskeySupported as s, registerPasskey as c, signInWithPasskey as l } from "./web/passkeyClient.js";
|
|
6
|
+
export { i as GuestClientError, r as RequireAuth, e as SubjectProvider, a as createGuestClient, o as indexedDbGuestStore, s as isPasskeySupported, c as registerPasskey, l as signInWithPasskey, t as useOptionalSubject, n as useSubject };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.87.0",
|
|
4
4
|
"description": "Authentication primitives: password hashing, session creation, schema mixin + tables. Pairs with the `auth` app template for the UI; both independently usable. Server-side only (uses node:crypto).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -73,6 +73,11 @@
|
|
|
73
73
|
"import": "./dist/native.js",
|
|
74
74
|
"default": "./dist/native.js"
|
|
75
75
|
},
|
|
76
|
+
"./guest": {
|
|
77
|
+
"types": "./dist/guest.d.ts",
|
|
78
|
+
"import": "./dist/guest.js",
|
|
79
|
+
"default": "./dist/guest.js"
|
|
80
|
+
},
|
|
76
81
|
"./package.json": "./package.json"
|
|
77
82
|
},
|
|
78
83
|
"main": "./dist/index.js",
|
|
@@ -83,8 +88,8 @@
|
|
|
83
88
|
"node": ">=24.0.0"
|
|
84
89
|
},
|
|
85
90
|
"dependencies": {
|
|
86
|
-
"@voltro/database": "0.
|
|
87
|
-
"@voltro/protocol": "0.
|
|
91
|
+
"@voltro/database": "0.87.0",
|
|
92
|
+
"@voltro/protocol": "0.87.0"
|
|
88
93
|
},
|
|
89
94
|
"peerDependencies": {
|
|
90
95
|
"effect": "4.0.1",
|