Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
registry-url: https://registry.npmjs.org/

- name: Determine package and version
Expand Down Expand Up @@ -68,5 +68,3 @@ jobs:
- name: Publish to npm
working-directory: packages/${{ steps.meta.outputs.package }}
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
22 changes: 11 additions & 11 deletions packages/core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@seamless-auth/core",
"version": "0.2.1",
"version": "0.3.0",
"description": "Framework-agnostic core authentication logic for SeamlessAuth",
"license": "AGPL-3.0-only",
"author": "Fells Code, LLC",
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/ensureCookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,42 @@ const COOKIE_REQUIREMENTS: Record<
},
"/logout": { name: "accessCookieName", required: true },
"/users/me": { name: "accessCookieName", required: true },
"/internal/metrics/dashboard": { name: "accessCookieName", required: true },
"/internal/auth-events/timeseries": {
name: "accessCookieName",
required: true,
},

"/internal/auth-events/grouped": { name: "accessCookieName", required: true },
"/internal/auth-events/login-stats": {
name: "accessCookieName",
required: true,
},

"/internal/security/anomalies": { name: "accessCookieName", required: true },

"/admin/user": {
name: "accessCookieName",
required: true,
},
"/admin/sessions": {
name: "accessCookieName",
required: true,
},
"/admin/auth-events": {
name: "accessCookieName",
required: true,
},

"/system-config/admin": {
name: "accessCookieName",
required: true,
},

"/system-config/roles": {
name: "accessCookieName",
required: true,
},
};

export async function ensureCookies(
Expand Down
94 changes: 94 additions & 0 deletions packages/core/src/handlers/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { authFetch } from "../authFetch.js";

type BaseOpts = {
authServerUrl: string;
authorization?: string;
};

type WithQuery = BaseOpts & {
query?: Record<string, any>;
};

type WithBody = BaseOpts & {
body?: any;
};

type Result = {
status: number;
body?: any;
error?: string;
};

function buildUrl(base: string, query?: Record<string, any>) {
if (!query) return base;

const qs = new URLSearchParams(
Object.entries(query)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => [k, String(v)]),
).toString();

return qs ? `${base}?${qs}` : base;
}

async function request(
method: "GET" | "POST" | "PATCH" | "DELETE",
path: string,
opts: WithQuery & WithBody,
): Promise<Result> {
const up = await authFetch(
buildUrl(`${opts.authServerUrl}${path}`, opts.query),
{
method,
authorization: opts.authorization,
body: opts.body,
},
);

const data = await up.json();

if (!up.ok) {
return {
status: up.status,
error: data?.error || "admin_request_failed",
};
}

return {
status: up.status,
body: data,
};
}

export const getUsersHandler = (opts: BaseOpts) =>
request("GET", "/admin/users", opts);

export const createUserHandler = (opts: WithBody) =>
request("POST", "/admin/users", opts);

export const deleteUserHandler = (opts: BaseOpts) =>
request("DELETE", "/admin/users", opts);

export const updateUserHandler = (userId: string, opts: WithBody) =>
request("PATCH", `/admin/users/${userId}`, opts);

export const getUserDetailHandler = (userId: string, opts: BaseOpts) =>
request("GET", `/admin/users/${userId}`, opts);

export const getUserAnomaliesHandler = (userId: string, opts: BaseOpts) =>
request("GET", `/admin/users/${userId}/anomalies`, opts);

export const getAuthEventsHandler = (opts: WithQuery) =>
request("GET", "/admin/auth-events", opts);

export const getCredentialCountHandler = (opts: BaseOpts) =>
request("GET", "/admin/credential-count", opts);

export const listAllSessionsHandler = (opts: WithQuery) =>
request("GET", "/admin/sessions", opts);

export const listUserSessionsHandler = (userId: string, opts: BaseOpts) =>
request("GET", `/admin/sessions/${userId}`, opts);

export const revokeAllUserSessionsHandler = (userId: string, opts: BaseOpts) =>
request("DELETE", `/admin/sessions/${userId}/revoke-all`, opts);
46 changes: 46 additions & 0 deletions packages/core/src/handlers/bootstrapAdminInvite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { authFetch } from "../authFetch.js";

export interface BootstrapAdminInviteOptions {
authServerUrl: string;
email: string;
authorization?: string;
}

export interface BootstrapAdminInviteResult {
status: number;
body?: {
url?: string;
expiresAt: string;
token?: string;
};
error?: string;
}

export async function bootstrapAdminInviteHandler(
opts: BootstrapAdminInviteOptions,
): Promise<BootstrapAdminInviteResult> {
const up = await authFetch(
`${opts.authServerUrl}/internal/bootstrap/admin-invite`,
{
method: "POST",
headers: {
authorization: opts.authorization || "",
},
body: { email: opts.email },
},
);

const data = await up.json();

if (!up.ok) {
return {
status: up.status,
error: data?.error?.message || "bootstrap_failed",
};
}

return {
status: up.status,
body: data?.data,
};
}
4 changes: 3 additions & 1 deletion packages/core/src/handlers/finishRegister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { verifySignedAuthResponse } from "../verifySignedAuthResponse.js";

export interface FinishRegisterInput {
authorization?: string;
headers?: Record<string, string>;
body: unknown;
}

Expand Down Expand Up @@ -31,8 +32,9 @@ export async function finishRegisterHandler(
): Promise<FinishRegisterResult> {
const up = await authFetch(`${opts.authServerUrl}/webAuthn/register/finish`, {
method: "POST",
body: input.body,
authorization: input.authorization,
headers: input.headers,
body: input.body,
});

const data = await up.json();
Expand Down
69 changes: 69 additions & 0 deletions packages/core/src/handlers/internalMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { authFetch } from "../authFetch.js";

type BaseOpts = {
authServerUrl: string;
authorization?: string;
};

type WithQuery = BaseOpts & {
query?: Record<string, string | number | boolean | undefined>;
};

type Result = {
status: number;
body?: any;
error?: string;
};

function buildUrl(base: string, query?: WithQuery["query"]) {
if (!query) return base;
const qs = new URLSearchParams(
Object.entries(query)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => [k, String(v)]),
).toString();

return qs ? `${base}?${qs}` : base;
}

async function get(path: string, opts: WithQuery): Promise<Result> {
const up = await authFetch(
buildUrl(`${opts.authServerUrl}${path}`, opts.query),
{
method: "GET",
authorization: opts.authorization,
},
);

const data = await up.json();

if (!up.ok) {
return {
status: up.status,
error: data?.error || "internal_request_failed",
};
}

return {
status: up.status,
body: data,
};
}

export const getAuthEventSummaryHandler = (opts: WithQuery) =>
get("/internal/auth-events/summary", opts);

export const getAuthEventTimeseriesHandler = (opts: WithQuery) =>
get("/internal/auth-events/timeseries", opts);

export const getLoginStatsHandler = (opts: BaseOpts) =>
get("/internal/auth-events/login-stats", opts);

export const getSecurityAnomaliesHandler = (opts: BaseOpts) =>
get("/internal/security/anomalies", opts);

export const getDashboardMetricsHandler = (opts: BaseOpts) =>
get("/internal/metrics/dashboard", opts);

export const getGroupedEventSummaryHandler = (opts: BaseOpts) =>
get("/internal/auth-events/grouped", opts);
Loading
Loading