@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
|
@@ -22,27 +22,29 @@ var i = 60, a = "email_not_verified", o = "emailVerified", s = (e) => e?.policy
|
|
|
22
22
|
body: JSON.stringify(t),
|
|
23
23
|
contentType: "application/json; charset=utf-8"
|
|
24
24
|
}), p = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, m = (t, i, a) => r.gen(function* () {
|
|
25
|
-
let o =
|
|
26
|
-
if (
|
|
25
|
+
let o = t.email;
|
|
26
|
+
if (o === null) return !1;
|
|
27
|
+
let s = a.emailVerification, c = (a.now ?? Date.now)(), l = (s?.resendCooldownSeconds ?? 60) * 1e3;
|
|
28
|
+
if (l > 0) {
|
|
27
29
|
let e = yield* i.latestToken(t.id, "email-verify");
|
|
28
|
-
if (e &&
|
|
30
|
+
if (e && c - e.createdAt.getTime() < l) return !1;
|
|
29
31
|
}
|
|
30
|
-
let
|
|
32
|
+
let u = n("email-verify", s?.ttlSeconds ?? 86400);
|
|
31
33
|
if (yield* i.insertToken({
|
|
32
34
|
id: e("tok"),
|
|
33
|
-
tokenHash:
|
|
35
|
+
tokenHash: u.tokenHash,
|
|
34
36
|
userId: t.id,
|
|
35
37
|
purpose: "email-verify",
|
|
36
|
-
expiresAt:
|
|
38
|
+
expiresAt: u.expiresAt
|
|
37
39
|
}).pipe(r.ignore), !a.sendEmail) return !1;
|
|
38
|
-
let
|
|
40
|
+
let d = p(a.appBaseUrl ?? "", s?.callbackPath ?? "/auth/verify-email/callback", u.token);
|
|
39
41
|
return yield* r.tryPromise(() => a.sendEmail({
|
|
40
|
-
to:
|
|
42
|
+
to: o,
|
|
41
43
|
kind: "email-verify",
|
|
42
|
-
actionUrl:
|
|
44
|
+
actionUrl: d,
|
|
43
45
|
subject: "Confirm your email address",
|
|
44
|
-
text: `Confirm your email address: ${
|
|
45
|
-
html: `<p><a href="${
|
|
46
|
+
text: `Confirm your email address: ${d}`,
|
|
47
|
+
html: `<p><a href="${d}">Confirm your email address</a> — this link expires in 24 hours.</p>`
|
|
46
48
|
})).pipe(r.ignore), !0;
|
|
47
49
|
}), h = (e, t, n) => r.gen(function* () {
|
|
48
50
|
if (!e.email && !e.userId) return f(400, { error: "email or userId required" });
|
package/dist/guest.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { SubjectIdentity } from '@voltro/protocol';
|
|
2
|
+
|
|
3
|
+
/** Serializes operations in one client. A rejected existing credential never
|
|
4
|
+
* creates a replacement identity. Storage failures never enable memory fallback. */
|
|
5
|
+
export declare const createGuestClient: (options: GuestClientOptions) => {
|
|
6
|
+
connect: () => Promise<GuestClientSession>;
|
|
7
|
+
/** Prepare without replacing the active proof, persist both proofs, then
|
|
8
|
+
* commit. A later connect recovers the same identity after a lost response. */
|
|
9
|
+
rotate: () => Promise<GuestClientSession>;
|
|
10
|
+
/** Revoke on the server first. Keep the durable proof on transport failure
|
|
11
|
+
* so the caller can retry; erase locally only after confirmed revocation. */
|
|
12
|
+
revoke: () => Promise<void>;
|
|
13
|
+
/** Turn this guest into an email/password account under the same subject ID
|
|
14
|
+
* (`authRoutesPlugin({ guestUpgrade })`). The device proof is erased only after the
|
|
15
|
+
* server confirmed the account; on any refusal or transport failure it stays. */
|
|
16
|
+
upgrade: (input: {
|
|
17
|
+
readonly email: string;
|
|
18
|
+
readonly password: string;
|
|
19
|
+
}) => Promise<{
|
|
20
|
+
readonly subject: unknown;
|
|
21
|
+
}>;
|
|
22
|
+
/** The registered account signed in on this device (cookie clients), or null. */
|
|
23
|
+
account: () => Promise<GuestClientAccount | null>;
|
|
24
|
+
/** The signed-in account if there is one, else this device's guest (resumed or created once).
|
|
25
|
+
* The usual entry for a game that offers accounts: a reload after `upgrade` or `signIn` stays the account. */
|
|
26
|
+
start: () => Promise<GuestClientPlayer>;
|
|
27
|
+
/** Sign in to an existing account with email and password (`authRoutesPlugin`), e.g. on a second
|
|
28
|
+
* device. This device's guest proof stays: after `signOut`, `start` resumes that guest. */
|
|
29
|
+
signIn: (input: {
|
|
30
|
+
readonly email: string;
|
|
31
|
+
readonly password: string;
|
|
32
|
+
}) => Promise<GuestClientAccount>;
|
|
33
|
+
/** End the account session on the server and clear its cookie. */
|
|
34
|
+
signOut: () => Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** A registered account signed in on this device (by upgrade or sign-in), through the session cookie. */
|
|
38
|
+
declare interface GuestClientAccount {
|
|
39
|
+
readonly subject: Extract<SubjectIdentity, {
|
|
40
|
+
readonly type: 'user';
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export declare class GuestClientError extends Error {
|
|
45
|
+
readonly code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response';
|
|
46
|
+
readonly status?: number | undefined;
|
|
47
|
+
constructor(code: 'storage_unavailable' | 'credential_rejected' | 'upgrade_conflict' | 'account_signed_in' | 'transport_failed' | 'invalid_response', status?: number | undefined);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export declare interface GuestClientOptions {
|
|
51
|
+
readonly apiBaseUrl: string;
|
|
52
|
+
readonly storage: GuestCredentialStore;
|
|
53
|
+
readonly prefix?: string;
|
|
54
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
55
|
+
/** Web clients include cookies and obtain a signed CSRF token for each post. */
|
|
56
|
+
readonly cookies?: boolean;
|
|
57
|
+
/** Prefix of the app's auth routes, for `upgrade`, `account`, `signIn` and `signOut` (default `/auth`). */
|
|
58
|
+
readonly accountPrefix?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What `start` found: the signed-in account, else this device's guest. */
|
|
62
|
+
declare type GuestClientPlayer = {
|
|
63
|
+
readonly kind: 'account';
|
|
64
|
+
readonly subject: GuestClientAccount['subject'];
|
|
65
|
+
} | {
|
|
66
|
+
readonly kind: 'guest';
|
|
67
|
+
readonly subject: GuestClientSession['subject'];
|
|
68
|
+
readonly session: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export declare interface GuestClientSession {
|
|
72
|
+
readonly subject: Extract<SubjectIdentity, {
|
|
73
|
+
readonly type: 'user';
|
|
74
|
+
}> & {
|
|
75
|
+
readonly kind: 'guest';
|
|
76
|
+
};
|
|
77
|
+
readonly session: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Platform-backed durable secret storage. Native implementations use the
|
|
81
|
+
* operating system's secure store; writes resolve only after persistence. */
|
|
82
|
+
export declare interface GuestCredentialStore {
|
|
83
|
+
/** Serialize the whole read/create/write operation across users of this
|
|
84
|
+
* namespace, including other browser tabs or native client instances. */
|
|
85
|
+
readonly exclusive: <A>(work: () => Promise<A>) => Promise<A>;
|
|
86
|
+
readonly read: () => Promise<string | null>;
|
|
87
|
+
/** Persist the opaque value, including an in-progress rotation journal. */
|
|
88
|
+
readonly write: (value: string) => Promise<void>;
|
|
89
|
+
readonly remove: () => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Browser-safe shape only. Possession is verified against SQL on the server. */
|
|
93
|
+
export declare const parseGuestCredential: (credential: string) => {
|
|
94
|
+
readonly guestId: string;
|
|
95
|
+
readonly deviceId: string;
|
|
96
|
+
readonly secret: string;
|
|
97
|
+
} | null;
|
|
98
|
+
|
|
99
|
+
export { }
|
package/dist/guest.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { parseGuestCredential as e } from "./guestCredential.js";
|
|
2
|
+
//#region src/guestClient.ts
|
|
3
|
+
var t = class extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
constructor(e, t) {
|
|
7
|
+
super("Guest authentication: " + e), this.code = e, this.status = t, this.name = "GuestClientError";
|
|
8
|
+
}
|
|
9
|
+
}, n = (e) => {
|
|
10
|
+
if (typeof e != "object" || !e) throw new t("invalid_response");
|
|
11
|
+
let n = e, r = n.subject;
|
|
12
|
+
if (typeof n.session != "string" || !n.session || !r || r.type !== "user" || r.kind !== "guest" || typeof r.id != "string" || !r.id.trim() || typeof r.tenantId != "string" || !r.tenantId.trim()) throw new t("invalid_response");
|
|
13
|
+
return {
|
|
14
|
+
session: n.session,
|
|
15
|
+
subject: r
|
|
16
|
+
};
|
|
17
|
+
}, r = (r, i) => {
|
|
18
|
+
let a = n(r), o = e(i);
|
|
19
|
+
if (!o || a.subject.id !== o.guestId) throw new t("invalid_response");
|
|
20
|
+
return a;
|
|
21
|
+
}, i = (t, n) => {
|
|
22
|
+
let r = e(t), i = e(n);
|
|
23
|
+
return r !== null && i !== null && r.guestId === i.guestId && r.deviceId === i.deviceId && r.secret !== i.secret;
|
|
24
|
+
}, a = (e) => {
|
|
25
|
+
if (e === null || !e.startsWith("{")) return e;
|
|
26
|
+
let n;
|
|
27
|
+
try {
|
|
28
|
+
n = JSON.parse(e);
|
|
29
|
+
} catch {
|
|
30
|
+
throw new t("storage_unavailable");
|
|
31
|
+
}
|
|
32
|
+
if (!n || typeof n != "object" || Array.isArray(n)) throw new t("storage_unavailable");
|
|
33
|
+
let r = n;
|
|
34
|
+
if (r.kind !== "guest-rotation" || typeof r.current != "string" || typeof r.next != "string" || Object.keys(r).sort().join(",") !== "current,kind,next" || !i(r.current, r.next)) throw new t("storage_unavailable");
|
|
35
|
+
return {
|
|
36
|
+
kind: "guest-rotation",
|
|
37
|
+
current: r.current,
|
|
38
|
+
next: r.next
|
|
39
|
+
};
|
|
40
|
+
}, o = (e) => {
|
|
41
|
+
let n = e.fetch ?? globalThis.fetch, o = e.apiBaseUrl.replace(/\/+$/, "") + (e.prefix ?? "/auth/guest"), s = Promise.resolve(), c = (n) => {
|
|
42
|
+
let r = async () => {
|
|
43
|
+
try {
|
|
44
|
+
return await e.storage.exclusive(n);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
throw e instanceof t ? e : new t("storage_unavailable");
|
|
47
|
+
}
|
|
48
|
+
}, i = s.then(r, r);
|
|
49
|
+
return s = i.then(() => void 0, () => void 0), i;
|
|
50
|
+
}, l = async (e) => {
|
|
51
|
+
try {
|
|
52
|
+
return await e();
|
|
53
|
+
} catch {
|
|
54
|
+
throw new t("storage_unavailable");
|
|
55
|
+
}
|
|
56
|
+
}, u = async (r, i) => {
|
|
57
|
+
let a = { "content-type": "application/json" }, s = e.cookies ? "include" : "omit";
|
|
58
|
+
try {
|
|
59
|
+
if (e.cookies) {
|
|
60
|
+
let e = await n(o + "/csrf", {
|
|
61
|
+
credentials: s,
|
|
62
|
+
cache: "no-store"
|
|
63
|
+
});
|
|
64
|
+
if (!e.ok) throw new t("transport_failed", e.status);
|
|
65
|
+
let r = await e.json();
|
|
66
|
+
if (!r || typeof r != "object" || !("token" in r) || typeof r.token != "string") throw new t("invalid_response");
|
|
67
|
+
a["x-csrf-token"] = r.token;
|
|
68
|
+
}
|
|
69
|
+
let c = await n(o + r, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
credentials: s,
|
|
72
|
+
headers: a,
|
|
73
|
+
body: JSON.stringify(i),
|
|
74
|
+
cache: "no-store"
|
|
75
|
+
});
|
|
76
|
+
if (!c.ok) throw new t(c.status === 401 ? "credential_rejected" : "transport_failed", c.status);
|
|
77
|
+
let l = await c.json();
|
|
78
|
+
if (!l || typeof l != "object" || Array.isArray(l)) throw new t("invalid_response");
|
|
79
|
+
return l;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
throw e instanceof t ? e : new t("transport_failed");
|
|
82
|
+
}
|
|
83
|
+
}, d = e.apiBaseUrl.replace(/\/+$/, "") + (e.accountPrefix ?? "/auth"), f = async (e, r, i, a = "json") => {
|
|
84
|
+
try {
|
|
85
|
+
let o = await n(d + "/csrf", {
|
|
86
|
+
credentials: "include",
|
|
87
|
+
cache: "no-store"
|
|
88
|
+
});
|
|
89
|
+
if (!o.ok) throw new t("transport_failed", o.status);
|
|
90
|
+
let s = await o.json();
|
|
91
|
+
if (!s || typeof s != "object" || !("csrfToken" in s) || typeof s.csrfToken != "string") throw new t("invalid_response");
|
|
92
|
+
let c = await n(d + e, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
credentials: "include",
|
|
95
|
+
cache: "no-store",
|
|
96
|
+
...a === "redirect" ? { redirect: "manual" } : {},
|
|
97
|
+
headers: {
|
|
98
|
+
"content-type": "application/json",
|
|
99
|
+
"x-csrf-token": s.csrfToken
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(r)
|
|
102
|
+
});
|
|
103
|
+
if (a === "redirect" && (c.type === "opaqueredirect" || c.status >= 200 && c.status < 400)) return {};
|
|
104
|
+
if (!c.ok) throw new t(i(c.status), c.status);
|
|
105
|
+
let l = await c.json();
|
|
106
|
+
if (!l || typeof l != "object" || Array.isArray(l)) throw new t("invalid_response");
|
|
107
|
+
return l;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
throw e instanceof t ? e : new t("transport_failed");
|
|
110
|
+
}
|
|
111
|
+
}, p = async () => {
|
|
112
|
+
if (!e.cookies) return null;
|
|
113
|
+
let r;
|
|
114
|
+
try {
|
|
115
|
+
r = await n(d + "/session", {
|
|
116
|
+
credentials: "include",
|
|
117
|
+
cache: "no-store"
|
|
118
|
+
});
|
|
119
|
+
} catch {
|
|
120
|
+
throw new t("transport_failed");
|
|
121
|
+
}
|
|
122
|
+
if (r.status === 401 || r.status === 404) return null;
|
|
123
|
+
if (!r.ok) throw new t("transport_failed", r.status);
|
|
124
|
+
let i;
|
|
125
|
+
try {
|
|
126
|
+
i = await r.json();
|
|
127
|
+
} catch {
|
|
128
|
+
throw new t("invalid_response");
|
|
129
|
+
}
|
|
130
|
+
let a = i && typeof i == "object" ? i.subject : void 0;
|
|
131
|
+
if (!a || a.type !== "user" || typeof a.id != "string" || !a.id.trim() || typeof a.tenantId != "string") throw new t("invalid_response");
|
|
132
|
+
return a.kind === "guest" ? null : { subject: a };
|
|
133
|
+
}, m = async (n, i) => {
|
|
134
|
+
let a = r(n, i.current);
|
|
135
|
+
if (n.deviceCredential !== void 0 && n.deviceCredential !== i.next) throw new t("invalid_response");
|
|
136
|
+
return await l(() => e.storage.write(i.next)), a;
|
|
137
|
+
}, h = async (e) => {
|
|
138
|
+
let n;
|
|
139
|
+
try {
|
|
140
|
+
n = await u("/resume", { credential: e.next });
|
|
141
|
+
} catch (r) {
|
|
142
|
+
if (!(r instanceof t) || r.code !== "credential_rejected") throw r;
|
|
143
|
+
n = await u("/rotate", {
|
|
144
|
+
credential: e.current,
|
|
145
|
+
nextCredential: e.next
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return m(n, e);
|
|
149
|
+
}, g = () => c(async () => {
|
|
150
|
+
let n = a(await l(() => e.storage.read()));
|
|
151
|
+
if (typeof n == "object" && n) return h(n);
|
|
152
|
+
if (n !== null) return r(await u("/resume", { credential: n }), n);
|
|
153
|
+
if (await p()) throw new t("account_signed_in");
|
|
154
|
+
let i = await u("/create", {});
|
|
155
|
+
if (typeof i.deviceCredential != "string" || !i.deviceCredential) throw new t("invalid_response");
|
|
156
|
+
let o = r(i, i.deviceCredential);
|
|
157
|
+
try {
|
|
158
|
+
await l(() => e.storage.write(String(i.deviceCredential)));
|
|
159
|
+
} catch (e) {
|
|
160
|
+
try {
|
|
161
|
+
await u("/revoke", { credential: i.deviceCredential });
|
|
162
|
+
} catch {}
|
|
163
|
+
throw e;
|
|
164
|
+
}
|
|
165
|
+
return o;
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
connect: g,
|
|
169
|
+
rotate: () => c(async () => {
|
|
170
|
+
let n = a(await l(() => e.storage.read()));
|
|
171
|
+
if (n === null) throw new t("credential_rejected");
|
|
172
|
+
if (typeof n == "object") return h(n);
|
|
173
|
+
let r = await u("/prepare-rotation", { credential: n });
|
|
174
|
+
if (typeof r.deviceCredential != "string" || !i(n, r.deviceCredential)) throw new t("invalid_response");
|
|
175
|
+
let o = {
|
|
176
|
+
kind: "guest-rotation",
|
|
177
|
+
current: n,
|
|
178
|
+
next: r.deviceCredential
|
|
179
|
+
};
|
|
180
|
+
return await l(() => e.storage.write(JSON.stringify(o))), m(await u("/rotate", {
|
|
181
|
+
credential: n,
|
|
182
|
+
nextCredential: o.next
|
|
183
|
+
}), o);
|
|
184
|
+
}),
|
|
185
|
+
revoke: () => c(async () => {
|
|
186
|
+
let n = a(await l(() => e.storage.read()));
|
|
187
|
+
if (n !== null) {
|
|
188
|
+
for (let e of typeof n == "string" ? [n] : [n.next, n.current]) try {
|
|
189
|
+
await u("/revoke", { credential: e });
|
|
190
|
+
break;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
if (!(e instanceof t) || e.code !== "credential_rejected") throw e;
|
|
193
|
+
}
|
|
194
|
+
await l(() => e.storage.remove());
|
|
195
|
+
}
|
|
196
|
+
}),
|
|
197
|
+
upgrade: (n) => c(async () => {
|
|
198
|
+
let r = a(await l(() => e.storage.read()));
|
|
199
|
+
if (typeof r != "string") throw new t("credential_rejected");
|
|
200
|
+
let i = await f("/upgrade-guest", {
|
|
201
|
+
credential: r,
|
|
202
|
+
email: n.email,
|
|
203
|
+
password: n.password
|
|
204
|
+
}, (e) => e === 401 ? "credential_rejected" : e === 409 ? "upgrade_conflict" : "transport_failed");
|
|
205
|
+
if (i.ok !== !0) throw new t("invalid_response");
|
|
206
|
+
return await l(() => e.storage.remove()), { subject: i.subject };
|
|
207
|
+
}),
|
|
208
|
+
account: () => p(),
|
|
209
|
+
start: async () => {
|
|
210
|
+
let e = await p();
|
|
211
|
+
if (e) return {
|
|
212
|
+
kind: "account",
|
|
213
|
+
subject: e.subject
|
|
214
|
+
};
|
|
215
|
+
let t = await g();
|
|
216
|
+
return {
|
|
217
|
+
kind: "guest",
|
|
218
|
+
subject: t.subject,
|
|
219
|
+
session: t.session
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
signIn: async (e) => {
|
|
223
|
+
await f("/sign-in", {
|
|
224
|
+
email: e.email,
|
|
225
|
+
password: e.password
|
|
226
|
+
}, (e) => e === 400 || e === 401 || e === 429 ? "credential_rejected" : "transport_failed");
|
|
227
|
+
let n = await p();
|
|
228
|
+
if (!n) throw new t("credential_rejected");
|
|
229
|
+
return n;
|
|
230
|
+
},
|
|
231
|
+
signOut: async () => {
|
|
232
|
+
await f("/sign-out", {}, () => "transport_failed", "redirect");
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
//#endregion
|
|
237
|
+
export { t as GuestClientError, o as createGuestClient };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/guestCredential.ts
|
|
2
|
+
var e = (e) => {
|
|
3
|
+
if (typeof e != "string" || e.length > 512) return null;
|
|
4
|
+
let [t, n, r, i] = e.split(".");
|
|
5
|
+
return !t || !/^guest_[a-zA-Z0-9_-]+$/.test(t) || !n || !/^gdev_[a-zA-Z0-9_-]+$/.test(n) || !r || !/^[a-zA-Z0-9_-]{43}$/.test(r) || i !== void 0 ? null : {
|
|
6
|
+
guestId: t,
|
|
7
|
+
deviceId: n,
|
|
8
|
+
secret: r
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { e as parseGuestCredential };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { hashPassword as e } from "./password.js";
|
|
2
|
+
import { newAuthId as t } from "./userStore.js";
|
|
3
|
+
import { dataStoreSessionRegistry as n } from "./sessionRegistry.js";
|
|
4
|
+
import { GuestAuthError as r } from "./guestService.js";
|
|
5
|
+
import { decideAdmission as i } from "./admission.js";
|
|
6
|
+
import { runSubjectGuards as a } from "./subjectGuard.js";
|
|
7
|
+
import { issueUserSession as o } from "./handlers.js";
|
|
8
|
+
import { Effect as s } from "effect";
|
|
9
|
+
import { and as c, eq as l } from "@voltro/database";
|
|
10
|
+
//#region src/guestPasswordUpgrade.ts
|
|
11
|
+
var u = (e, t, n) => ({
|
|
12
|
+
table: e,
|
|
13
|
+
predicate: l(t, n),
|
|
14
|
+
order: [],
|
|
15
|
+
take: void 0,
|
|
16
|
+
skip: void 0,
|
|
17
|
+
projection: void 0
|
|
18
|
+
}), d = (e, t, n) => ({
|
|
19
|
+
kind: "refused",
|
|
20
|
+
status: e,
|
|
21
|
+
code: t,
|
|
22
|
+
message: n
|
|
23
|
+
}), f = (f) => s.gen(function* () {
|
|
24
|
+
let { email: p, password: m, config: h } = f;
|
|
25
|
+
if (!p || !m) return d(400, "invalid", "email and password required");
|
|
26
|
+
if (m.length < 8) return d(400, "invalid", "password must be at least 8 characters");
|
|
27
|
+
if (yield* f.users.findByEmail(p)) return d(409, "email_taken", "email already registered");
|
|
28
|
+
let g = h.defaultTenantId, _ = yield* i(h.admit, {
|
|
29
|
+
email: p,
|
|
30
|
+
tenantId: g,
|
|
31
|
+
via: "sign-up"
|
|
32
|
+
});
|
|
33
|
+
if (_) return d(403, "not_admitted", _.message ?? _.reason);
|
|
34
|
+
let v = yield* e(m);
|
|
35
|
+
return yield* f.service.upgrade(f.credential, async (e) => {
|
|
36
|
+
let i = e.store;
|
|
37
|
+
if (e.tenantId !== g) throw new r({ code: "credential_rejected" });
|
|
38
|
+
if ((await i.query(u("users", "email", p))).length !== 0) throw new r({ code: "upgrade_conflict" });
|
|
39
|
+
let d = {
|
|
40
|
+
id: e.subjectId,
|
|
41
|
+
tenantId: g,
|
|
42
|
+
email: p,
|
|
43
|
+
passwordHash: v,
|
|
44
|
+
createdAt: e.createdAt,
|
|
45
|
+
mfaSecret: null,
|
|
46
|
+
mfaEnrolledAt: null,
|
|
47
|
+
emailVerifiedAt: null
|
|
48
|
+
};
|
|
49
|
+
if (!(await s.runPromise(a(h.subjectGuards ?? [], d))).ok) throw new r({ code: "credential_rejected" });
|
|
50
|
+
await i.insert("users", {
|
|
51
|
+
...d,
|
|
52
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
53
|
+
}), await i.insert("memberships", {
|
|
54
|
+
id: t("mem"),
|
|
55
|
+
userId: d.id,
|
|
56
|
+
tenantId: g,
|
|
57
|
+
role: "owner",
|
|
58
|
+
joinedAt: /* @__PURE__ */ new Date()
|
|
59
|
+
});
|
|
60
|
+
let m = {
|
|
61
|
+
...n(i),
|
|
62
|
+
listMemberships: (e) => s.promise(async () => (await i.query({
|
|
63
|
+
...u("memberships", "userId", e),
|
|
64
|
+
predicate: c(l("userId", e), l("tenantId", g))
|
|
65
|
+
})).map((t) => ({
|
|
66
|
+
userId: e,
|
|
67
|
+
tenantId: g,
|
|
68
|
+
role: String(t.role),
|
|
69
|
+
joinedAt: t.joinedAt instanceof Date ? t.joinedAt : new Date(String(t.joinedAt))
|
|
70
|
+
})))
|
|
71
|
+
}, _ = new Proxy(f.users, { get: (e, t) => typeof t == "string" && t in m ? m[t] : Reflect.get(e, t) }), y = await s.runPromise(o(d, _, h, {
|
|
72
|
+
clearLoginFailures: !1,
|
|
73
|
+
ipAddress: f.ipAddress ?? null,
|
|
74
|
+
userAgent: f.userAgent ?? null
|
|
75
|
+
}));
|
|
76
|
+
return {
|
|
77
|
+
kind: "ok",
|
|
78
|
+
userId: d.id,
|
|
79
|
+
subject: y.subject,
|
|
80
|
+
setCookie: y.setCookie
|
|
81
|
+
};
|
|
82
|
+
}).pipe(s.catch((e) => s.succeed(e.code === "upgrade_conflict" ? d(409, "email_taken", "email already registered") : e.code === "credential_rejected" ? d(401, "guest_rejected", "guest credential rejected") : d(503, "unavailable", "upgrade unavailable"))));
|
|
83
|
+
}).pipe(s.catch(() => s.succeed(d(500, "upgrade_failed", "upgrade failed"))));
|
|
84
|
+
//#endregion
|
|
85
|
+
export { f as completeGuestPasswordUpgrade };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { clearSessionCookie as e, resolveSessionSecret as t, sessionSecretsOf as n } from "./session.js";
|
|
2
|
+
import { CSRF_COOKIE_NAME as r, issueCsrfToken as i, verifyCsrf as a } from "./csrf.js";
|
|
3
|
+
import { validateGuestSecrets as o } from "./guestSecrets.js";
|
|
4
|
+
import { GuestAuthError as s, dataStoreGuestAuth as c } from "./guestService.js";
|
|
5
|
+
import { guestTables as l } from "./guestSchema.js";
|
|
6
|
+
import { sessionsTable as u } from "./schema.js";
|
|
7
|
+
import { Duration as d, Effect as f, Result as p } from "effect";
|
|
8
|
+
import { buildSetCookie as m, noteSigningLifetime as h, readCookie as g, readSessionValue as _, verifySessionKeyed as v } from "@voltro/protocol/session";
|
|
9
|
+
import { definePlugin as y } from "@voltro/protocol";
|
|
10
|
+
//#region src/guestPlugin.ts
|
|
11
|
+
var b = (e, t, n = {}) => ({
|
|
12
|
+
status: e,
|
|
13
|
+
body: JSON.stringify(t),
|
|
14
|
+
contentType: "application/json",
|
|
15
|
+
headers: {
|
|
16
|
+
"cache-control": "no-store",
|
|
17
|
+
...n
|
|
18
|
+
}
|
|
19
|
+
}), x = (x) => {
|
|
20
|
+
let S = x.prefix ?? "/auth/guest";
|
|
21
|
+
if (!/^\/[a-zA-Z0-9/_-]+$/.test(S) || S.endsWith("/")) throw Error("Guest route prefix must be an absolute path without a trailing slash");
|
|
22
|
+
if (!x.tenantId.trim()) throw Error("Guest authentication requires a server-owned tenant");
|
|
23
|
+
let C = d.toSeconds(x.ttl ?? "365 days"), w = d.toSeconds(x.sessionTtl ?? "7 days");
|
|
24
|
+
if (![C, w].every((e) => Number.isSafeInteger(e) && e > 0 && e <= 31536e4)) throw Error("Guest authentication lifetimes must be positive bounded whole seconds");
|
|
25
|
+
h("session", w * 1e3);
|
|
26
|
+
let T = () => o(x.secret ?? t()), E, D = /* @__PURE__ */ new WeakMap(), O = (e) => {
|
|
27
|
+
let t = D.get(e);
|
|
28
|
+
if (t) return t;
|
|
29
|
+
let n = x.service?.(e) ?? c(e, {
|
|
30
|
+
tenantId: x.tenantId,
|
|
31
|
+
secret: T,
|
|
32
|
+
ttlSeconds: C,
|
|
33
|
+
sessionTtlSeconds: w,
|
|
34
|
+
upgradeable: x.upgradeable ?? !0,
|
|
35
|
+
...x.secure === void 0 ? {} : { secure: x.secure }
|
|
36
|
+
});
|
|
37
|
+
return D.set(e, n), n;
|
|
38
|
+
}, k = (e) => f.gen(function* () {
|
|
39
|
+
return e.tenantId !== x.tenantId || e.kind !== "guest" ? yield* f.fail(new s({ code: "credential_rejected" })) : E ? O(E) : yield* f.fail(new s({ code: "store_unavailable" }));
|
|
40
|
+
}), A = async (t) => {
|
|
41
|
+
let n = T(), o = typeof n == "string" ? n : n.current.secret;
|
|
42
|
+
if (t.method === "GET" && t.path === S + "/csrf") {
|
|
43
|
+
let e = i(o);
|
|
44
|
+
return b(200, { token: e }, { "set-cookie": m(r, e, {
|
|
45
|
+
path: "/",
|
|
46
|
+
sameSite: "lax",
|
|
47
|
+
secure: x.secure ?? !0,
|
|
48
|
+
httpOnly: !1,
|
|
49
|
+
maxAgeSeconds: 3600
|
|
50
|
+
}) });
|
|
51
|
+
}
|
|
52
|
+
if (![
|
|
53
|
+
"/create",
|
|
54
|
+
"/resume",
|
|
55
|
+
"/prepare-rotation",
|
|
56
|
+
"/rotate",
|
|
57
|
+
"/revoke"
|
|
58
|
+
].some((e) => t.path === S + e)) return b(404, { error: "not_found" });
|
|
59
|
+
if (t.method !== "POST") return b(405, { error: "method_not_allowed" }, { allow: "POST" });
|
|
60
|
+
if ((t.headers["content-type"] ?? "").split(";")[0]?.trim() !== "application/json") return b(415, { error: "json_required" });
|
|
61
|
+
if (t.rawBody.byteLength > 2048) return b(413, { error: "body_too_large" });
|
|
62
|
+
if (t.headers.cookie && !a(t.headers["x-csrf-token"], g(t.headers.cookie, "voltro:csrf"), o)) return b(403, { error: "csrf_rejected" });
|
|
63
|
+
let s = t.store ?? E;
|
|
64
|
+
if (!s) return b(503, { error: "store_unavailable" });
|
|
65
|
+
let c;
|
|
66
|
+
try {
|
|
67
|
+
c = JSON.parse(new TextDecoder().decode(t.rawBody));
|
|
68
|
+
} catch {
|
|
69
|
+
return b(400, { error: "invalid_input" });
|
|
70
|
+
}
|
|
71
|
+
if (typeof c != "object" || !c || Array.isArray(c)) return b(400, { error: "invalid_input" });
|
|
72
|
+
let l = c, u = t.path === S + "/create", d = t.path === S + "/rotate";
|
|
73
|
+
if (Object.keys(l).some((e) => u || e !== "credential" && !(d && e === "nextCredential")) || !u && typeof l.credential != "string" || d && typeof l.nextCredential != "string") return b(400, { error: "invalid_input" });
|
|
74
|
+
let h = O(s);
|
|
75
|
+
if (u && _(t.headers)) return b(409, { error: "session_already_present" });
|
|
76
|
+
let v = typeof l.credential == "string" ? l.credential : "";
|
|
77
|
+
if (t.path === S + "/prepare-rotation") {
|
|
78
|
+
let e = await f.runPromise(f.result(h.prepareRotation(v)));
|
|
79
|
+
return p.isFailure(e) ? b(e.failure.code === "credential_rejected" ? 401 : 503, { error: e.failure.code }) : b(200, e.success);
|
|
80
|
+
}
|
|
81
|
+
let y = await f.runPromise(f.result(u ? h.create() : t.path === S + "/resume" ? h.resume(v) : d ? h.rotate(v, String(l.nextCredential)) : h.revoke(v).pipe(f.as(null))));
|
|
82
|
+
if (p.isFailure(y)) return b(y.failure.code === "credential_rejected" ? 401 : 503, { error: y.failure.code });
|
|
83
|
+
if (y.success === null) return b(200, { revoked: !0 }, { "set-cookie": e({ secure: x.secure ?? !0 }) });
|
|
84
|
+
let { subject: C, value: w, setCookie: D, deviceCredential: k } = y.success;
|
|
85
|
+
return b(200, {
|
|
86
|
+
subject: C,
|
|
87
|
+
session: w,
|
|
88
|
+
...k === void 0 ? {} : { deviceCredential: k }
|
|
89
|
+
}, { "set-cookie": D });
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
...y({
|
|
93
|
+
name: "@voltro/plugin-auth#guest",
|
|
94
|
+
baseName: "@voltro/plugin-auth",
|
|
95
|
+
permissions: ["store:write"],
|
|
96
|
+
extendSchema: { tables: [...l, u] },
|
|
97
|
+
declaredEnv: x.secret === void 0 ? [{
|
|
98
|
+
name: "VOLTRO_SESSION_SECRET",
|
|
99
|
+
required: !0,
|
|
100
|
+
secret: !0,
|
|
101
|
+
generate: "base64url",
|
|
102
|
+
description: "Signs this project's ordinary and guest sessions"
|
|
103
|
+
}] : [],
|
|
104
|
+
privacy: { collects: [{
|
|
105
|
+
category: "identifiers",
|
|
106
|
+
linked: !0,
|
|
107
|
+
tracking: !1,
|
|
108
|
+
purposes: ["app-functionality"]
|
|
109
|
+
}] },
|
|
110
|
+
subjectPrivacy: {
|
|
111
|
+
tables: [...l, u].map((e) => e.tableName),
|
|
112
|
+
kinds: ["guest"],
|
|
113
|
+
erasePhase: "identity"
|
|
114
|
+
},
|
|
115
|
+
onErase: (e, t) => k(t).pipe(f.flatMap((t) => t.erase(e)), f.asVoid),
|
|
116
|
+
onExport: (e, t) => k(t).pipe(f.flatMap((t) => t.export(e))),
|
|
117
|
+
httpRoutes: [{
|
|
118
|
+
method: "*",
|
|
119
|
+
path: S,
|
|
120
|
+
handle: A
|
|
121
|
+
}],
|
|
122
|
+
bindDataStore: (e) => {
|
|
123
|
+
E = e;
|
|
124
|
+
},
|
|
125
|
+
onActivate: () => f.sync(() => {
|
|
126
|
+
T();
|
|
127
|
+
}),
|
|
128
|
+
onDeactivate: () => f.sync(() => {
|
|
129
|
+
E = void 0;
|
|
130
|
+
})
|
|
131
|
+
}),
|
|
132
|
+
guest: { service: () => {
|
|
133
|
+
if (!E) throw Error("Guest lifecycle requires the bound app DataStore");
|
|
134
|
+
return O(E);
|
|
135
|
+
} },
|
|
136
|
+
auth: { sessionStrategy: {
|
|
137
|
+
id: "voltro-guest",
|
|
138
|
+
resolve: async (e) => {
|
|
139
|
+
let t = _(e.headers);
|
|
140
|
+
if (!t) return { kind: "skip" };
|
|
141
|
+
let r = v(t, n(T()));
|
|
142
|
+
if (!r) return {
|
|
143
|
+
kind: "failed",
|
|
144
|
+
reason: "voltro session cookie failed verification"
|
|
145
|
+
};
|
|
146
|
+
if (r.subject.type !== "user" || r.subject.kind !== "guest") return { kind: "skip" };
|
|
147
|
+
let i = e.store ?? E;
|
|
148
|
+
if (!i) return {
|
|
149
|
+
kind: "failed",
|
|
150
|
+
reason: "guest session registry unavailable"
|
|
151
|
+
};
|
|
152
|
+
let a = await f.runPromise(f.result(O(i).isLive(r.subject)));
|
|
153
|
+
return p.isSuccess(a) && a.success ? {
|
|
154
|
+
kind: "matched",
|
|
155
|
+
subject: r.subject,
|
|
156
|
+
credentialExpiresAt: r.exp * 1e3
|
|
157
|
+
} : {
|
|
158
|
+
kind: "failed",
|
|
159
|
+
reason: "guest session revoked or expired"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
} }
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
//#endregion
|
|
166
|
+
export { x as guestAuthPlugin };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { boolean as e, id as t, integer as n, table as r, text as i, timestamp as a } from "@voltro/database";
|
|
2
|
+
//#region src/guestSchema.ts
|
|
3
|
+
var o = r("authGuests", {
|
|
4
|
+
id: t(),
|
|
5
|
+
tenantId: i(),
|
|
6
|
+
upgradeable: e(),
|
|
7
|
+
expiresAt: a(),
|
|
8
|
+
createdAt: a(),
|
|
9
|
+
lastSeenAt: a()
|
|
10
|
+
}).index(["tenantId"]), s = r("authGuestDevices", {
|
|
11
|
+
id: t(),
|
|
12
|
+
guestId: i(),
|
|
13
|
+
tenantId: i(),
|
|
14
|
+
secretHash: i().serverOnly(),
|
|
15
|
+
generation: n(),
|
|
16
|
+
expiresAt: a(),
|
|
17
|
+
createdAt: a()
|
|
18
|
+
}).index(["guestId"]).index(["tenantId"]), c = [o, s];
|
|
19
|
+
//#endregion
|
|
20
|
+
export { s as authGuestDevicesTable, o as authGuestsTable, c as guestTables };
|