Skip to content

Commit 21a494a

Browse files
committed
feat: Add admin, internal, bootstrap workflows. Update CI to new non longlived token validation.
1 parent 1596a80 commit 21a494a

19 files changed

Lines changed: 982 additions & 28 deletions

.github/workflows/release.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222

2323
- uses: actions/setup-node@v4
2424
with:
25-
node-version: 20
25+
node-version: 24
2626
registry-url: https://registry.npmjs.org/
2727

2828
- name: Determine package and version
@@ -68,5 +68,3 @@ jobs:
6868
- name: Publish to npm
6969
working-directory: packages/${{ steps.meta.outputs.package }}
7070
run: npm publish --access public
71-
env:
72-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

packages/core/package-lock.json

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@seamless-auth/core",
3-
"version": "0.2.1",
3+
"version": "0.3.0",
44
"description": "Framework-agnostic core authentication logic for SeamlessAuth",
55
"license": "AGPL-3.0-only",
66
"author": "Fells Code, LLC",

packages/core/src/ensureCookies.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,42 @@ const COOKIE_REQUIREMENTS: Record<
8080
},
8181
"/logout": { name: "accessCookieName", required: true },
8282
"/users/me": { name: "accessCookieName", required: true },
83+
"/internal/metrics/dashboard": { name: "accessCookieName", required: true },
84+
"/internal/auth-events/timeseries": {
85+
name: "accessCookieName",
86+
required: true,
87+
},
88+
89+
"/internal/auth-events/grouped": { name: "accessCookieName", required: true },
90+
"/internal/auth-events/login-stats": {
91+
name: "accessCookieName",
92+
required: true,
93+
},
94+
95+
"/internal/security/anomalies": { name: "accessCookieName", required: true },
96+
97+
"/admin/user": {
98+
name: "accessCookieName",
99+
required: true,
100+
},
101+
"/admin/sessions": {
102+
name: "accessCookieName",
103+
required: true,
104+
},
105+
"/admin/auth-events": {
106+
name: "accessCookieName",
107+
required: true,
108+
},
109+
110+
"/system-config/admin": {
111+
name: "accessCookieName",
112+
required: true,
113+
},
114+
115+
"/system-config/roles": {
116+
name: "accessCookieName",
117+
required: true,
118+
},
83119
};
84120

85121
export async function ensureCookies(
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { authFetch } from "../authFetch.js";
2+
3+
type BaseOpts = {
4+
authServerUrl: string;
5+
authorization?: string;
6+
};
7+
8+
type WithQuery = BaseOpts & {
9+
query?: Record<string, any>;
10+
};
11+
12+
type WithBody = BaseOpts & {
13+
body?: any;
14+
};
15+
16+
type Result = {
17+
status: number;
18+
body?: any;
19+
error?: string;
20+
};
21+
22+
function buildUrl(base: string, query?: Record<string, any>) {
23+
if (!query) return base;
24+
25+
const qs = new URLSearchParams(
26+
Object.entries(query)
27+
.filter(([, v]) => v !== undefined && v !== null)
28+
.map(([k, v]) => [k, String(v)]),
29+
).toString();
30+
31+
return qs ? `${base}?${qs}` : base;
32+
}
33+
34+
async function request(
35+
method: "GET" | "POST" | "PATCH" | "DELETE",
36+
path: string,
37+
opts: WithQuery & WithBody,
38+
): Promise<Result> {
39+
const up = await authFetch(
40+
buildUrl(`${opts.authServerUrl}${path}`, opts.query),
41+
{
42+
method,
43+
authorization: opts.authorization,
44+
body: opts.body,
45+
},
46+
);
47+
48+
const data = await up.json();
49+
50+
if (!up.ok) {
51+
return {
52+
status: up.status,
53+
error: data?.error || "admin_request_failed",
54+
};
55+
}
56+
57+
return {
58+
status: up.status,
59+
body: data,
60+
};
61+
}
62+
63+
export const getUsersHandler = (opts: BaseOpts) =>
64+
request("GET", "/admin/users", opts);
65+
66+
export const createUserHandler = (opts: WithBody) =>
67+
request("POST", "/admin/users", opts);
68+
69+
export const deleteUserHandler = (opts: BaseOpts) =>
70+
request("DELETE", "/admin/users", opts);
71+
72+
export const updateUserHandler = (userId: string, opts: WithBody) =>
73+
request("PATCH", `/admin/users/${userId}`, opts);
74+
75+
export const getUserDetailHandler = (userId: string, opts: BaseOpts) =>
76+
request("GET", `/admin/users/${userId}`, opts);
77+
78+
export const getUserAnomaliesHandler = (userId: string, opts: BaseOpts) =>
79+
request("GET", `/admin/users/${userId}/anomalies`, opts);
80+
81+
export const getAuthEventsHandler = (opts: WithQuery) =>
82+
request("GET", "/admin/auth-events", opts);
83+
84+
export const getCredentialCountHandler = (opts: BaseOpts) =>
85+
request("GET", "/admin/credential-count", opts);
86+
87+
export const listAllSessionsHandler = (opts: WithQuery) =>
88+
request("GET", "/admin/sessions", opts);
89+
90+
export const listUserSessionsHandler = (userId: string, opts: BaseOpts) =>
91+
request("GET", `/admin/sessions/${userId}`, opts);
92+
93+
export const revokeAllUserSessionsHandler = (userId: string, opts: BaseOpts) =>
94+
request("DELETE", `/admin/sessions/${userId}/revoke-all`, opts);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { authFetch } from "../authFetch.js";
2+
3+
export interface BootstrapAdminInviteOptions {
4+
authServerUrl: string;
5+
email: string;
6+
authorization?: string;
7+
}
8+
9+
export interface BootstrapAdminInviteResult {
10+
status: number;
11+
body?: {
12+
url?: string;
13+
expiresAt: string;
14+
token?: string;
15+
};
16+
error?: string;
17+
}
18+
19+
export async function bootstrapAdminInviteHandler(
20+
opts: BootstrapAdminInviteOptions,
21+
): Promise<BootstrapAdminInviteResult> {
22+
const up = await authFetch(
23+
`${opts.authServerUrl}/internal/bootstrap/admin-invite`,
24+
{
25+
method: "POST",
26+
headers: {
27+
authorization: opts.authorization || "",
28+
},
29+
body: { email: opts.email },
30+
},
31+
);
32+
33+
const data = await up.json();
34+
35+
if (!up.ok) {
36+
return {
37+
status: up.status,
38+
error: data?.error?.message || "bootstrap_failed",
39+
};
40+
}
41+
42+
return {
43+
status: up.status,
44+
body: data?.data,
45+
};
46+
}

packages/core/src/handlers/finishRegister.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { verifySignedAuthResponse } from "../verifySignedAuthResponse.js";
44

55
export interface FinishRegisterInput {
66
authorization?: string;
7+
headers?: Record<string, string>;
78
body: unknown;
89
}
910

@@ -31,8 +32,9 @@ export async function finishRegisterHandler(
3132
): Promise<FinishRegisterResult> {
3233
const up = await authFetch(`${opts.authServerUrl}/webAuthn/register/finish`, {
3334
method: "POST",
34-
body: input.body,
3535
authorization: input.authorization,
36+
headers: input.headers,
37+
body: input.body,
3638
});
3739

3840
const data = await up.json();
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { authFetch } from "../authFetch.js";
2+
3+
type BaseOpts = {
4+
authServerUrl: string;
5+
authorization?: string;
6+
};
7+
8+
type WithQuery = BaseOpts & {
9+
query?: Record<string, string | number | boolean | undefined>;
10+
};
11+
12+
type Result = {
13+
status: number;
14+
body?: any;
15+
error?: string;
16+
};
17+
18+
function buildUrl(base: string, query?: WithQuery["query"]) {
19+
if (!query) return base;
20+
const qs = new URLSearchParams(
21+
Object.entries(query)
22+
.filter(([, v]) => v !== undefined && v !== null)
23+
.map(([k, v]) => [k, String(v)]),
24+
).toString();
25+
26+
return qs ? `${base}?${qs}` : base;
27+
}
28+
29+
async function get(path: string, opts: WithQuery): Promise<Result> {
30+
const up = await authFetch(
31+
buildUrl(`${opts.authServerUrl}${path}`, opts.query),
32+
{
33+
method: "GET",
34+
authorization: opts.authorization,
35+
},
36+
);
37+
38+
const data = await up.json();
39+
40+
if (!up.ok) {
41+
return {
42+
status: up.status,
43+
error: data?.error || "internal_request_failed",
44+
};
45+
}
46+
47+
return {
48+
status: up.status,
49+
body: data,
50+
};
51+
}
52+
53+
export const getAuthEventSummaryHandler = (opts: WithQuery) =>
54+
get("/internal/auth-events/summary", opts);
55+
56+
export const getAuthEventTimeseriesHandler = (opts: WithQuery) =>
57+
get("/internal/auth-events/timeseries", opts);
58+
59+
export const getLoginStatsHandler = (opts: BaseOpts) =>
60+
get("/internal/auth-events/login-stats", opts);
61+
62+
export const getSecurityAnomaliesHandler = (opts: BaseOpts) =>
63+
get("/internal/security/anomalies", opts);
64+
65+
export const getDashboardMetricsHandler = (opts: BaseOpts) =>
66+
get("/internal/metrics/dashboard", opts);
67+
68+
export const getGroupedEventSummaryHandler = (opts: BaseOpts) =>
69+
get("/internal/auth-events/grouped", opts);

0 commit comments

Comments
 (0)