@voltro/plugin-auth 0.81.0 → 0.82.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 +1520 -6
- package/dist/index.d.ts +216 -3
- package/dist/index.js +77 -467
- package/dist/native.d.ts +105 -0
- package/dist/native.js +93 -0
- package/dist/plugin-CNW-X70p.js +2015 -0
- package/dist/plugin.d.ts +88 -3
- package/dist/plugin.js +1 -1
- package/dist/session.d.ts +15 -0
- package/dist/session.js +20 -14
- package/dist/{strategy-MP_KxNuA.js → strategy-XaGjSicX.js} +33 -12
- package/dist/strategy.d.ts +21 -1
- package/dist/strategy.js +1 -1
- package/dist/tokens.d.ts +5 -1
- package/dist/tokens.js +9 -8
- package/package.json +8 -3
- package/dist/plugin-DFQ8N52Q.js +0 -1334
package/dist/plugin.d.ts
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import { AuthStrategy } from '@voltro/protocol';
|
|
2
2
|
import { ConnectionCredential } from '@voltro/protocol';
|
|
3
|
+
import { DataStore } from '@voltro/database';
|
|
3
4
|
import { Effect } from 'effect';
|
|
4
5
|
import { SessionSecrets } from '@voltro/protocol/session';
|
|
6
|
+
import { SqlClient } from '@effect/sql/SqlClient';
|
|
5
7
|
import { Subject } from '@voltro/protocol';
|
|
6
8
|
import { VoidIfEmpty } from 'effect/Types';
|
|
7
9
|
import { VoltroPlugin } from '@voltro/protocol';
|
|
8
10
|
import { YieldableError } from 'effect/Cause';
|
|
9
11
|
|
|
12
|
+
/** What the decision can read: the app's database the plugin is bound to
|
|
13
|
+
* (undefined before the bind, and for a handler called without the plugin). */
|
|
14
|
+
declare interface AdmissionContext {
|
|
15
|
+
readonly store: DataStore | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Why a join is refused. */
|
|
19
|
+
declare interface AdmissionRefusal {
|
|
20
|
+
/** A stable code the client can branch on, e.g. `seats`. */
|
|
21
|
+
readonly reason: string;
|
|
22
|
+
/** A sentence for the person who was refused. */
|
|
23
|
+
readonly message: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Decide whether a person may join a tenant — `null` to let them in. */
|
|
27
|
+
declare type AdmitJoining = (joining: Joining, context: AdmissionContext) => Effect.Effect<AdmissionRefusal | null> | Promise<AdmissionRefusal | null>;
|
|
28
|
+
|
|
10
29
|
declare interface AuthConfig {
|
|
11
30
|
/**
|
|
12
31
|
* The session-signing key. OMIT IT and the value is read from
|
|
@@ -50,6 +69,28 @@ declare interface AuthConfig {
|
|
|
50
69
|
* but any concern (unverified email, suspended tenant, …) can hook here.
|
|
51
70
|
* Absent / empty ⇒ every authenticated user proceeds unchanged. */
|
|
52
71
|
readonly subjectGuards?: ReadonlyArray<SubjectGuard>;
|
|
72
|
+
/** Admission: asked before anyone JOINS a tenant — a sign-up, an invitation
|
|
73
|
+
* accepted with a new account or an existing one — and before anything is
|
|
74
|
+
* written (the account, the membership, the redeemed invitation). A refusal
|
|
75
|
+
* answers 403 `not_admitted` with its `reason` and `message`. The seam for a
|
|
76
|
+
* seat limit, an invite-only tenant, an address allow-list. Absent ⇒ every
|
|
77
|
+
* join proceeds. Signing in is not joining: subject guards govern that. */
|
|
78
|
+
readonly admit?: AdmitJoining;
|
|
79
|
+
/**
|
|
80
|
+
* Tenants a public sign-up may name (`tenantId` in the sign-up body) besides
|
|
81
|
+
* `defaultTenantId` — a readers' tenant beside the staff one. Anything else
|
|
82
|
+
* named is refused (400 `tenant_not_open`) before an account exists: the
|
|
83
|
+
* request must not choose the tenant it joins, or anyone could sign up into
|
|
84
|
+
* a stranger's. Joining a particular organisation goes through an invitation.
|
|
85
|
+
*/
|
|
86
|
+
readonly signUpTenants?: ReadonlyArray<string>;
|
|
87
|
+
/** Make sure the user has a row in the app's `actors` table before their
|
|
88
|
+
* session exists. The `audit()` mixin stamps `createdBy` / `updatedBy`
|
|
89
|
+
* with the user id, and those columns reference `actors`, so a user with no
|
|
90
|
+
* row there cannot write to any audited table on an FK-enforcing database.
|
|
91
|
+
* `authRoutesPlugin` wires this to the app store it is bound to; called by
|
|
92
|
+
* `issueUserSession`, so every session-granting path covers it. */
|
|
93
|
+
readonly ensureActor?: (user: UserRecord) => Effect.Effect<void>;
|
|
53
94
|
/** Brute-force lockout. After `maxAttempts` failed credential attempts
|
|
54
95
|
* (wrong password OR wrong MFA code) within `windowSeconds`, the account —
|
|
55
96
|
* keyed by EMAIL, so an unknown address locks exactly like a real one and
|
|
@@ -94,8 +135,12 @@ export declare interface AuthPlugin extends VoltroPlugin {
|
|
|
94
135
|
export declare const authRoutesPlugin: (options: AuthRoutesPluginOptions) => AuthPlugin;
|
|
95
136
|
|
|
96
137
|
export declare interface AuthRoutesPluginOptions extends AuthConfig {
|
|
97
|
-
/**
|
|
98
|
-
|
|
138
|
+
/**
|
|
139
|
+
* The user store backing every handler — a `UserStore`, or a
|
|
140
|
+
* `UserStoreResolver` (e.g. `appUserStore()`) that the plugin calls once the
|
|
141
|
+
* framework has opened the app's database, so users live in that database.
|
|
142
|
+
*/
|
|
143
|
+
readonly store: UserStore | UserStoreResolver;
|
|
99
144
|
/** Passkey config — when omitted, the four passkey routes return 501. */
|
|
100
145
|
readonly passkey?: PasskeyConfig;
|
|
101
146
|
/** Challenge store for the passkey ceremonies. Defaults to an in-memory
|
|
@@ -139,6 +184,13 @@ export declare interface AuthRoutesPluginOptions extends AuthConfig {
|
|
|
139
184
|
* on while leaving either unanswered. See `ImpersonationConfig`.
|
|
140
185
|
*/
|
|
141
186
|
readonly impersonation?: ImpersonationConfig;
|
|
187
|
+
/**
|
|
188
|
+
* Native login for cookie-less clients (React Native, desktop shells).
|
|
189
|
+
* PRESENT mounts `/auth/native/authorize`, `/native/complete` and
|
|
190
|
+
* `/native/exchange`; ABSENT mounts nothing. `redirectUris` is exact-match —
|
|
191
|
+
* an app names the custom-scheme or universal-link URL it listens on.
|
|
192
|
+
*/
|
|
193
|
+
readonly native?: NativeLoginConfig;
|
|
142
194
|
/** Disambiguates multiple instances of this plugin in one app. */
|
|
143
195
|
readonly name?: string;
|
|
144
196
|
}
|
|
@@ -418,6 +470,13 @@ declare interface InvitationStore {
|
|
|
418
470
|
readonly supersedePendingInvitations: (tenantId: string, email: string, revokedByUserId: string, now: Date) => Effect.Effect<number>;
|
|
419
471
|
}
|
|
420
472
|
|
|
473
|
+
/** Someone about to join a tenant. */
|
|
474
|
+
declare interface Joining {
|
|
475
|
+
readonly email: string;
|
|
476
|
+
readonly tenantId: string;
|
|
477
|
+
readonly via: 'sign-up' | 'invitation';
|
|
478
|
+
}
|
|
479
|
+
|
|
421
480
|
/**
|
|
422
481
|
* Brute-force lockout policy, resolved from `AuthConfig.lockout` and passed
|
|
423
482
|
* INTO the store on each failure so the store itself stays policy-free.
|
|
@@ -508,6 +567,19 @@ declare interface MfaStore {
|
|
|
508
567
|
readonly countRecoveryCodes: (userId: string) => Effect.Effect<number>;
|
|
509
568
|
}
|
|
510
569
|
|
|
570
|
+
declare interface NativeLoginConfig {
|
|
571
|
+
/** Exact redirect URIs the app may name — a custom scheme
|
|
572
|
+
* (`acme://auth/callback`) or an https universal link. No wildcards. */
|
|
573
|
+
readonly redirectUris: ReadonlyArray<string>;
|
|
574
|
+
/** The web app's sign-in page, relative to the app origin. Default `/login`.
|
|
575
|
+
* It must honour `?next=` (the auth template does). */
|
|
576
|
+
readonly loginPath?: string;
|
|
577
|
+
/** Lifetime of the pending request cookie. Default 10 minutes. */
|
|
578
|
+
readonly pendingTtlSeconds?: number;
|
|
579
|
+
/** Cookie name for the pending request. Default `voltro:native`. */
|
|
580
|
+
readonly pendingCookieName?: string;
|
|
581
|
+
}
|
|
582
|
+
|
|
511
583
|
declare interface PasskeyConfig {
|
|
512
584
|
/** Relying-party id — the registrable domain (e.g. `example.com`). */
|
|
513
585
|
readonly rpId: string;
|
|
@@ -622,6 +694,10 @@ declare interface SessionStore {
|
|
|
622
694
|
/** Revoke every session for the user EXCEPT `keepSessionId` — the
|
|
623
695
|
* "sign out other devices" action. Returns the count removed. */
|
|
624
696
|
readonly revokeAllOtherSessions: (userId: string, keepSessionId: string) => Effect.Effect<number>;
|
|
697
|
+
/** Revoke every session for the user — "sign out everywhere": a password
|
|
698
|
+
* reset, a deactivation, an account deletion, a security event. Returns
|
|
699
|
+
* the count removed. */
|
|
700
|
+
readonly revokeAllSessions: (userId: string) => Effect.Effect<number>;
|
|
625
701
|
}
|
|
626
702
|
|
|
627
703
|
/**
|
|
@@ -642,7 +718,7 @@ declare type SubjectGuardVerdict = {
|
|
|
642
718
|
readonly message: string;
|
|
643
719
|
};
|
|
644
720
|
|
|
645
|
-
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify';
|
|
721
|
+
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify' | 'native-code';
|
|
646
722
|
|
|
647
723
|
/** A single-use, hashed, expiring token (magic-link / password-reset). */
|
|
648
724
|
declare interface TokenRecord {
|
|
@@ -750,4 +826,13 @@ declare interface UserRecord {
|
|
|
750
826
|
declare interface UserStore extends IdentityStore, MfaStore, MembershipStore, SessionStore, TokenStore, InvitationStore, ImpersonationStore, PasskeyStore, LockoutStore {
|
|
751
827
|
}
|
|
752
828
|
|
|
829
|
+
/** What a resolver is handed once the app's database is open. */
|
|
830
|
+
declare interface UserStoreBindContext {
|
|
831
|
+
/** The framework's open pool — absent when the app runs `store: 'memory'`. */
|
|
832
|
+
readonly sql?: SqlClient;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Builds the `UserStore` once the framework has opened the app's database. */
|
|
836
|
+
declare type UserStoreResolver = (ctx: UserStoreBindContext) => UserStore;
|
|
837
|
+
|
|
753
838
|
export { }
|
package/dist/plugin.js
CHANGED
package/dist/session.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export declare interface IssueSessionOptions {
|
|
|
36
36
|
readonly ttlSeconds?: number;
|
|
37
37
|
readonly domain?: string;
|
|
38
38
|
readonly secure?: boolean;
|
|
39
|
+
/** Clock override (epoch ms) — a testing seam for back-dated sessions. */
|
|
40
|
+
readonly now?: () => number;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export { KeyedSecret }
|
|
@@ -53,6 +55,19 @@ export { KeyedSecret }
|
|
|
53
55
|
*/
|
|
54
56
|
export declare const readSession: (cookieHeader: string | undefined, secret: SessionSecretInput) => SubjectIdentity | null;
|
|
55
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Keyed read over the WHOLE header bag: the `x-voltro-session` header a
|
|
60
|
+
* cookie-less client presents (React Native, a desktop shell — the value it
|
|
61
|
+
* obtained through the native login exchange), or the session cookie a browser
|
|
62
|
+
* carries. Same verify, same keyed rotation, same renewal signal as
|
|
63
|
+
* `readSessionKeyed`; only where the value comes from differs.
|
|
64
|
+
*
|
|
65
|
+
* `readSessionKeyed` stays for the callers that hold a cookie string (an SSR
|
|
66
|
+
* layout reading `request.headers.cookie`); this is what the routes and the
|
|
67
|
+
* rpc strategy use, because they must accept both.
|
|
68
|
+
*/
|
|
69
|
+
export declare const readSessionFromHeaders: (headers: Readonly<Record<string, string | undefined>>, secret: SessionSecretInput, options?: VerifyOptions) => VerifyResult | null;
|
|
70
|
+
|
|
56
71
|
/**
|
|
57
72
|
* Keyed read: the full `VerifyResult` — subject + the `kid` that
|
|
58
73
|
* verified + the sliding-window `renew` flag + `exp`/`iat`. The auth
|
package/dist/session.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import { DEFAULT_SESSION_KID as e, buildSetCookie as t, readCookie as n,
|
|
1
|
+
import { DEFAULT_SESSION_KID as e, buildSetCookie as t, readCookie as n, readSessionValue as r, resolveOptionalSessionSecrets as i, resolveSessionSecret as a, resolveSessionSecrets as o, signSession as s, timingSafeStringEqual as c, verifySessionKeyed as l } from "@voltro/protocol/session";
|
|
2
2
|
//#region src/session.ts
|
|
3
|
-
var
|
|
3
|
+
var u = "voltro:session", d = 604800, f = a, p = o, m = (e) => typeof e == "string" ? e : e.current, h = (t) => {
|
|
4
4
|
if (typeof t != "string") return t;
|
|
5
|
-
let n =
|
|
6
|
-
kid: n &&
|
|
5
|
+
let n = i(), r = {
|
|
6
|
+
kid: n && c(n.current.secret, t) ? n.current.kid : e,
|
|
7
7
|
secret: t
|
|
8
8
|
};
|
|
9
9
|
return n?.previous ? {
|
|
10
|
-
current:
|
|
10
|
+
current: r,
|
|
11
11
|
previous: n.previous
|
|
12
|
-
} : { current:
|
|
13
|
-
},
|
|
14
|
-
let i = r.ttlSeconds ??
|
|
12
|
+
} : { current: r };
|
|
13
|
+
}, g = (e, n, r = {}) => {
|
|
14
|
+
let i = r.ttlSeconds ?? d, a = s(e, m(n), {
|
|
15
|
+
ttlSeconds: i,
|
|
16
|
+
...r.now === void 0 ? {} : { now: r.now }
|
|
17
|
+
});
|
|
15
18
|
return {
|
|
16
19
|
value: a,
|
|
17
|
-
setCookie: t(
|
|
20
|
+
setCookie: t(u, a, {
|
|
18
21
|
maxAgeSeconds: i,
|
|
19
22
|
httpOnly: !0,
|
|
20
23
|
sameSite: "lax",
|
|
@@ -23,10 +26,13 @@ var l = "voltro:session", u = 604800, d = i, f = a, p = (e) => typeof e == "stri
|
|
|
23
26
|
path: "/"
|
|
24
27
|
})
|
|
25
28
|
};
|
|
26
|
-
},
|
|
27
|
-
let i = n(e,
|
|
28
|
-
return i ?
|
|
29
|
-
},
|
|
29
|
+
}, _ = (e, t) => v(e, t)?.subject ?? null, v = (e, t, r = {}) => {
|
|
30
|
+
let i = n(e, u);
|
|
31
|
+
return i ? l(i, h(t), r) : null;
|
|
32
|
+
}, y = (e, t, n = {}) => {
|
|
33
|
+
let i = r(e, u);
|
|
34
|
+
return i ? l(i, h(t), n) : null;
|
|
35
|
+
}, b = (e = {}) => t(u, "", {
|
|
30
36
|
maxAgeSeconds: 0,
|
|
31
37
|
httpOnly: !0,
|
|
32
38
|
sameSite: "lax",
|
|
@@ -35,4 +41,4 @@ var l = "voltro:session", u = 604800, d = i, f = a, p = (e) => typeof e == "stri
|
|
|
35
41
|
path: "/"
|
|
36
42
|
});
|
|
37
43
|
//#endregion
|
|
38
|
-
export {
|
|
44
|
+
export { u as SESSION_COOKIE_NAME, b as clearSessionCookie, g as issueSession, _ as readSession, y as readSessionFromHeaders, v as readSessionKeyed, f as resolveSessionSecret, p as resolveSessionSecrets, h as sessionSecretsOf };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sessionSecretsOf as e } from "./session.js";
|
|
2
2
|
import { Data as t, Effect as n } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { readSessionValue as r, resolveSessionSecret as i, verifySessionKeyed as a } from "@voltro/protocol/session";
|
|
4
4
|
import { generateId as o } from "@voltro/database";
|
|
5
5
|
//#region src/revocation.ts
|
|
6
6
|
var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
@@ -124,6 +124,11 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
|
124
124
|
}
|
|
125
125
|
return n;
|
|
126
126
|
}),
|
|
127
|
+
revokeAllSessions: (e) => n.sync(() => {
|
|
128
|
+
let t = 0;
|
|
129
|
+
for (let n = a.length - 1; n >= 0; n--) a[n].userId === e && (a.splice(n, 1), t++);
|
|
130
|
+
return t;
|
|
131
|
+
}),
|
|
127
132
|
insertToken: (e) => n.sync(() => {
|
|
128
133
|
let t = {
|
|
129
134
|
...e,
|
|
@@ -288,30 +293,46 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
|
|
|
288
293
|
let t = e.metadata?.memberships;
|
|
289
294
|
return Array.isArray(t) ? t.filter((e) => typeof e == "object" && !!e && typeof e.tenantId == "string" && typeof e.role == "string") : [];
|
|
290
295
|
}, _ = (e) => typeof e.isLive == "function", v = (e) => e.revocation && _(e.revocation) ? e.revocation : e.store ? l(e.store, e.revocation ?? {}) : null, y = (t = {}) => {
|
|
291
|
-
let o = t.cookieName ?? "voltro:session", s = v(t)
|
|
296
|
+
let o = t.cookieName ?? "voltro:session", s = v(t), c = t.revocation && !_(t.revocation) ? t.revocation : {}, u = /* @__PURE__ */ new WeakMap(), d = (e) => {
|
|
297
|
+
if (s) return s;
|
|
298
|
+
let n = t.sessionRegistry;
|
|
299
|
+
if (n === void 0) return null;
|
|
300
|
+
if (typeof n != "function") return l(n, c);
|
|
301
|
+
if (e === void 0) return null;
|
|
302
|
+
let r = u.get(e);
|
|
303
|
+
if (r) return r;
|
|
304
|
+
let i = l(n(e), c);
|
|
305
|
+
return u.set(e, i), i;
|
|
306
|
+
};
|
|
292
307
|
return {
|
|
293
308
|
id: "voltro-password",
|
|
294
|
-
resolve: (
|
|
295
|
-
let
|
|
296
|
-
if (!
|
|
297
|
-
let
|
|
298
|
-
if (!
|
|
309
|
+
resolve: (s) => {
|
|
310
|
+
let c = r(s.headers, o);
|
|
311
|
+
if (!c) return { kind: "skip" };
|
|
312
|
+
let l = e(t.secret ?? i()), u = a(c, l);
|
|
313
|
+
if (!u) return {
|
|
299
314
|
kind: "failed",
|
|
300
315
|
reason: "voltro session cookie failed verification"
|
|
301
316
|
};
|
|
302
|
-
let f = b(
|
|
303
|
-
if (!
|
|
317
|
+
let f = b(u.subject, "voltro-password"), p = h(f);
|
|
318
|
+
if (!p) return t.requireSessionId === !0 ? {
|
|
319
|
+
kind: "failed",
|
|
320
|
+
reason: "voltro session cookie carries no session id"
|
|
321
|
+
} : {
|
|
304
322
|
kind: "matched",
|
|
305
323
|
subject: f
|
|
306
324
|
};
|
|
307
|
-
let
|
|
308
|
-
return
|
|
325
|
+
let m = d(s.store);
|
|
326
|
+
return m ? n.runPromise(m.isLive(p)).then((e) => e ? {
|
|
309
327
|
kind: "matched",
|
|
310
328
|
subject: f
|
|
311
329
|
} : {
|
|
312
330
|
kind: "failed",
|
|
313
331
|
reason: "voltro session revoked"
|
|
314
|
-
}) : {
|
|
332
|
+
}) : t.requireSessionId === !0 && t.sessionRegistry !== void 0 ? {
|
|
333
|
+
kind: "failed",
|
|
334
|
+
reason: "session registry not available yet"
|
|
335
|
+
} : {
|
|
315
336
|
kind: "matched",
|
|
316
337
|
subject: f
|
|
317
338
|
};
|
package/dist/strategy.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthStrategy } from '@voltro/protocol';
|
|
2
|
+
import { DataStore } from '@voltro/database';
|
|
2
3
|
import { Effect } from 'effect';
|
|
3
4
|
import { SessionSecrets } from '@voltro/protocol/session';
|
|
4
5
|
import { VoidIfEmpty } from 'effect/Types';
|
|
@@ -233,6 +234,10 @@ declare interface SessionRecord {
|
|
|
233
234
|
readonly lastSeenAt: Date;
|
|
234
235
|
}
|
|
235
236
|
|
|
237
|
+
/** The session rows an app registers and revokes — the session half of a
|
|
238
|
+
* `UserStore`, usable without the rest of it. */
|
|
239
|
+
declare type SessionRegistry = SessionStore;
|
|
240
|
+
|
|
236
241
|
declare interface SessionRevocationChecker {
|
|
237
242
|
/** Is this session still live? `false` means the row was revoked (or
|
|
238
243
|
* is past its `expiresAt`) — the caller rejects the cookie. Verdicts
|
|
@@ -286,9 +291,13 @@ declare interface SessionStore {
|
|
|
286
291
|
/** Revoke every session for the user EXCEPT `keepSessionId` — the
|
|
287
292
|
* "sign out other devices" action. Returns the count removed. */
|
|
288
293
|
readonly revokeAllOtherSessions: (userId: string, keepSessionId: string) => Effect.Effect<number>;
|
|
294
|
+
/** Revoke every session for the user — "sign out everywhere": a password
|
|
295
|
+
* reset, a deactivation, an account deletion, a security event. Returns
|
|
296
|
+
* the count removed. */
|
|
297
|
+
readonly revokeAllSessions: (userId: string) => Effect.Effect<number>;
|
|
289
298
|
}
|
|
290
299
|
|
|
291
|
-
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify';
|
|
300
|
+
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify' | 'native-code';
|
|
292
301
|
|
|
293
302
|
/** A single-use, hashed, expiring token (magic-link / password-reset). */
|
|
294
303
|
declare interface TokenRecord {
|
|
@@ -421,6 +430,17 @@ export declare interface VoltroPasswordStrategyOptions {
|
|
|
421
430
|
* routes and the rpc auth chain. Ignored without a `store`
|
|
422
431
|
* (unless a checker is passed directly). */
|
|
423
432
|
readonly revocation?: SessionRevocationOptions | SessionRevocationChecker;
|
|
433
|
+
/** Session rows to check cookies against when the app mints its own
|
|
434
|
+
* sessions (`issueRegisteredSession`): a registry, or a factory over the
|
|
435
|
+
* app's store (`dataStoreSessionRegistry`) — built once per store, from
|
|
436
|
+
* the `store` the framework hands every strategy. The checker is tuned by
|
|
437
|
+
* `revocation` (options) as for `store`. */
|
|
438
|
+
readonly sessionRegistry?: SessionRegistry | ((store: DataStore) => SessionRegistry);
|
|
439
|
+
/** Refuse a cookie that carries no `metadata.sessionId` — one minted
|
|
440
|
+
* outside the registry. Without this such a cookie is accepted as before
|
|
441
|
+
* (stateless); with it, nothing but a registered session authenticates,
|
|
442
|
+
* and every session can be revoked. */
|
|
443
|
+
readonly requireSessionId?: boolean;
|
|
424
444
|
}
|
|
425
445
|
|
|
426
446
|
export { }
|
package/dist/strategy.js
CHANGED
package/dist/tokens.d.ts
CHANGED
|
@@ -58,9 +58,13 @@ export declare const mintSecret: (ttlSeconds: number) => Omit<MintedToken, "purp
|
|
|
58
58
|
*/
|
|
59
59
|
export declare const mintToken: (purpose: TokenPurpose, ttlSeconds?: number) => MintedToken;
|
|
60
60
|
|
|
61
|
+
/** Lifetime of a native-login one-time code: the app redeems it within
|
|
62
|
+
* seconds of the browser redirect; five minutes covers a slow hand-off. */
|
|
63
|
+
export declare const NATIVE_CODE_TTL_S: number;
|
|
64
|
+
|
|
61
65
|
/** Default password-reset lifetime — a bit longer. */
|
|
62
66
|
export declare const PASSWORD_RESET_TTL_S: number;
|
|
63
67
|
|
|
64
|
-
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify';
|
|
68
|
+
declare type TokenPurpose = 'magic-link' | 'password-reset' | 'mfa-pending' | 'email-verify' | 'native-code';
|
|
65
69
|
|
|
66
70
|
export { }
|
package/dist/tokens.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { createHash as e, randomBytes as t } from "node:crypto";
|
|
2
2
|
//#region src/tokens.ts
|
|
3
|
-
var n = 900, r = 3600, i = 86400, a = 300, o = {
|
|
3
|
+
var n = 900, r = 3600, i = 86400, a = 300, o = 300, s = {
|
|
4
4
|
"magic-link": 900,
|
|
5
5
|
"password-reset": r,
|
|
6
6
|
"mfa-pending": 300,
|
|
7
|
-
"email-verify": i
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
"email-verify": i,
|
|
8
|
+
"native-code": 300
|
|
9
|
+
}, c = (e) => e.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""), l = (t) => e("sha256").update(t).digest("hex"), u = (e) => {
|
|
10
|
+
let n = c(t(32));
|
|
10
11
|
return {
|
|
11
12
|
token: n,
|
|
12
|
-
tokenHash:
|
|
13
|
+
tokenHash: l(n),
|
|
13
14
|
expiresAt: new Date(Date.now() + e * 1e3)
|
|
14
15
|
};
|
|
15
|
-
},
|
|
16
|
-
...
|
|
16
|
+
}, d = (e, t) => ({
|
|
17
|
+
...u(t ?? s[e]),
|
|
17
18
|
purpose: e
|
|
18
19
|
});
|
|
19
20
|
//#endregion
|
|
20
|
-
export { i as EMAIL_VERIFY_TTL_S, n as MAGIC_LINK_TTL_S, a as MFA_PENDING_TTL_S, r as PASSWORD_RESET_TTL_S,
|
|
21
|
+
export { i as EMAIL_VERIFY_TTL_S, n as MAGIC_LINK_TTL_S, a as MFA_PENDING_TTL_S, o as NATIVE_CODE_TTL_S, r as PASSWORD_RESET_TTL_S, l as hashToken, u as mintSecret, d as mintToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.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",
|
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
"import": "./dist/web.js",
|
|
69
69
|
"default": "./dist/web.js"
|
|
70
70
|
},
|
|
71
|
+
"./native": {
|
|
72
|
+
"types": "./dist/native.d.ts",
|
|
73
|
+
"import": "./dist/native.js",
|
|
74
|
+
"default": "./dist/native.js"
|
|
75
|
+
},
|
|
71
76
|
"./package.json": "./package.json"
|
|
72
77
|
},
|
|
73
78
|
"main": "./dist/index.js",
|
|
@@ -79,8 +84,8 @@
|
|
|
79
84
|
},
|
|
80
85
|
"dependencies": {
|
|
81
86
|
"@effect/sql": "^0.52.1",
|
|
82
|
-
"@voltro/database": "0.
|
|
83
|
-
"@voltro/protocol": "0.
|
|
87
|
+
"@voltro/database": "0.82.0",
|
|
88
|
+
"@voltro/protocol": "0.82.0"
|
|
84
89
|
},
|
|
85
90
|
"peerDependencies": {
|
|
86
91
|
"effect": "^3.22.2",
|