@stacksjs/auth 0.70.88 → 0.70.91
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/dist/authentication.d.ts +43 -0
- package/dist/authentication.js +413 -0
- package/dist/authenticator.d.ts +17 -0
- package/dist/authenticator.js +14 -0
- package/dist/authorizable.d.ts +77 -0
- package/dist/authorizable.js +28 -0
- package/dist/client.d.ts +22 -0
- package/dist/client.js +26 -0
- package/dist/email-verification.d.ts +29 -0
- package/dist/email-verification.js +111 -0
- package/dist/gate.d.ts +180 -0
- package/dist/gate.js +203 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +26 -0
- package/dist/internal-constants.d.ts +19 -0
- package/dist/internal-constants.js +1 -0
- package/dist/middleware.d.ts +14 -0
- package/dist/middleware.js +28 -0
- package/dist/passkey.d.ts +97 -0
- package/dist/passkey.js +76 -0
- package/dist/password/reset.d.ts +10 -0
- package/dist/password/reset.js +156 -0
- package/dist/policy.d.ts +33 -0
- package/dist/policy.js +91 -0
- package/dist/rate-limiter.d.ts +44 -0
- package/dist/rate-limiter.js +91 -0
- package/dist/rbac-seed.d.ts +24 -0
- package/dist/rbac-seed.js +30 -0
- package/dist/rbac-store-bqb.d.ts +18 -0
- package/dist/rbac-store-bqb.js +180 -0
- package/dist/rbac.d.ts +265 -0
- package/dist/rbac.js +324 -0
- package/dist/register.d.ts +3 -0
- package/dist/register.js +43 -0
- package/dist/session-auth.d.ts +67 -0
- package/dist/session-auth.js +151 -0
- package/dist/team.d.ts +121 -0
- package/dist/team.js +88 -0
- package/dist/token.d.ts +16 -0
- package/dist/token.js +21 -0
- package/dist/tokens.d.ts +284 -0
- package/dist/tokens.js +489 -0
- package/dist/two-factor.d.ts +67 -0
- package/dist/two-factor.js +113 -0
- package/dist/user.d.ts +34 -0
- package/dist/user.js +41 -0
- package/package.json +3 -3
package/dist/team.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resolver — no I/O — that decides which team is "active" for a user
|
|
3
|
+
* given their memberships and an optional switch preference. Reusable across
|
|
4
|
+
* apps: it encodes the whole workspace-switching precedence in one place.
|
|
5
|
+
*
|
|
6
|
+
* - Honors `activeTeamId` only when the user may access it: a member always
|
|
7
|
+
* may; a privileged operator may access any team when `allowAnyTeam` is set
|
|
8
|
+
* (the app decides who is privileged — e.g. a super-admin flag).
|
|
9
|
+
* - Otherwise falls back to the highest-priority membership (owner > admin >
|
|
10
|
+
* anything else), matching what dashboard pages have always shown, so the
|
|
11
|
+
* "current team" never differs between a rendered form and the action it
|
|
12
|
+
* posts to.
|
|
13
|
+
*/
|
|
14
|
+
export declare function selectActiveTeam(opts: {
|
|
15
|
+
memberships: Array<{ team_id: number | string, role?: string | null }>
|
|
16
|
+
activeTeamId?: number | null
|
|
17
|
+
allowAnyTeam?: boolean
|
|
18
|
+
rolePriority?: Record<string, number>
|
|
19
|
+
}): { teamId: number | null, role: string | null };
|
|
20
|
+
/** Read the active-team switch preference from a request's cookie. */
|
|
21
|
+
export declare function getActiveTeamPreference(request: TeamAuthRequest): number | null;
|
|
22
|
+
/**
|
|
23
|
+
* Build the `Set-Cookie` that pins the active team (workspace switcher). A
|
|
24
|
+
* year-long HttpOnly cookie: only the server needs it (SSR reads it, the
|
|
25
|
+
* switch action writes it), and it carries no privilege of its own.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildActiveTeamCookie(teamId: number, opts?: { maxAgeSeconds?: number, secure?: boolean }): string;
|
|
28
|
+
/** Clear the active-team cookie {@link buildActiveTeamCookie} sets. */
|
|
29
|
+
export declare function clearActiveTeamCookie(): string;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve the requesting user's active team membership (team id + role)
|
|
32
|
+
* from their auth credential. Driver-aware: reads config/auth.ts's
|
|
33
|
+
* configured guard driver rather than hardcoding a validation scheme.
|
|
34
|
+
*
|
|
35
|
+
* Dashboard forms are plain HTML POSTs (no client JS, no Authorization
|
|
36
|
+
* header) — the browser only ever sends the auth cookie set on login,
|
|
37
|
+
* so that's checked for the 'token' driver alongside a bearer-header
|
|
38
|
+
* fallback for JS/API callers.
|
|
39
|
+
*
|
|
40
|
+
* Owner membership wins over admin, which wins over any other active
|
|
41
|
+
* membership, when a user belongs to more than one team — the same
|
|
42
|
+
* precedence server-rendered dashboard pages use, so a user never sees
|
|
43
|
+
* a different "current team" between the page that rendered a form and
|
|
44
|
+
* the action that form posts to.
|
|
45
|
+
*
|
|
46
|
+
* Returns `null` when unauthenticated or without an active team
|
|
47
|
+
* membership — callers must treat that as "reject the request," not
|
|
48
|
+
* "fall back to a default team."
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveAuthenticatedMembership(request: TeamAuthRequest): Promise<TeamMembershipResult | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Full team context for a server-rendered dashboard: the authenticated user,
|
|
53
|
+
* the resolved active team (honoring the workspace switcher), and the list of
|
|
54
|
+
* teams the user can switch between. One call replaces the per-page auth+team
|
|
55
|
+
* boilerplate every dashboard view used to copy, and centralizes the
|
|
56
|
+
* switch-aware scoping so pages and the actions they post to never disagree.
|
|
57
|
+
*
|
|
58
|
+
* `opts.allowAnyTeam(user)` lets an app grant an operator (e.g. a super-admin)
|
|
59
|
+
* access to every team — they can switch to, and see, teams they aren't a
|
|
60
|
+
* member of. The full user row is returned so the app can read its own
|
|
61
|
+
* columns (like a super-admin flag) without a second query.
|
|
62
|
+
*/
|
|
63
|
+
export declare function resolveTeamContext(request: TeamAuthRequest, opts?: { allowAnyTeam?: (user: any) => boolean }): Promise<TeamContext>;
|
|
64
|
+
/**
|
|
65
|
+
* Team id only — the common case for actions that just need to scope a
|
|
66
|
+
* write to the requester's team. See {@link resolveAuthenticatedMembership}
|
|
67
|
+
* when the caller also needs the role (e.g. owner/admin-only settings).
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveAuthenticatedTeamId(request: TeamAuthRequest): Promise<number | null>;
|
|
70
|
+
/**
|
|
71
|
+
* The authenticated USER (not team) from a request's real auth
|
|
72
|
+
* credential — bearer header first, then the login cookie, driver-aware
|
|
73
|
+
* like {@link resolveAuthenticatedMembership} (which builds on this).
|
|
74
|
+
* For dashboard form actions that operate on the requester themselves
|
|
75
|
+
* (security settings, profile) rather than on team-scoped rows: plain
|
|
76
|
+
* HTML POSTs carry no Authorization header, so `request.user()` (stamped
|
|
77
|
+
* by the auth middleware from a bearer/session) is undefined there and
|
|
78
|
+
* the login cookie is the only credential available.
|
|
79
|
+
*/
|
|
80
|
+
export declare function resolveAuthenticatedUser(request: TeamAuthRequest): Promise<{ id: number, email?: string } | undefined>;
|
|
81
|
+
/**
|
|
82
|
+
* Name of the cookie that remembers which team the user last switched the
|
|
83
|
+
* dashboard to (a workspace switcher). It is NOT a security credential: the
|
|
84
|
+
* server re-validates membership against `selectActiveTeam` on every request,
|
|
85
|
+
* so a tampered value can only ever resolve to a team the user already
|
|
86
|
+
* belongs to (or, for operators, any team when `allowAnyTeam` is set).
|
|
87
|
+
*/
|
|
88
|
+
export declare const ACTIVE_TEAM_COOKIE: 'active_team';
|
|
89
|
+
/**
|
|
90
|
+
* Team resolution from a request's real auth credential (bearer token or
|
|
91
|
+
* session cookie) — never from a client-supplied form field. Extracted
|
|
92
|
+
* from app-land (stacksjs/status config/auth-team.ts) because every app
|
|
93
|
+
* with team-scoped dashboard forms needs exactly this, and `config/` is
|
|
94
|
+
* for autoloaded config files, not shared helpers.
|
|
95
|
+
*
|
|
96
|
+
* Structural request type on purpose: callers pass whatever request
|
|
97
|
+
* object their action received. Partial objects (tests, non-HTTP
|
|
98
|
+
* callers) resolve to "unauthenticated" rather than crashing.
|
|
99
|
+
*/
|
|
100
|
+
export declare interface TeamAuthRequest {
|
|
101
|
+
bearerToken?: () => string | null | undefined
|
|
102
|
+
cookies?: { get: (name: string) => string | null | undefined }
|
|
103
|
+
}
|
|
104
|
+
export declare interface TeamMembershipResult {
|
|
105
|
+
teamId: number
|
|
106
|
+
role: string
|
|
107
|
+
}
|
|
108
|
+
/** A team the authenticated user may switch the dashboard to. */
|
|
109
|
+
export declare interface SwitchableTeam {
|
|
110
|
+
id: number
|
|
111
|
+
name: string
|
|
112
|
+
role: string
|
|
113
|
+
}
|
|
114
|
+
/** Full dashboard team context — see {@link resolveTeamContext}. */
|
|
115
|
+
export declare interface TeamContext {
|
|
116
|
+
user: any | null
|
|
117
|
+
teamId: number | null
|
|
118
|
+
role: string | null
|
|
119
|
+
teams: SwitchableTeam[]
|
|
120
|
+
activeTeamId: number | null
|
|
121
|
+
}
|
package/dist/team.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { config } from "@stacksjs/config";
|
|
2
|
+
import { db } from "@stacksjs/database";
|
|
3
|
+
import { Auth } from "./authentication";
|
|
4
|
+
import { sessionUser } from "./session-auth";
|
|
5
|
+
export const ACTIVE_TEAM_COOKIE = "active_team";
|
|
6
|
+
export function selectActiveTeam(opts) {
|
|
7
|
+
const priority = opts.rolePriority ?? { owner: 0, admin: 1 }, sorted = [...opts.memberships ?? []].sort((a, b) => (priority[a.role ?? ""] ?? 2) - (priority[b.role ?? ""] ?? 2)), active = opts.activeTeamId == null ? null : Number(opts.activeTeamId);
|
|
8
|
+
if (active != null && Number.isFinite(active)) {
|
|
9
|
+
const member = sorted.find((x) => Number(x.team_id) === active);
|
|
10
|
+
if (member)
|
|
11
|
+
return { teamId: active, role: member.role ?? null };
|
|
12
|
+
if (opts.allowAnyTeam)
|
|
13
|
+
return { teamId: active, role: "admin" };
|
|
14
|
+
}
|
|
15
|
+
const first = sorted[0];
|
|
16
|
+
return first ? { teamId: Number(first.team_id), role: first.role ?? null } : { teamId: null, role: null };
|
|
17
|
+
}
|
|
18
|
+
export function getActiveTeamPreference(request) {
|
|
19
|
+
const raw = request.cookies?.get(ACTIVE_TEAM_COOKIE);
|
|
20
|
+
if (!raw)
|
|
21
|
+
return null;
|
|
22
|
+
const n = Number(raw);
|
|
23
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
24
|
+
}
|
|
25
|
+
export function buildActiveTeamCookie(teamId, opts = {}) {
|
|
26
|
+
const maxAge = Math.max(0, Math.floor(opts.maxAgeSeconds ?? 31536000)), parts = [`${ACTIVE_TEAM_COOKIE}=${encodeURIComponent(String(teamId))}`, "Path=/", "HttpOnly", "SameSite=Lax", `Max-Age=${maxAge}`];
|
|
27
|
+
if (opts.secure)
|
|
28
|
+
parts.push("Secure");
|
|
29
|
+
return parts.join("; ");
|
|
30
|
+
}
|
|
31
|
+
export function clearActiveTeamCookie() {
|
|
32
|
+
return `${ACTIVE_TEAM_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
|
|
33
|
+
}
|
|
34
|
+
export async function resolveAuthenticatedMembership(request) {
|
|
35
|
+
const user = await resolveAuthenticatedUser(request);
|
|
36
|
+
if (!user?.id)
|
|
37
|
+
return null;
|
|
38
|
+
const memberships = await db.selectFrom("team_members").where("user_id", "=", user.id).where("status", "=", "active").select(["team_id", "role"]).execute();
|
|
39
|
+
if (memberships.length === 0)
|
|
40
|
+
return null;
|
|
41
|
+
const picked = selectActiveTeam({
|
|
42
|
+
memberships: memberships.map((m) => ({
|
|
43
|
+
team_id: Number(m.team_id),
|
|
44
|
+
role: m.role == null ? null : String(m.role)
|
|
45
|
+
})),
|
|
46
|
+
activeTeamId: getActiveTeamPreference(request)
|
|
47
|
+
});
|
|
48
|
+
return picked.teamId != null ? { teamId: picked.teamId, role: picked.role ?? "" } : null;
|
|
49
|
+
}
|
|
50
|
+
export async function resolveTeamContext(request, opts = {}) {
|
|
51
|
+
const empty = { user: null, teamId: null, role: null, teams: [], activeTeamId: null }, guardName = config.auth?.default || "api", user = (config.auth?.guards?.[guardName]?.driver || "token") === "session" ? await resolveSessionUser(request) : await resolveTokenUser(request);
|
|
52
|
+
if (!user?.id)
|
|
53
|
+
return empty;
|
|
54
|
+
const memberships = await db.selectFrom("team_members").where("user_id", "=", user.id).where("status", "=", "active").select(["team_id", "role"]).execute(), allowAny = opts.allowAnyTeam ? !!opts.allowAnyTeam(user) : !1, activeTeamId = getActiveTeamPreference(request), picked = selectActiveTeam({
|
|
55
|
+
memberships: memberships.map((m) => ({ team_id: Number(m.team_id), role: String(m.role) })),
|
|
56
|
+
activeTeamId,
|
|
57
|
+
allowAnyTeam: allowAny
|
|
58
|
+
}), roleByTeam = new Map(memberships.map((m) => [Number(m.team_id), String(m.role)]));
|
|
59
|
+
let teamRows = [];
|
|
60
|
+
if (allowAny)
|
|
61
|
+
teamRows = await db.selectFrom("teams").select(["id", "name"]).execute();
|
|
62
|
+
else {
|
|
63
|
+
const ids = [...roleByTeam.keys()];
|
|
64
|
+
teamRows = ids.length ? await db.selectFrom("teams").whereIn("id", ids).select(["id", "name"]).execute() : [];
|
|
65
|
+
}
|
|
66
|
+
const teams = teamRows.map((t) => ({ id: Number(t.id), name: String(t.name), role: roleByTeam.get(Number(t.id)) || "viewer" })).sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
return { user, teamId: picked.teamId, role: picked.role, teams, activeTeamId };
|
|
68
|
+
}
|
|
69
|
+
export async function resolveAuthenticatedTeamId(request) {
|
|
70
|
+
const membership = await resolveAuthenticatedMembership(request);
|
|
71
|
+
return membership ? membership.teamId : null;
|
|
72
|
+
}
|
|
73
|
+
export async function resolveAuthenticatedUser(request) {
|
|
74
|
+
const guardName = config.auth?.default || "api", user = ((config.auth?.guards?.[guardName] || { driver: "token" }).driver || "token") === "session" ? await resolveSessionUser(request) : await resolveTokenUser(request);
|
|
75
|
+
return user?.id ? user : void 0;
|
|
76
|
+
}
|
|
77
|
+
async function resolveSessionUser(request) {
|
|
78
|
+
const sessionId = request.cookies?.get("session_id");
|
|
79
|
+
if (!sessionId)
|
|
80
|
+
return;
|
|
81
|
+
return sessionUser(sessionId);
|
|
82
|
+
}
|
|
83
|
+
async function resolveTokenUser(request) {
|
|
84
|
+
const cookieName = config.auth?.defaultTokenName || "auth-token", bearer = (typeof request.bearerToken === "function" ? request.bearerToken() : void 0) ?? request.cookies?.get(cookieName);
|
|
85
|
+
if (!bearer)
|
|
86
|
+
return;
|
|
87
|
+
return Auth.getUserFromToken(bearer);
|
|
88
|
+
}
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { AuthToken } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* `TokenManager` used to expose `createAccessToken` / `validateToken` /
|
|
4
|
+
* `rotateToken` / `revokeToken` static methods that hardcoded a 30-day
|
|
5
|
+
* `expires_at`, bypassed `config.auth.tokenExpiry`, and compared raw
|
|
6
|
+
* tokens against DB-stored hashes (i.e. broken). They had no callers —
|
|
7
|
+
* the real path lives on `Auth` (see authentication.ts) and respects
|
|
8
|
+
* the configured 1-hour expiry plus the refresh-token rotation
|
|
9
|
+
* landed for #1839. Removed entirely so the trap can't fire.
|
|
10
|
+
*
|
|
11
|
+
* `generateJWT` is the one piece worth keeping — `Auth.createTokenForUser`
|
|
12
|
+
* and `Auth.rotateToken` both use it to mint the JWT body.
|
|
13
|
+
*/
|
|
14
|
+
export declare class TokenManager {
|
|
15
|
+
static generateJWT(userId: number, expiresInSeconds?: number): string;
|
|
16
|
+
}
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
|
|
5
|
+
export class TokenManager {
|
|
6
|
+
static generateJWT(userId, expiresInSeconds = 3600) {
|
|
7
|
+
const appKey = process.env.APP_KEY;
|
|
8
|
+
if (!appKey)
|
|
9
|
+
throw Error("APP_KEY is not set. JWT tokens cannot be generated without a secure application key.");
|
|
10
|
+
const header = {
|
|
11
|
+
alg: "HS256",
|
|
12
|
+
typ: "JWT"
|
|
13
|
+
}, nowSeconds = Math.floor(Date.now() / 1000), payload = {
|
|
14
|
+
sub: userId,
|
|
15
|
+
iat: nowSeconds,
|
|
16
|
+
exp: nowSeconds + expiresInSeconds,
|
|
17
|
+
jti: randomBytes(16).toString("hex")
|
|
18
|
+
}, encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url"), encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url"), signature = createHmac("sha256", appKey).update(`${encodedHeader}.${encodedPayload}`).digest("base64url");
|
|
19
|
+
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import type { AccessToken, CreateClientOptions, CreateClientResult, DatabaseDriver, OAuthClient, PersonalAccessTokenResult, RefreshTokenResult, TokenScopes } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* Read `users.password_changed_at` for a user.
|
|
4
|
+
*
|
|
5
|
+
* Binds a token's validity to the account's credential state: a token
|
|
6
|
+
* issued before the user last changed their password is no longer
|
|
7
|
+
* trusted, regardless of its own `revoked`/`expires_at` flags. This is
|
|
8
|
+
* the durable, use-time backstop behind the post-reset revocation sweep
|
|
9
|
+
* (#1947) — even a freshly minted pair that the sweep never saw is
|
|
10
|
+
* rejected on first use.
|
|
11
|
+
*
|
|
12
|
+
* Returns `null` on ANY error (missing column / missing table) so a
|
|
13
|
+
* not-yet-migrated database degrades to legacy-allow rather than locking
|
|
14
|
+
* everyone out. Accepts an optional query runner so the refresh exchange
|
|
15
|
+
* can read the stamp inside its own transaction.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getPasswordChangedAt(userId: unknown, q?: { unsafe: (sql: string, params?: any[]) => any }): Promise<Date | null>;
|
|
18
|
+
/**
|
|
19
|
+
* True when a credential issued at `createdAt` predates the user's last
|
|
20
|
+
* password change (`changedAt`) and must therefore be rejected.
|
|
21
|
+
*
|
|
22
|
+
* Legacy-allow semantics:
|
|
23
|
+
* - `changedAt` null (no stamp / un-migrated) => never reject.
|
|
24
|
+
* - `createdAt` missing/unparseable => never reject.
|
|
25
|
+
*
|
|
26
|
+
* Strict `<` so a token minted in the SAME second as (or after) the
|
|
27
|
+
* reset — e.g. the victim's immediate post-reset login — is NOT bricked
|
|
28
|
+
* by CURRENT_TIMESTAMP's one-second granularity.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isIssuedBeforePasswordChange(createdAt: unknown, changedAt: Date | null): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Get all access tokens for a user
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* import { tokens } from '@stacksjs/auth'
|
|
36
|
+
* const userTokens = await tokens(user.id)
|
|
37
|
+
*/
|
|
38
|
+
export declare function tokens(userId: number): Promise<AccessToken[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Get a specific token by its plain text value
|
|
41
|
+
* Uses hash comparison for security
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* import { findToken } from '@stacksjs/auth'
|
|
45
|
+
* const token = await findToken('abc123...')
|
|
46
|
+
*/
|
|
47
|
+
export declare function findToken(plainTextToken: string): Promise<AccessToken | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Get the current access token from the request context
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* import { currentAccessToken } from '@stacksjs/auth'
|
|
53
|
+
* const token = await currentAccessToken()
|
|
54
|
+
*/
|
|
55
|
+
export declare function currentAccessToken(): Promise<AccessToken | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Check if the current token has a given scope/ability
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* import { tokenCan } from '@stacksjs/auth'
|
|
61
|
+
* if (await tokenCan('posts:create')) {
|
|
62
|
+
* // user can create posts
|
|
63
|
+
* }
|
|
64
|
+
*/
|
|
65
|
+
export declare function tokenCan(scope: string): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Check if the current token does NOT have a given scope/ability
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* import { tokenCant } from '@stacksjs/auth'
|
|
71
|
+
* if (await tokenCant('admin')) {
|
|
72
|
+
* throw new Error('Admin access required')
|
|
73
|
+
* }
|
|
74
|
+
*/
|
|
75
|
+
export declare function tokenCant(scope: string): Promise<boolean>;
|
|
76
|
+
/**
|
|
77
|
+
* Check if token has ALL of the given scopes
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* import { tokenCanAll } from '@stacksjs/auth'
|
|
81
|
+
* if (await tokenCanAll(['posts:read', 'posts:write'])) {
|
|
82
|
+
* // user has both scopes
|
|
83
|
+
* }
|
|
84
|
+
*/
|
|
85
|
+
export declare function tokenCanAll(scopes: string[]): Promise<boolean>;
|
|
86
|
+
/**
|
|
87
|
+
* Check if token has ANY of the given scopes
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* import { tokenCanAny } from '@stacksjs/auth'
|
|
91
|
+
* if (await tokenCanAny(['admin', 'moderator'])) {
|
|
92
|
+
* // user has at least one of the scopes
|
|
93
|
+
* }
|
|
94
|
+
*/
|
|
95
|
+
export declare function tokenCanAny(scopes: string[]): Promise<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* Get all scopes/abilities for the current token
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* import { tokenAbilities } from '@stacksjs/auth'
|
|
101
|
+
* const abilities = await tokenAbilities()
|
|
102
|
+
* // ['read', 'write', 'posts:create']
|
|
103
|
+
*/
|
|
104
|
+
export declare function tokenAbilities(): Promise<string[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Create a new personal access token for a user
|
|
107
|
+
* Tokens are hashed before storage for security
|
|
108
|
+
*
|
|
109
|
+
* @param userId - The user ID to create the token for
|
|
110
|
+
* @param name - A name/description for the token
|
|
111
|
+
* @param scopes - Array of scopes/abilities for the token
|
|
112
|
+
* @param options - Additional options
|
|
113
|
+
* @param options.expiresInMinutes - Token expiry in minutes (default: 60)
|
|
114
|
+
* @param options.withRefreshToken - Whether to create a refresh token (default: true)
|
|
115
|
+
* @param options.refreshExpiresInDays - Refresh token expiry in days (default: 30)
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* import { createToken } from '@stacksjs/auth'
|
|
119
|
+
* const result = await createToken(user.id, 'My API Token', ['read', 'write'])
|
|
120
|
+
* console.log(result.plainTextToken) // Save this - it won't be shown again!
|
|
121
|
+
* console.log(result.refreshToken) // Use this to get new access tokens
|
|
122
|
+
*/
|
|
123
|
+
export declare function createToken(userId: number, name?: string, scopes?: string[], options?: {
|
|
124
|
+
expiresInMinutes?: number
|
|
125
|
+
withRefreshToken?: boolean
|
|
126
|
+
refreshExpiresInDays?: number
|
|
127
|
+
}): Promise<PersonalAccessTokenResult>;
|
|
128
|
+
/**
|
|
129
|
+
* Exchange a refresh token for a new access token
|
|
130
|
+
*
|
|
131
|
+
* @param refreshTokenPlain - The plain text refresh token
|
|
132
|
+
* @param options - Additional options
|
|
133
|
+
* @param options.expiresInMinutes - New access token expiry in minutes (default: 60)
|
|
134
|
+
* @param options.refreshExpiresInDays - New refresh token expiry in days (default: 30)
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* import { refreshToken } from '@stacksjs/auth'
|
|
138
|
+
* const result = await refreshToken('your-refresh-token')
|
|
139
|
+
* // Use result.plainTextToken as new access token
|
|
140
|
+
* // Use result.refreshToken as new refresh token (old one is revoked)
|
|
141
|
+
*/
|
|
142
|
+
export declare function refreshToken(refreshTokenPlain: string, options?: {
|
|
143
|
+
expiresInMinutes?: number
|
|
144
|
+
refreshExpiresInDays?: number
|
|
145
|
+
}): Promise<RefreshTokenResult>;
|
|
146
|
+
/**
|
|
147
|
+
* Validate a refresh token without exchanging it
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* import { validateRefreshToken } from '@stacksjs/auth'
|
|
151
|
+
* const isValid = await validateRefreshToken('your-refresh-token')
|
|
152
|
+
*/
|
|
153
|
+
export declare function validateRefreshToken(refreshTokenPlain: string): Promise<boolean>;
|
|
154
|
+
/**
|
|
155
|
+
* Revoke a specific refresh token
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* import { revokeRefreshToken } from '@stacksjs/auth'
|
|
159
|
+
* await revokeRefreshToken('your-refresh-token')
|
|
160
|
+
*/
|
|
161
|
+
export declare function revokeRefreshToken(refreshTokenPlain: string): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Revoke all refresh tokens for a user
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* import { revokeAllRefreshTokens } from '@stacksjs/auth'
|
|
167
|
+
* await revokeAllRefreshTokens(user.id)
|
|
168
|
+
*/
|
|
169
|
+
export declare function revokeAllRefreshTokens(userId: number): Promise<void>;
|
|
170
|
+
/**
|
|
171
|
+
* Delete expired refresh tokens (cleanup)
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* import { deleteExpiredRefreshTokens } from '@stacksjs/auth'
|
|
175
|
+
* const count = await deleteExpiredRefreshTokens()
|
|
176
|
+
*/
|
|
177
|
+
export declare function deleteExpiredRefreshTokens(): Promise<number>;
|
|
178
|
+
/**
|
|
179
|
+
* Delete revoked refresh tokens older than specified days
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* import { deleteRevokedRefreshTokens } from '@stacksjs/auth'
|
|
183
|
+
* const count = await deleteRevokedRefreshTokens(7)
|
|
184
|
+
*/
|
|
185
|
+
export declare function deleteRevokedRefreshTokens(daysOld?: number): Promise<number>;
|
|
186
|
+
/**
|
|
187
|
+
* Revoke a specific access token
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* import { revokeToken } from '@stacksjs/auth'
|
|
191
|
+
* await revokeToken('abc123...')
|
|
192
|
+
*/
|
|
193
|
+
export declare function revokeToken(plainTextToken: string): Promise<void>;
|
|
194
|
+
/**
|
|
195
|
+
* Revoke a token by its ID
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* import { revokeTokenById } from '@stacksjs/auth'
|
|
199
|
+
* await revokeTokenById(123)
|
|
200
|
+
*/
|
|
201
|
+
export declare function revokeTokenById(tokenId: number): Promise<void>;
|
|
202
|
+
/**
|
|
203
|
+
* Revoke all tokens for a user
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* import { revokeAllTokens } from '@stacksjs/auth'
|
|
207
|
+
* await revokeAllTokens(user.id)
|
|
208
|
+
*/
|
|
209
|
+
export declare function revokeAllTokens(userId: number): Promise<void>;
|
|
210
|
+
/**
|
|
211
|
+
* Revoke all tokens except the current one
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* import { revokeOtherTokens } from '@stacksjs/auth'
|
|
215
|
+
* await revokeOtherTokens(user.id)
|
|
216
|
+
*/
|
|
217
|
+
export declare function revokeOtherTokens(userId: number): Promise<void>;
|
|
218
|
+
/**
|
|
219
|
+
* Delete expired tokens (cleanup)
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* import { deleteExpiredTokens } from '@stacksjs/auth'
|
|
223
|
+
* const count = await deleteExpiredTokens()
|
|
224
|
+
*/
|
|
225
|
+
export declare function deleteExpiredTokens(): Promise<number>;
|
|
226
|
+
/**
|
|
227
|
+
* Delete revoked tokens older than specified days
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* import { deleteRevokedTokens } from '@stacksjs/auth'
|
|
231
|
+
* const count = await deleteRevokedTokens(30) // older than 30 days
|
|
232
|
+
*/
|
|
233
|
+
export declare function deleteRevokedTokens(daysOld?: number): Promise<number>;
|
|
234
|
+
/**
|
|
235
|
+
* Get all OAuth clients for a user
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* import { clients } from '@stacksjs/auth'
|
|
239
|
+
* const userClients = await clients(user.id)
|
|
240
|
+
*/
|
|
241
|
+
export declare function clients(userId: number): Promise<OAuthClient[]>;
|
|
242
|
+
/**
|
|
243
|
+
* Get a specific OAuth client by ID
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* import { findClient } from '@stacksjs/auth'
|
|
247
|
+
* const client = await findClient(1)
|
|
248
|
+
*/
|
|
249
|
+
export declare function findClient(clientId: number): Promise<OAuthClient | null>;
|
|
250
|
+
/**
|
|
251
|
+
* Create a new OAuth client
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* import { createClient } from '@stacksjs/auth'
|
|
255
|
+
* const client = await createClient({
|
|
256
|
+
* name: 'My App',
|
|
257
|
+
* redirect: 'https://myapp.com/callback'
|
|
258
|
+
* })
|
|
259
|
+
*/
|
|
260
|
+
export declare function createClient(options: CreateClientOptions): Promise<CreateClientResult>;
|
|
261
|
+
/**
|
|
262
|
+
* Revoke an OAuth client
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* import { revokeClient } from '@stacksjs/auth'
|
|
266
|
+
* await revokeClient(1)
|
|
267
|
+
*/
|
|
268
|
+
export declare function revokeClient(clientId: number): Promise<void>;
|
|
269
|
+
// ============================================================================
|
|
270
|
+
// HELPER FUNCTIONS
|
|
271
|
+
// ============================================================================
|
|
272
|
+
export declare function parseScopes(scopes: string | string[] | null | undefined): TokenScopes;
|
|
273
|
+
/** Current database driver */
|
|
274
|
+
declare const dbDriver: DatabaseDriver;
|
|
275
|
+
/** Cross-database SQL helpers */
|
|
276
|
+
declare const sql: unknown;
|
|
277
|
+
/**
|
|
278
|
+
* Alias for currentAccessToken
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* import { token } from '@stacksjs/auth'
|
|
282
|
+
* const t = await token()
|
|
283
|
+
*/
|
|
284
|
+
export declare const token: unknown;
|