@voltro/plugin-auth 0.84.0 → 0.86.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.
@@ -0,0 +1,120 @@
1
+ import { hashToken as e, mintToken as t } from "./tokens.js";
2
+ import { configSigningKey as n, issueUserSession as r } from "./handlers.js";
3
+ import { createHash as i, createHmac as a, randomBytes as o, timingSafeEqual as s } from "node:crypto";
4
+ import { Effect as c } from "effect";
5
+ import { buildSetCookie as l, readCookie as u } from "@voltro/protocol/session";
6
+ //#region src/nativeRoutes.ts
7
+ var d = "voltro:native", f = 600, p = "/login", m = (e) => e.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), h = (e) => Buffer.from(e.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - e.length % 4) % 4), "base64"), g = (e) => m(i("sha256").update(e, "ascii").digest()), _ = (e, t) => {
8
+ let n = m(Buffer.from(JSON.stringify(e), "utf8"));
9
+ return `${n}.${m(a("sha256", t).update(n).digest())}`;
10
+ }, v = (e, t, n = Date.now) => {
11
+ if (!e) return null;
12
+ let r = e.indexOf(".");
13
+ if (r <= 0 || r === e.length - 1) return null;
14
+ let i = e.slice(0, r), o = h(e.slice(r + 1)), c = a("sha256", t).update(i).digest();
15
+ if (o.length !== c.length || !s(o, c)) return null;
16
+ try {
17
+ let e = JSON.parse(h(i).toString("utf8"));
18
+ return typeof e.exp != "number" || e.exp * 1e3 < n() ? null : e;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }, y = (e, t) => t.includes(e), b = (e, t) => {
23
+ let n = new URLSearchParams(t).toString();
24
+ return `${e}${e.includes("?") ? "&" : "?"}${n}`;
25
+ }, x = (e, t) => ({
26
+ status: e,
27
+ body: JSON.stringify(t),
28
+ contentType: "application/json; charset=utf-8"
29
+ }), S = (e, t) => ({
30
+ status: 302,
31
+ body: "",
32
+ location: e,
33
+ ...t === void 0 ? {} : { setCookie: t }
34
+ }), C = (e) => {
35
+ if (!e) return null;
36
+ let { ch: t, r: n, s: r, exp: i } = e;
37
+ return typeof t != "string" || typeof n != "string" || typeof r != "string" || typeof i != "number" ? null : {
38
+ ch: t,
39
+ r: n,
40
+ s: r,
41
+ exp: i
42
+ };
43
+ }, w = (e, t, n) => l(e.native.pendingCookieName ?? "voltro:native", t, {
44
+ maxAgeSeconds: n,
45
+ httpOnly: !0,
46
+ sameSite: "lax",
47
+ secure: e.config.cookieSecure ?? !0,
48
+ path: `${e.prefix}/native`
49
+ }), T = (e) => `${e.native.loginPath ?? p}?next=${encodeURIComponent(`${e.prefix}/native/complete`)}`, E = (e, t, r, i) => c.gen(function* () {
50
+ let a = e.get("redirect_uri") ?? "", o = e.get("code_challenge") ?? "", s = e.get("code_challenge_method") ?? "S256", c = e.get("state") ?? "";
51
+ if (!y(a, i.native.redirectUris)) return x(400, { error: "redirect_uri_not_allowed" });
52
+ if (s !== "S256" || o.length < 43 || o.length > 128) return S(b(a, {
53
+ error: "invalid_request",
54
+ state: c
55
+ }));
56
+ if (c.length === 0) return S(b(a, { error: "invalid_request" }));
57
+ let l = i.now ?? Date.now, u = i.native.pendingTtlSeconds ?? f, d = {
58
+ ch: o,
59
+ r: a,
60
+ s: c,
61
+ exp: Math.floor(l() / 1e3) + u
62
+ }, p = w(i, _({ ...d }, n(i.config)), u);
63
+ return t && t.subject.type === "user" && t.sessionId ? {
64
+ ...yield* D(d, {
65
+ userId: t.subject.id,
66
+ sessionId: t.sessionId
67
+ }, r, i),
68
+ setCookie: w(i, "", 0)
69
+ } : S(T(i), p);
70
+ }), D = (e, r, i, a) => c.gen(function* () {
71
+ let s = t("native-code", 300);
72
+ yield* i.insertToken({
73
+ id: `tok_${m(o(12))}`,
74
+ tokenHash: s.tokenHash,
75
+ userId: r.userId,
76
+ purpose: "native-code",
77
+ expiresAt: s.expiresAt
78
+ });
79
+ let c = _({
80
+ sid: r.sessionId,
81
+ uid: r.userId,
82
+ ch: e.ch,
83
+ exp: Math.floor(s.expiresAt.getTime() / 1e3)
84
+ }, n(a.config));
85
+ return S(b(e.r, {
86
+ code: `${s.token}.${c}`,
87
+ state: e.s
88
+ }));
89
+ }), O = (e, t, r, i) => c.gen(function* () {
90
+ let a = C(v(u(e, i.native.pendingCookieName ?? "voltro:native"), n(i.config), i.now));
91
+ return a ? !t || t.subject.type !== "user" || !t.sessionId ? S(T(i)) : {
92
+ ...yield* D(a, {
93
+ userId: t.subject.id,
94
+ sessionId: t.sessionId
95
+ }, r, i),
96
+ setCookie: w(i, "", 0)
97
+ } : x(400, { error: "no_pending_native_login" });
98
+ }), k = (t, i, a) => c.gen(function* () {
99
+ let o = t.code.indexOf(".");
100
+ if (o <= 0) return x(400, { error: "invalid_code" });
101
+ let s = t.code.slice(0, o), c = v(t.code.slice(o + 1), n(a.config), a.now), l = c?.sid, u = c?.uid, d = c?.ch;
102
+ if (typeof l != "string" || typeof u != "string" || typeof d != "string") return x(400, { error: "invalid_code" });
103
+ let f = yield* i.consumeToken(e(s), "native-code");
104
+ if (!f || f.userId !== u || t.codeVerifier.length < 43 || g(t.codeVerifier) !== d) return x(400, { error: "invalid_code" });
105
+ let p = yield* i.findById(u);
106
+ if (!p) return x(400, { error: "invalid_code" });
107
+ let m = yield* r(p, i, a.config, {
108
+ sessionId: l,
109
+ sessionRow: "existing",
110
+ clearLoginFailures: !1
111
+ });
112
+ return x(200, {
113
+ ok: !0,
114
+ session: m.value,
115
+ expiresAt: m.expiresAt,
116
+ subject: m.subject
117
+ });
118
+ });
119
+ //#endregion
120
+ export { d as NATIVE_PENDING_COOKIE, E as handleNativeAuthorize, O as handleNativeComplete, k as handleNativeExchange, y as isAllowedRedirectUri, T as nativeLoginRedirect, g as s256, _ as signClaims, v as verifyClaims };
@@ -0,0 +1,162 @@
1
+ import { issueSession as e } from "./session.js";
2
+ import { newAuthId as t, subjectFromUser as n } from "./userStore.js";
3
+ import { configSessionSecrets as r, guardSubject as i } from "./handlers.js";
4
+ import { generateChallenge as a, verifyAssertion as o, verifyRegistration as s } from "./webauthn.js";
5
+ import { Effect as c } from "effect";
6
+ //#region src/passkeyHandlers.ts
7
+ var l = (e, t, n = {}) => ({
8
+ status: e,
9
+ body: JSON.stringify(t),
10
+ contentType: "application/json; charset=utf-8",
11
+ ...n
12
+ }), u = 604800, d = () => {
13
+ let e = /* @__PURE__ */ new Map();
14
+ return {
15
+ put: (t, n, r) => c.sync(() => {
16
+ e.set(t, {
17
+ challenge: n,
18
+ expiresAt: Date.now() + r * 1e3
19
+ });
20
+ }),
21
+ take: (t) => c.sync(() => {
22
+ let n = e.get(t);
23
+ return e.delete(t), !n || n.expiresAt < Date.now() ? null : n.challenge;
24
+ })
25
+ };
26
+ }, f = (e) => ({
27
+ put: (n, r, i) => c.gen(function* () {
28
+ let a = /* @__PURE__ */ new Date(), o = new Date(a.getTime() + i * 1e3);
29
+ (yield* e`
30
+ UPDATE "passkeyChallenges"
31
+ SET challenge = ${r}, "expiresAt" = ${o}, "createdAt" = ${a}
32
+ WHERE key = ${n}
33
+ RETURNING key
34
+ `).length === 0 && (yield* e`
35
+ INSERT INTO "passkeyChallenges" (id, key, challenge, "expiresAt", "createdAt")
36
+ VALUES (${t("pkc")}, ${n}, ${r}, ${o}, ${a})
37
+ `);
38
+ }).pipe(c.asVoid, c.orElseSucceed(() => void 0)),
39
+ take: (t) => c.gen(function* () {
40
+ let n = (yield* e`
41
+ DELETE FROM "passkeyChallenges" WHERE key = ${t}
42
+ RETURNING challenge, "expiresAt"
43
+ `)[0];
44
+ return n && n.expiresAt.getTime() > Date.now() ? n.challenge : null;
45
+ }).pipe(c.orElseSucceed(() => null))
46
+ }), p = 300, m = (e, t, n, r) => c.gen(function* () {
47
+ if (!(yield* t.findById(e.userId))) return l(404, { error: "user_not_found" });
48
+ let i = a();
49
+ yield* n.put(`${e.userId}:register`, i, p);
50
+ let o = yield* t.listPasskeys(e.userId);
51
+ return l(200, {
52
+ challenge: i,
53
+ rp: {
54
+ id: r.rpId,
55
+ name: r.rpName
56
+ },
57
+ user: {
58
+ id: e.userId,
59
+ name: e.userName,
60
+ displayName: e.userName
61
+ },
62
+ pubKeyCredParams: [{
63
+ type: "public-key",
64
+ alg: -7
65
+ }, {
66
+ type: "public-key",
67
+ alg: -257
68
+ }],
69
+ excludeCredentials: o.map((e) => ({
70
+ type: "public-key",
71
+ id: e.credentialId
72
+ })),
73
+ authenticatorSelection: {
74
+ userVerification: "preferred",
75
+ residentKey: "preferred"
76
+ },
77
+ timeout: p * 1e3
78
+ });
79
+ }), h = (e, n, r, i) => c.gen(function* () {
80
+ if (!(yield* n.findById(e.userId))) return l(404, { error: "user_not_found" });
81
+ let a = yield* r.take(`${e.userId}:register`);
82
+ if (!a) return l(400, { error: "no_pending_challenge" });
83
+ let o = s({
84
+ clientDataJSON: e.clientDataJSON,
85
+ authenticatorData: e.authenticatorData,
86
+ expectedChallenge: a,
87
+ expectedOrigin: i.origin,
88
+ rpId: i.rpId
89
+ });
90
+ return o.ok ? (yield* n.insertPasskey({
91
+ id: t("pk"),
92
+ credentialId: o.result.credentialId,
93
+ userId: e.userId,
94
+ publicKey: o.result.publicKey,
95
+ counter: o.result.counter,
96
+ transports: e.transports ?? null
97
+ }).pipe(c.ignore), l(200, {
98
+ ok: !0,
99
+ credentialId: o.result.credentialId
100
+ })) : l(400, { error: o.error });
101
+ }), g = (e, t, n, r) => c.gen(function* () {
102
+ let i = a(), o = e.userId ? `${e.userId}:assert` : "*:assert";
103
+ yield* n.put(o, i, p);
104
+ let s = e.userId ? (yield* t.listPasskeys(e.userId)).map((e) => ({
105
+ type: "public-key",
106
+ id: e.credentialId
107
+ })) : [];
108
+ return l(200, {
109
+ challenge: i,
110
+ rpId: r.rpId,
111
+ allowCredentials: s,
112
+ userVerification: "preferred",
113
+ timeout: p * 1e3
114
+ });
115
+ }), _ = (a, s, d, f, p) => c.gen(function* () {
116
+ let m = yield* s.findPasskey(a.credentialId);
117
+ if (!m) return l(401, { error: "unknown_credential" });
118
+ let h = a.userId ? `${a.userId}:assert` : "*:assert", g = yield* d.take(h);
119
+ if (!g) return l(400, { error: "no_pending_challenge" });
120
+ let _ = o({
121
+ clientDataJSON: a.clientDataJSON,
122
+ authenticatorData: a.authenticatorData,
123
+ signature: a.signature,
124
+ expectedChallenge: g,
125
+ expectedOrigin: f.origin,
126
+ rpId: f.rpId,
127
+ storedPublicKey: m.publicKey,
128
+ storedCounter: m.counter
129
+ });
130
+ if (!_.ok) return l(401, { error: _.error });
131
+ if (!(yield* s.advancePasskeyCounter(a.credentialId, _.result.newCounter)) && _.result.newCounter !== 0) return l(401, { error: "counter_regressed" });
132
+ let v = yield* s.findById(m.userId);
133
+ if (!v) return l(401, { error: "unknown_credential" });
134
+ let y = yield* i(v, p);
135
+ if (y) return y;
136
+ let b = yield* s.listMemberships(v.id), x = t("sess");
137
+ yield* s.insertSession({
138
+ id: x,
139
+ userId: v.id,
140
+ tenantId: v.tenantId,
141
+ expiresAt: new Date(Date.now() + u * 1e3),
142
+ ipAddress: null,
143
+ userAgent: null
144
+ }).pipe(c.ignore);
145
+ let S = n(v, { memberships: b }), C = S.type === "user" ? {
146
+ ...S,
147
+ metadata: {
148
+ ...S.metadata ?? {},
149
+ sessionId: x
150
+ }
151
+ } : S, w = e(C, r(p), {
152
+ ttlSeconds: u,
153
+ ...p.cookieDomain ? { domain: p.cookieDomain } : {},
154
+ secure: p.cookieSecure ?? !0
155
+ });
156
+ return l(200, {
157
+ ok: !0,
158
+ subject: C
159
+ }, { setCookie: w.setCookie });
160
+ });
161
+ //#endregion
162
+ export { f as dataStoreChallengeStore, g as handlePasskeyAssertOptions, _ as handlePasskeyAssertVerify, m as handlePasskeyRegisterOptions, h as handlePasskeyRegisterVerify, d as memoryChallengeStore };
package/dist/password.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { randomBytes as e, scrypt as t, timingSafeEqual as n } from "node:crypto";
2
2
  import { Data as r, Effect as i } from "effect";
3
3
  //#region src/password.ts
4
- var a = 32768, o = 8, s = 1, c = 32, l = 16, u = (e, t) => 128 * e * t * 2, d = 268435456, f = class extends r.TaggedError("PasswordEmptyError") {}, p = class extends r.TaggedError("PasswordHashError") {}, m = (e, n, r, a) => i.async((o) => {
4
+ var a = 32768, o = 8, s = 1, c = 32, l = 16, u = (e, t) => 128 * e * t * 2, d = 268435456, f = class extends r.TaggedError("PasswordEmptyError") {}, p = class extends r.TaggedError("PasswordHashError") {}, m = (e, n, r, a) => i.callback((o) => {
5
5
  t(e, n, r, {
6
6
  ...a,
7
7
  maxmem: u(a.N, a.r)
@@ -35,7 +35,7 @@ var a = 32768, o = 8, s = 1, c = 32, l = 16, u = (e, t) => 128 * e * t * 2, d =
35
35
  p: o
36
36
  });
37
37
  return l.length === c.length && n(l, c);
38
- }).pipe(i.catchAll(() => i.succeed(!1))), _ = (e) => {
38
+ }).pipe(i.catch(() => i.succeed(!1))), _ = (e) => {
39
39
  let t = e.split("$");
40
40
  if (t.length !== 6) return null;
41
41
  let [n, r, i, a, o, s] = t;
package/dist/plugin.d.ts CHANGED
@@ -3,7 +3,7 @@ import { ConnectionCredential } from '@voltro/protocol';
3
3
  import { DataStore } from '@voltro/database';
4
4
  import { Effect } from 'effect';
5
5
  import { SessionSecrets } from '@voltro/protocol/session';
6
- import { SqlClient } from '@effect/sql/SqlClient';
6
+ import { SqlClient } from 'effect/sql/SqlClient';
7
7
  import { Subject } from '@voltro/protocol';
8
8
  import { VoidIfEmpty } from 'effect/Types';
9
9
  import { VoltroPlugin } from '@voltro/protocol';
@@ -648,6 +648,10 @@ declare interface SendEmailInput {
648
648
  /** A server-side session row — enumerated so apps can list active
649
649
  * devices + revoke them. Written on sign-in; deleted on revoke. */
650
650
  declare interface SessionRecord {
651
+ /** Stable device lineage. The original row is its transaction lock anchor. */
652
+ readonly familyId?: string | null;
653
+ /** A replaced credential remains an inert revocation alias until the device ends. */
654
+ readonly rotatedAt?: Date | null;
651
655
  readonly id: string;
652
656
  readonly userId: string;
653
657
  readonly tenantId: string;