Compare commits

..

10 Commits

Author SHA1 Message Date
jkunz 2d1e6ea6e1 v1.17.0
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-20 08:12:07 +00:00
jkunz 98e614a945 feat(auth): harden authentication with argon2 passwords and rotating hashed refresh tokens 2026-04-20 08:12:07 +00:00
jkunz ad3e51a9e8 v1.16.0
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-29 15:06:40 +00:00
jkunz d8f72d620a feat(dev): add local development docs, update tswatch preset and add Playwright screenshots 2026-01-29 15:06:40 +00:00
jkunz 53b36e506c v1.15.0
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-01-29 14:24:08 +00:00
jkunz 7d5ad29a27 feat(build): add tsbundle/tswatch configs, update build/watch scripts, bump dependencies, and add CLI documentation 2026-01-29 14:24:08 +00:00
jkunz 724ec2d134 v1.14.1
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2025-12-22 15:56:20 +00:00
jkunz 32ffc1bbaa fix(oidc): migrate OIDC endpoints and internal handlers to use typedserver IRequestContext and update dependencies 2025-12-22 15:56:20 +00:00
jkunz a91dd9dda6 v1.14.0
Docker (tags) / security (push) Failing after 0s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2025-12-16 12:46:42 +00:00
jkunz 5462257398 feat(docs): add package READMEs and publish metadata; update web package publish order 2025-12-16 12:46:42 +00:00
49 changed files with 5970 additions and 2574 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

+43
View File
@@ -1,5 +1,48 @@
# Changelog
## 2026-04-20 - 1.17.0 - feat(auth)
harden authentication with argon2 passwords and rotating hashed refresh tokens
- replace SHA-256 password hashing with argon2 while preserving verification and upgrade support for legacy hashes
- rotate refresh tokens on JWT refresh, detect token reuse, and invalidate compromised sessions
- store refresh and transfer tokens as hashes with one-time transfer token validation and expiry
- persist refresh tokens separately on the client so sessions can recover and refresh without embedding tokens in JWTs
- add authentication tests covering password verification, legacy hash migration, refresh token rotation, reuse detection, and one-time transfer tokens
## 2026-01-29 - 1.16.0 - feat(dev)
add local development docs, update tswatch preset and add Playwright screenshots
- readme.md: added a Local Development section with prerequisites, quick-start commands, environment variables, development routes, and default development credentials + security note
- npmextra.json: changed @git.zone/tswatch preset from "website" to "service" and disabled the built-in server (removed port/serveDir/liveReload and set server.enabled false); removed triggerReload from website watcher
- .playwright-mcp: added Playwright screenshots (login-page.png, register-page.png, account-dashboard.png) for visual tests / CI
## 2026-01-29 - 1.15.0 - feat(build)
add tsbundle/tswatch configs, update build/watch scripts, bump dependencies, and add CLI documentation
- Add tsbundle and tswatch configuration to npmextra.json to support bundling and a local dev server (dist_serve, liveReload, watch patterns).
- Update package.json build/watch scripts to use generic tsbundle/tswatch invocations (removed explicit 'website' target).
- Bump dependencies and devDependencies: @git.zone/tsbuild ^4.0.2 -> ^4.1.2, @git.zone/tsbundle ^2.6.3 -> ^2.8.3, @git.zone/tswatch ^2.3.13 -> ^3.0.1, @api.global/typedserver ^8.1.0 -> ^8.3.0, several @design.estate packages, @push.rocks/taskbuffer ^3.5.0 -> ^4.1.1, @types/node 25.0.3 -> 25.1.0, and other minor/patch bumps.
- Add a new CLI README (ts_idpcli/readme.md) with usage, commands, programmatic API examples and configuration.
- Update README license/Legal sections in ts_idpclient, ts_interfaces and ts_web to include license, trademark, and company information.
## 2025-12-22 - 1.14.1 - fix(oidc)
migrate OIDC endpoints and internal handlers to use typedserver IRequestContext and update dependencies
- Updated route handlers in ts/index.ts to pass ctx (IRequestContext) instead of req
- Refactored OIDC manager handlers to accept plugins.typedserver.IRequestContext and use ctx.url, ctx.headers, ctx.formData (handleAuthorize, handleToken, handleUserInfo, handleRevoke)
- Bumped dependencies to support the new typedserver API: @api.global/typedserver -> ^8.1.0
- Other dependency updates: @design.estate/dees-catalog ^3.4.0, @git.zone/tspublish ^1.11.0, @types/node ^25.0.3
- Changing public handler method signatures is a breaking API change; recommend a major version bump
## 2025-12-16 - 1.14.0 - feat(docs)
add package READMEs and publish metadata; update web package publish order
- Add comprehensive README for ts_web (web components/UI)
- Add README for ts_idpclient (TypeScript client)
- Add README for ts_interfaces (type definitions/interfaces)
- Add tspublish.json for ts_idpcli (@idp.global/cli) and ts_idpclient (@idp.global/client)
- Update ts_web/tspublish.json order from 4 to 5
## 2025-12-15 - 1.13.0 - feat(oidc)
feat(oidc): add OIDC provider (OidcManager, endpoints, and interfaces)
+35
View File
@@ -50,5 +50,40 @@
"registries": ["https://verdaccio.lossless.digital"],
"accessLevel": "public"
}
},
"@git.zone/tsbundle": {
"bundles": [
{
"from": "./ts_web/index.ts",
"to": "./dist_serve/bundle.js",
"outputMode": "bundle",
"bundler": "esbuild",
"production": true
}
]
},
"@git.zone/tswatch": {
"preset": "service",
"server": {
"enabled": false
},
"watchers": [
{
"name": "backend",
"watch": "./ts/**/*",
"command": "npm run startTs",
"restart": true,
"debounce": 300,
"runOnStart": true
}
],
"bundles": [
{
"name": "website",
"from": "./ts_web/index.ts",
"to": "./dist_serve/bundle.js",
"watchPatterns": ["./ts_web/**/*"]
}
]
}
}
+29 -27
View File
@@ -1,14 +1,14 @@
{
"name": "@idp.global/idp.global",
"version": "1.13.0",
"version": "1.17.0",
"description": "An identity provider software managing user authentications, registrations, and sessions.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
"scripts": {
"test": "npm run build",
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle website --production",
"watch": "tswatch website",
"test": "pnpm run build && tstest test/",
"build": "tsbuild tsfolders --web --allowimplicitany && tsbundle",
"watch": "tswatch",
"start": "(node cli.js)",
"startTs": "(node cli.ts.js)",
"buildDocs": "tsdoc"
@@ -16,49 +16,51 @@
"author": "Task Venture Capital GmbH",
"license": "MIT",
"dependencies": {
"@api.global/typedrequest": "^3.2.5",
"@api.global/typedrequest": "^3.3.0",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^7.11.1",
"@api.global/typedsocket": "^4.1.0",
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@consent.software/catalog": "^2.0.1",
"@design.estate/dees-catalog": "^3.3.1",
"@design.estate/dees-domtools": "^2.3.6",
"@design.estate/dees-element": "^2.1.3",
"@git.zone/tspublish": "^1.10.3",
"@push.rocks/lik": "^6.2.2",
"@design.estate/dees-catalog": "^3.81.0",
"@design.estate/dees-domtools": "^2.5.4",
"@design.estate/dees-element": "^2.2.4",
"@git.zone/tspublish": "^1.11.5",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartcli": "^4.0.19",
"@push.rocks/smartdata": "^7.0.15",
"@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartdata": "^7.1.7",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^13.1.0",
"@push.rocks/smarthash": "^3.2.6",
"@push.rocks/smartinteract": "^2.0.6",
"@push.rocks/smartjson": "^6.0.0",
"@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartlog": "^3.2.2",
"@push.rocks/smartmail": "^2.2.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.0.27",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/smartstate": "^2.3.0",
"@push.rocks/smarttime": "^4.2.3",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smarturl": "^3.1.0",
"@push.rocks/taskbuffer": "^3.5.0",
"@push.rocks/taskbuffer": "^8.0.2",
"@push.rocks/webjwt": "^1.0.9",
"@push.rocks/websetup": "^3.0.15",
"@push.rocks/webstore": "^2.0.20",
"@push.rocks/webstore": "^2.0.21",
"@serve.zone/platformclient": "^1.1.2",
"@tsclass/tsclass": "^9.3.0",
"@uptime.link/webwidget": "^1.2.6"
"@tsclass/tsclass": "^9.5.0",
"@uptime.link/webwidget": "^1.2.6",
"argon2": "^0.44.0"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.0.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tswatch": "^2.3.13",
"@push.rocks/projectinfo": "^5.0.1",
"@types/node": "^24.10.1"
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.10.0",
"@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^3.6.3",
"@git.zone/tswatch": "^3.3.2",
"@push.rocks/projectinfo": "^5.1.0",
"@types/node": "^25.6.0"
},
"private": true,
"repository": {
+3797 -2071
View File
File diff suppressed because it is too large Load Diff
+331 -253
View File
@@ -1,312 +1,390 @@
# @idp.global/idp.global
An identity provider software managing user authentications, registrations, and sessions.
🔐 **A modern, open-source Identity Provider (IdP) SaaS platform** for managing user authentication, registrations, sessions, and organization-based access control.
## Install
Built with TypeScript and designed for modern web applications, idp.global provides a complete identity management solution that you can self-host or use as a service.
To install `@idp.global/idp.global`, you can run the following command in your terminal:
## Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## ✨ Features
### 🔑 Authentication & Authorization
- **Multiple Login Methods**: Email/password, email magic links, API tokens
- **JWT-Based Sessions**: Secure token management with automatic refresh
- **Two-Factor Authentication**: Enhanced security with 2FA support
- **Password Reset**: Secure password recovery flow
- **Device Management**: Track and manage authenticated devices
### 🏢 Organization Management
- **Multi-Tenant Architecture**: Support multiple organizations per user
- **Role-Based Access Control (RBAC)**: Fine-grained permissions system
- **Organization Roles**: Admin, member, and custom role support
- **Member Invitations**: Bulk invite and manage team members
- **Ownership Transfer**: Seamlessly transfer organization ownership
### 🔗 Third-Party Integration
- **OpenID Connect (OIDC) Provider**: Full OIDC compliance for third-party apps
- Discovery endpoint (`/.well-known/openid-configuration`)
- JWKS endpoint for token verification
- Authorization code flow with PKCE
- Token refresh and revocation
- **OAuth 2.0**: Standard OAuth flows for app authorization
- **Supported Scopes**: `openid`, `profile`, `email`, `organizations`, `roles`
### 💳 Billing Integration
- **Paddle Integration**: Built-in payment processing support
- **Billing Plans**: Flexible subscription management
- **Checkout Flows**: Streamlined payment experiences
### 🎨 Modern Web UI
- **Responsive Design**: Beautiful UI components built with `@design.estate/dees-catalog`
- **Account Management**: User profile, settings, and preferences
- **Organization Dashboard**: Manage members, roles, and apps
- **Admin Panel**: Global administration interface
### 📡 Real-Time Communication
- **WebSocket Support**: Real-time updates via TypedSocket
- **Typed API Requests**: Type-safe client-server communication
- **Public Key Distribution**: Automatic JWT key rotation notifications
## 🏗️ Architecture
idp.global is built as a modular TypeScript monorepo:
```
├── ts/ # Server-side code (Node.js)
│ └── reception/ # Core identity management logic
├── ts_interfaces/ # Shared TypeScript interfaces (published as @idp.global/interfaces)
├── ts_idpclient/ # Browser/Node client library (published as @idp.global/idpclient)
├── ts_idpcli/ # Command-line interface tool
└── ts_web/ # Web frontend (published as @idp.global/web)
```
### Core Managers
| Manager | Responsibility |
|---------|----------------|
| `JwtManager` | JWT generation, validation, and key management |
| `LoginSessionManager` | Session creation and authentication |
| `UserManager` | User CRUD and profile management |
| `OrganizationManager` | Organization lifecycle management |
| `RoleManager` | RBAC and permission management |
| `OidcManager` | OpenID Connect provider functionality |
| `AppManager` | OAuth client app registration |
| `BillingPlanManager` | Subscription and payment handling |
## 🚀 Quick Start
### 🐳 Docker Deployment (Recommended)
The easiest way to run idp.global is using Docker:
```bash
npm install @idp.global/idp.global
# Pull the latest image
docker pull code.foss.global/idp.global/idp.global
# Run with environment variables
docker run -d \
-p 2999:2999 \
-e MONGODB_URL=mongodb://your-mongo:27017/idp \
-e IDP_BASEURL=https://your-domain.com \
-e INSTANCE_NAME=idp.global \
code.foss.global/idp.global/idp.global
```
This will download and install the necessary dependencies along with the module to your project.
### Environment Variables
## Usage
| Variable | Description | Required |
|----------|-------------|----------|
| `MONGODB_URL` | MongoDB connection string | ✅ Yes |
| `IDP_BASEURL` | Public URL of your idp.global instance | ✅ Yes |
| `INSTANCE_NAME` | Name for this IDP instance | No (default: `idp.global`) |
| `SERVEZONE_PLATFROM_AUTHORIZATION` | ServeZone platform auth token | No |
To use `@idp.global/idp.global`, one needs to understand its key components and functionalities. Below, we'll guide you through setting up, logging in, registering, and managing users and organizations within an IDP (Identity Provider) environment using this package.
### Docker Compose Example
### Setting Up the Environment
```yaml
version: '3.8'
services:
idp:
image: code.foss.global/idp.global/idp.global
ports:
- "2999:2999"
environment:
MONGODB_URL: mongodb://mongo:27017/idp
IDP_BASEURL: https://idp.yourdomain.com
INSTANCE_NAME: my-idp
depends_on:
- mongo
First, let's set up the environment:
mongo:
image: mongo:7
volumes:
- mongo-data:/data/db
```typescript
// Import the necessary modules
import * as serviceworker from '@api.global/typedserver/web_serviceworker_client';
import * as domtools from '@design.estate/dees-domtools';
import { html, render } from '@design.estate/dees-element';
import { IdpWelcome } from './elements/idp-welcome.js';
// Define an asynchronous run function
const run = async () => {
// Set up DOM tools
const domtoolsInstance = await domtools.DomTools.setupDomTools();
domtools.elementBasic.setup();
// Configure website information
domtoolsInstance.setWebsiteInfo({
metaObject: {
title: 'idp.global',
description: 'the code that runs idp.global',
canonicalDomain: 'https://idp.global',
ldCompany: {
name: 'Task Venture Capital GmbH',
status: 'active',
contact: {
address: {
name: 'Task Venture Capital GmbH',
city: 'Grasberg',
country: 'Germany',
houseNumber: '24',
postalCode: '28879',
streetName: 'Eickedorfer Vorweide',
},
}
},
},
});
// Set up the service worker
const serviceWorker = await serviceworker.getServiceworkerClient();
// Render the main template
const mainTemplate = html`
<style>
body {
margin: 0px;
--background-accent: #303f9f;
}
</style>
<idp-welcome></idp-welcome>
`;
render(mainTemplate, document.body);
};
// Run the function
run();
volumes:
mongo-data:
```
### Using the IDP Client
The server listens on port 2999 by default.
The IDP Client is essential to communicate with the IDP server. Below is a sample of how to set up and use the IDP client:
## 🛠️ Local Development
### Prerequisites
- Node.js 20+
- pnpm
- MongoDB (local or remote)
- SMTP server (for email verification in registration flow)
### Getting Started
```bash
# Clone the repository
git clone https://code.foss.global/idp.global/idp.global.git
cd idp.global
# Install dependencies
pnpm install
# Build the project
pnpm build
# Start development server with hot reload
pnpm watch
```
The server runs on **http://localhost:2999** with:
- 🔄 Auto-restart backend on changes (`ts/`)
- 📦 Automatic frontend bundle rebuilding (`ts_web/`)
### Environment Setup
Create environment variables for the backend:
```bash
export MONGODB_URL=mongodb://localhost:27017/idp-dev
export IDP_BASEURL=http://localhost:2999
export INSTANCE_NAME=idp-dev
```
### Development Routes
| Route | Description |
|-------|-------------|
| `/` | Welcome/landing page |
| `/login` | Sign in form |
| `/register` | New user registration |
| `/account` | User dashboard (requires auth) |
### 🔑 Default Development Credentials
For local development with the test database, use:
| Field | Value |
|-------|-------|
| **Email/Username** | `admin@idp.global` or `admin` |
| **Password** | `admin` |
This account has `isGlobalAdmin: true` for full platform access including the admin panel at `/account/admin`.
> ⚠️ **Security Note**: These credentials are for local development only. Never use default credentials in production environments.
## 📦 Published Packages
This monorepo publishes the following npm packages:
| Package | Description |
|---------|-------------|
| `@idp.global/interfaces` | TypeScript interfaces for API contracts |
| `@idp.global/idpclient` | Client library for browser and Node.js |
| `@idp.global/web` | Web UI components |
## 💻 Client Usage
### Browser Client
```typescript
import { IdpState } from './idp.state.js';
import * as plugins from './plugins.js';
import { IdpClient } from '@idp.global/idpclient';
// Instantiate IdpState which provides a singleton instance
export class IdpDemo {
private idpState = IdpState.getSingletonInstance();
// Initialize the client
const idpClient = new IdpClient('https://idp.global');
// Function to initialize and use IdpClient
public async demo() {
// Fetch the client instance
const { idpClient } = this.idpState;
// Handler for login
const handleLogin = async () => {
const response = await idpClient.requests.loginWithUserNameAndPassword.fire({
// Enable WebSocket connection
await idpClient.enableTypedSocket();
// Check login status
const isLoggedIn = await idpClient.determineLoginStatus();
// Login with email and password
const response = await idpClient.requests.loginWithUserNameAndPassword.fire({
username: 'user@example.com',
password: 'password123',
});
if (response.refreshToken) {
await idpClient.storeJwt(response.jwt);
console.log("Logged in successfully, JWT stored.");
} else {
console.log("Login failed.");
}
};
// Execute login handler
await handleLogin();
}
password: 'securepassword'
});
if (response.refreshToken) {
await idpClient.refreshJwt(response.refreshToken);
console.log('✅ Login successful!');
}
// Instantiate and run demo
const demo = new IdpDemo();
demo.demo();
// Get current user info
const userInfo = await idpClient.whoIs();
console.log('User:', userInfo.user);
// Get user's organizations
const orgs = await idpClient.getRolesAndOrganizations();
console.log('Organizations:', orgs.organizations);
```
### Managing User Authentication
Several functionalities are available for managing user authentication. These include registering, logging in, and refreshing JWTs.
#### Registration Process
The registration process is typically more involved and requires steps such as email validation, setting user-specific data, and verifying OTPs for additional security.
### Organization Management
```typescript
import * as plugins from './plugins.js';
import { IdpState } from './idp.state.js';
// Create a new organization
const result = await idpClient.createOrganization('My Company', 'my-company', 'manifest');
console.log('Created:', result.resultingOrganization);
// Registration stepper element
export class IdpRegistrationStepper extends plugins.DeesElement {
private idpState = IdpState.getSingletonInstance();
// Invite members
await idpClient.requests.createInvitation.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
email: 'newmember@example.com',
roles: ['member']
});
```
public async firstUpdated() {
await this.domtoolsPromise;
this.domtools.router.on(`/finishregistration`, async (routeArg) => {
const validationToken = routeArg.queryParams.validationtoken;
if (!validationToken) {
this.renderErrorMessage("Validation token not found.");
return;
}
const emailResponse = await this.validateEmail(validationToken);
if (!emailResponse.email) {
this.renderErrorMessage("Invalid validation token.");
return;
}
await this.renderRegistrationForm(emailResponse.email);
});
}
### CLI Tool
private async validateEmail(token: string) {
return await this.idpState.idpClient.requests.afterRegistrationEmailClicked.fire({
token
});
}
The `ts_idpcli` module provides a command-line interface:
private async renderRegistrationForm(email: string) {
const template = plugins.html`
<dees-form @formData="${async (event) => await this.handleFormSubmission(event, email)}">
<dees-input-text key="First Name" label="First Name" required></dees-input-text>
<dees-input-text key="Last Name" label="Last Name" required></dees-input-text>
<dees-form-submit>Next</dees-form-submit>
</dees-form>
`;
this.render(template, this.shadowRoot);
}
```bash
# Login
idp login
private async handleFormSubmission(event: FormDataEvent, email: string) {
const formData = (event.target as any).getFormData();
await this.idpState.idpClient.requests.setData.fire({
token: this.storedData.validationTokenUrlParam,
userData: {
email,
first_name: formData.FirstName,
last_name: formData.LastName,
},
});
// Proceed to the next steps as per the registration flow
}
# Show current user
idp whoami
private renderErrorMessage(message: string) {
const template = plugins.html`<div>Error: ${message}</div>`;
this.render(template, this.shadowRoot);
}
# List organizations
idp orgs
# List organization members
idp members --org <org-id>
# Invite a user
idp invite --org <org-id> --email user@example.com
```
## 🔐 OIDC Integration
idp.global implements a full OpenID Connect provider. Third-party applications can use it for SSO:
### Discovery Document
```
GET /.well-known/openid-configuration
```
### Authorization Flow
```
GET /oauth/authorize?
client_id=your-client-id&
redirect_uri=https://yourapp.com/callback&
response_type=code&
scope=openid profile email organizations&
state=random-state&
code_challenge=PKCE_CHALLENGE&
code_challenge_method=S256
```
### Token Exchange
```
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=your-client-id&
client_secret=your-client-secret&
code_verifier=PKCE_VERIFIER
```
### UserInfo
```
GET /oauth/userinfo
Authorization: Bearer ACCESS_TOKEN
```
Response:
```json
{
"sub": "user-id",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true,
"organizations": [
{ "id": "org-1", "name": "Acme Corp", "slug": "acme", "roles": ["admin"] }
],
"roles": ["user"]
}
```
### User Management
## 🛠️ Tech Stack
Managing user data including roles, organizations, and billing plans is essential in any identity provider software.
- **Runtime**: Node.js with ES Modules
- **Language**: TypeScript (strict mode)
- **Database**: MongoDB via `@push.rocks/smartdata`
- **Web Server**: `@api.global/typedserver`
- **Real-time**: `@api.global/typedsocket` (WebSocket)
- **JWT**: `@push.rocks/smartjwt` (RS256 signing)
- **Frontend**: `@design.estate/dees-element` (Web Components)
- **Build**: `@git.zone/tsbuild` + `@git.zone/tsbundle`
#### Getting User Data
## 📚 API Reference
```typescript
import * as plugins from './plugins.js';
### Request Interfaces
const fetchUserData = async (jwt: string) => {
const user = await plugins.typedrequest.TypedRequest<plugins.lointReception.request.IReq_GetUserData>(
`/getUserData`, 'POST').fire({jwt});
console.log(user);
};
All API requests are type-safe. See `ts_interfaces/request/` for the complete API:
fetchUserData('<JWT_TOKEN_HERE>');
```
- **Authentication**: `IReq_LoginWithEmail`, `IReq_LoginWithApiToken`, `IReq_RefreshJwt`
- **Registration**: `IReq_FirstRegistration`, `IReq_FinishRegistration`
- **User Management**: `IReq_GetUserData`, `IReq_SetUserData`, `IReq_GetUserSessions`
- **Organizations**: `IReq_CreateOrganization`, `IReq_GetOrgMembers`, `IReq_CreateInvitation`
- **Apps & OAuth**: `IReq_GetGlobalApps`, `IReq_CreateGlobalApp`
- **Billing**: `IReq_GetBillingPlan`, `IReq_UpdatePaymentMethod`
#### Creating an Organization
### Data Models
```typescript
import { IdpState } from './idp.state.js';
See `ts_interfaces/data/` for all data structures:
export class OrganizationManager {
private idpState = IdpState.getSingletonInstance();
public async createOrganization(name: string, slug: string, jwt: string) {
const response = await this.idpState.idpClient.requests.createOrganization.fire({
jwt: jwt,
organizationName: name,
organizationSlug: slug,
action: 'manifest',
});
if (response.resultingOrganization) {
console.log(`Organization ${name} created successfully.`);
} else {
console.log(`Organization creation failed.`);
}
}
}
// Usage
const organizationManager = new OrganizationManager();
organizationManager.createOrganization('Dev Org', 'dev-org', '<JWT_TOKEN_HERE>');
```
### Managing JWTs
The `@idp.global/idp.global` package involves managing JSON Web Tokens (JWTs) for session handling and security.
#### Refreshing JWTs
```typescript
import { IdpClient } from './idp.client.js';
export const refreshJwt = async (client: IdpClient) => {
const currentJwt = await client.getJwt();
if (!currentJwt) return null;
const response = await client.requests.refreshJwt.fire({
refreshToken: currentJwt.data.refreshToken
});
if (response.jwt) {
await client.storeJwt(response.jwt);
console.log("JWT refreshed and stored.");
return response.jwt;
} else {
console.log("JWT refresh failed.");
return null;
}
};
// Usage
const idpClient = new IdpClient('https://reception.lossless.one/typedrequest');
refreshJwt(idpClient);
```
### Handling Authentication Tokens
Handling tokens (JWTs, refresh tokens, transfer tokens) securely is crucial for maintaining session integrity.
#### Exchanging Refresh Token for Transfer Token
```typescript
import { IdpClient } from './idp.client.js';
const getTransferToken = async (client: IdpClient) => {
const refreshToken = await client.getJwt().data.refreshToken;
const response = await client.requests.obtainOneTimeToken.fire({
refreshToken
});
if(response.transferToken) {
console.log("Obtained Transfer Token: ", response.transferToken);
return response.transferToken;
} else {
console.log("Failed to obtain Transfer Token.");
return null;
}
};
// Usage
const idpClient = new IdpClient('https://reception.lossless.one/typedrequest');
getTransferToken(idpClient);
```
This comprehensive guide should help you understand the detailed setup and usage of the `@idp.global/idp.global` module effectively.
- `IUser` - User profile and credentials
- `IOrganization` - Organization entity
- `IRole` - User roles within organizations
- `IJwt` - JWT token structure
- `IApp` - OAuth application definitions
- `IOidcAccessToken`, `IAuthorizationCode` - OIDC tokens
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
+61
View File
@@ -0,0 +1,61 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { LoginSession } from '../ts/reception/classes.loginsession.js';
import { User } from '../ts/reception/classes.user.js';
import * as plugins from '../ts/plugins.js';
const createTestLoginSession = () => {
const loginSession = new LoginSession();
loginSession.id = 'test-session';
loginSession.data.userId = 'test-user';
(loginSession as LoginSession & { save: () => Promise<void> }).save = async () => undefined;
return loginSession;
};
tap.test('hashes passwords with argon2 and verifies them', async () => {
const passwordHash = await User.hashPassword('correct horse battery staple');
expect(passwordHash.startsWith('$argon2')).toBeTrue();
expect(await User.verifyPassword('correct horse battery staple', passwordHash)).toBeTrue();
expect(await User.verifyPassword('wrong password', passwordHash)).toBeFalse();
expect(User.shouldUpgradePasswordHash(passwordHash)).toBeFalse();
});
tap.test('accepts legacy sha256 hashes and marks them for upgrade', async () => {
const legacyHash = await plugins.smarthash.sha256FromString('legacy-password');
expect(User.isLegacyPasswordHash(legacyHash)).toBeTrue();
expect(await User.verifyPassword('legacy-password', legacyHash)).toBeTrue();
expect(await User.verifyPassword('different-password', legacyHash)).toBeFalse();
expect(User.shouldUpgradePasswordHash(legacyHash)).toBeTrue();
});
tap.test('rotates refresh tokens and detects reuse', async () => {
const loginSession = createTestLoginSession();
const firstRefreshToken = await loginSession.getRefreshToken();
const secondRefreshToken = await loginSession.getRefreshToken();
expect(firstRefreshToken.startsWith('refresh_')).toBeTrue();
expect(secondRefreshToken.startsWith('refresh_')).toBeTrue();
expect(firstRefreshToken).not.toEqual(secondRefreshToken);
expect(loginSession.data.refreshToken).toBeNullOrUndefined();
expect(loginSession.data.refreshTokenHash).toBeTruthy();
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('current');
expect(await loginSession.validateRefreshToken(firstRefreshToken)).toEqual('reused');
await loginSession.invalidate();
expect(await loginSession.validateRefreshToken(secondRefreshToken)).toEqual('invalidated');
});
tap.test('persists transfer tokens as one-time hashes', async () => {
const loginSession = createTestLoginSession();
const transferToken = await loginSession.getTransferToken();
expect(transferToken.startsWith('transfer_')).toBeTrue();
expect(loginSession.data.transferTokenHash).toBeTruthy();
expect(await loginSession.validateTransferToken(transferToken)).toBeTrue();
expect(await loginSession.validateTransferToken(transferToken)).toBeFalse();
});
export default tap.start();
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@idp.global/idp.global',
version: '1.13.0',
version: '1.17.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.'
}
+12 -12
View File
@@ -28,40 +28,40 @@ export const runCli = async () => {
typedserver.options.spaFallback = true;
// OIDC Discovery endpoint
typedserver.addRoute('/.well-known/openid-configuration', 'GET', async (req) => {
typedserver.addRoute('/.well-known/openid-configuration', 'GET', async (ctx) => {
return new Response(JSON.stringify(reception.oidcManager.getDiscoveryDocument()), {
headers: { 'Content-Type': 'application/json' },
});
});
// JWKS endpoint
typedserver.addRoute('/.well-known/jwks.json', 'GET', async (req) => {
typedserver.addRoute('/.well-known/jwks.json', 'GET', async (ctx) => {
return new Response(JSON.stringify(reception.oidcManager.getJwks()), {
headers: { 'Content-Type': 'application/json' },
});
});
// OAuth Authorization endpoint
typedserver.addRoute('/oauth/authorize', 'GET', async (req) => {
return reception.oidcManager.handleAuthorize(req);
typedserver.addRoute('/oauth/authorize', 'GET', async (ctx) => {
return reception.oidcManager.handleAuthorize(ctx);
});
// OAuth Token endpoint
typedserver.addRoute('/oauth/token', 'POST', async (req) => {
return reception.oidcManager.handleToken(req);
typedserver.addRoute('/oauth/token', 'POST', async (ctx) => {
return reception.oidcManager.handleToken(ctx);
});
// OAuth UserInfo endpoint (GET and POST)
typedserver.addRoute('/oauth/userinfo', 'GET', async (req) => {
return reception.oidcManager.handleUserInfo(req);
typedserver.addRoute('/oauth/userinfo', 'GET', async (ctx) => {
return reception.oidcManager.handleUserInfo(ctx);
});
typedserver.addRoute('/oauth/userinfo', 'POST', async (req) => {
return reception.oidcManager.handleUserInfo(req);
typedserver.addRoute('/oauth/userinfo', 'POST', async (ctx) => {
return reception.oidcManager.handleUserInfo(ctx);
});
// OAuth Revocation endpoint
typedserver.addRoute('/oauth/revoke', 'POST', async (req) => {
return reception.oidcManager.handleRevoke(req);
typedserver.addRoute('/oauth/revoke', 'POST', async (ctx) => {
return reception.oidcManager.handleRevoke(ctx);
});
},
});
+4 -1
View File
@@ -1,6 +1,7 @@
// Native scope
import * as crypto from 'node:crypto';
import * as path from 'path';
export { path };
export { crypto, path };
// Project scope
import * as idpInterfaces from '../dist_ts_interfaces/index.js';
@@ -32,8 +33,10 @@ import * as smartpromise from '@push.rocks/smartpromise';
import * as smarttime from '@push.rocks/smarttime';
import * as smartunique from '@push.rocks/smartunique';
import * as taskbuffer from '@push.rocks/taskbuffer';
import * as argon2 from 'argon2';
export {
argon2,
lik,
projectinfo,
qenv,
+36 -14
View File
@@ -1,5 +1,6 @@
import * as plugins from '../plugins.js';
import { JwtManager } from './classes.jwtmanager.js';
import type { LoginSession } from './classes.loginsession.js';
/**
* a User is identified by its username or email.
@@ -11,21 +12,27 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
public static async createJwtForRefreshToken(
jwtManagerInstance: JwtManager,
refreshTokenArg: string
) {
const loginSession =
await jwtManagerInstance.receptionRef.loginSessionManager.CLoginSession.getLoginSessionByRefreshToken(
): Promise<string | null> {
const sessionLookup =
await jwtManagerInstance.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
refreshTokenArg
);
if (!loginSession) {
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
return null;
}
const refreshTokenValid = await loginSession.validateRefreshToken(refreshTokenArg);
if (!refreshTokenValid) {
return null;
return this.createJwtForLoginSession(jwtManagerInstance, sessionLookup.loginSession);
}
public static async createJwtForLoginSession(
jwtManagerInstance: JwtManager,
loginSession: LoginSession
): Promise<string | null> {
const user = await jwtManagerInstance.receptionRef.userManager.CUser.getInstance({
id: loginSession.data.userId,
});
if (!user) {
return null;
}
const validUntil = plugins.smarttime.ExtendedDate.fromMillis(
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 1 })
);
@@ -33,10 +40,10 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
jwt.id = plugins.smartunique.shortId();
jwt.data = {
userId: user.id,
sessionId: loginSession.id,
validUntil: validUntil.getTime(),
refreshEvery: 1000000,
refreshFrom: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ days: 0.5 }),
refreshToken: await loginSession.getRefreshToken(), // TODO: handle multiple refresh tokens
justForLooks: {
validUntilIsoString: validUntil.toISOString(),
}
@@ -46,7 +53,7 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
const jwtString = await jwtManagerInstance.smartjwtInstance.createJWT({
id: jwt.id,
blocked: null,
blocked: false,
data: jwt.data,
} as plugins.idpInterfaces.data.IJwt);
return jwtString;
@@ -68,11 +75,26 @@ export class Jwt extends plugins.smartdata.SmartDataDbDoc<Jwt, plugins.idpInterf
}
public async getLoginSession() {
const loginSession = await this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
data: {
refreshToken: this.data.refreshToken,
}
if (this.data.sessionId) {
return this.manager.receptionRef.loginSessionManager.CLoginSession.getInstance({
id: this.data.sessionId,
});
return loginSession;
}
if (!this.data.refreshToken) {
return null;
}
const sessionLookup =
await this.manager.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
this.data.refreshToken
);
if (!sessionLookup) {
return null;
}
return sessionLookup.loginSession;
}
}
+39 -3
View File
@@ -25,10 +25,41 @@ export class JwtManager {
new plugins.typedrequest.TypedHandler(
'refreshJwt',
async (requestArg) => {
const resultJwt = await Jwt.createJwtForRefreshToken(this, requestArg.refreshToken);
const sessionLookup =
await this.receptionRef.loginSessionManager.findLoginSessionByRefreshToken(
requestArg.refreshToken
);
if (!sessionLookup || sessionLookup.validationStatus === 'invalid') {
return {
status: 'not found',
};
}
if (sessionLookup.validationStatus === 'invalidated') {
return {
status: 'invalidated',
};
}
if (sessionLookup.validationStatus === 'reused') {
await sessionLookup.loginSession.invalidate();
return {
status: 'invalidated',
};
}
const rotatedRefreshToken = await sessionLookup.loginSession.getRefreshToken();
const resultJwt = await Jwt.createJwtForLoginSession(this, sessionLookup.loginSession);
if (!rotatedRefreshToken || !resultJwt) {
return {
status: 'invalidated',
};
}
return {
status: 'loggedIn',
jwt: resultJwt,
refreshToken: rotatedRefreshToken,
};
}
)
@@ -120,19 +151,24 @@ export class JwtManager {
await this.pushPublicKeyToClients();
}
public async verifyJWTAndGetData(jwtArg: string): Promise<Jwt> {
public async verifyJWTAndGetData(jwtArg: string): Promise<Jwt | null> {
const jwtData: plugins.idpInterfaces.data.IJwt = await this.smartjwtInstance.verifyJWTAndGetData(jwtArg);
const jwt = await this.CJwt.getInstance({
id: jwtData.id,
});
if (!jwt) {
return null;
}
if (jwt.blocked) {
return null;
}
if (jwt) {
const loginSession = await jwt.getLoginSession();
if (!loginSession) {
if (!loginSession || loginSession.data.invalidated) {
await jwt.block();
if (!this.blockedJwtIdList.includes(jwt.id)) {
this.blockedJwtIdList.push(jwt.id);
}
return null;
}
}
+91 -11
View File
@@ -2,6 +2,8 @@ import * as plugins from '../plugins.js';
import { LoginSessionManager } from './classes.loginsessionmanager.js';
import { User } from './classes.user.js';
export type TRefreshTokenValidationResult = 'current' | 'invalid' | 'invalidated' | 'reused';
/**
* a LoginSession keeps track of a login over the whole time of the user being loggedin
*/
@@ -40,7 +42,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
}
public static async getLoginSessionByRefreshToken(refreshTokenArg: string) {
const loginSession = await LoginSession.getInstance({
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
let loginSession = await LoginSession.getInstance({
'data.refreshTokenHash': refreshTokenHash,
});
if (loginSession) {
return loginSession;
}
loginSession = await LoginSession.getInstance({
data: {
refreshToken: refreshTokenArg,
},
@@ -48,6 +57,14 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
return loginSession;
}
public static async hashSessionToken(tokenArg: string) {
return plugins.smarthash.sha256FromString(tokenArg);
}
public static createOpaqueToken(prefixArg: string) {
return `${prefixArg}${plugins.crypto.randomBytes(32).toString('base64url')}`;
}
// ========
// INSTANCE
// ========
@@ -60,13 +77,17 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
validUntil: Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ weeks: 1 }),
invalidated: false,
refreshToken: null,
refreshTokenHash: null,
rotatedRefreshTokenHashes: [],
transferTokenHash: null,
transferTokenExpiresAt: null,
deviceId: null,
deviceInfo: null,
createdAt: Date.now(),
lastActive: Date.now(),
};
public transferToken: string;
public transferToken: string | null = null;
constructor() {
super();
@@ -77,40 +98,99 @@ export class LoginSession extends plugins.smartdata.SmartDataDbDoc<
*/
public async invalidate() {
this.data.invalidated = true;
this.data.refreshToken = null;
this.data.refreshTokenHash = null;
this.data.transferTokenHash = null;
this.data.transferTokenExpiresAt = null;
await this.save();
}
/**
* a refresh token is unique to a login session and ONLY created once per login session
* a refresh token is unique to a login session and rotated whenever it is issued
* @returns
*/
public async getRefreshToken() {
if (this.data.invalidated) {
console.log('login session is invalidated. no refresh token can be generated.');
return null;
}
if (!this.data.refreshToken) {
this.data.refreshToken = plugins.smartunique.uni('refresh_');
const previousRefreshTokenHash =
this.data.refreshTokenHash ||
(this.data.refreshToken
? await LoginSession.hashSessionToken(this.data.refreshToken)
: null);
if (previousRefreshTokenHash) {
this.data.rotatedRefreshTokenHashes = [
...(this.data.rotatedRefreshTokenHashes || []),
previousRefreshTokenHash,
].slice(-5);
}
const refreshToken = LoginSession.createOpaqueToken('refresh_');
this.data.refreshTokenHash = await LoginSession.hashSessionToken(refreshToken);
this.data.refreshToken = null;
this.data.lastActive = Date.now();
await this.save();
return this.data.refreshToken;
return refreshToken;
}
public async getTransferToken() {
this.transferToken = plugins.smartunique.uni('transfer_');
this.transferToken = LoginSession.createOpaqueToken('transfer_');
this.data.transferTokenHash = await LoginSession.hashSessionToken(this.transferToken);
this.data.transferTokenExpiresAt =
Date.now() + plugins.smarttime.getMilliSecondsFromUnits({ minutes: 5 });
await this.save();
return this.transferToken;
}
public async validateRefreshToken(refreshTokenArg: string) {
return this.data.refreshToken === refreshTokenArg;
public async validateRefreshToken(
refreshTokenArg: string
): Promise<TRefreshTokenValidationResult> {
if (this.data.invalidated) {
return 'invalidated';
}
const refreshTokenHash = await LoginSession.hashSessionToken(refreshTokenArg);
if (
this.data.refreshTokenHash === refreshTokenHash ||
(!!this.data.refreshToken && this.data.refreshToken === refreshTokenArg)
) {
return 'current';
}
if ((this.data.rotatedRefreshTokenHashes || []).includes(refreshTokenHash)) {
return 'reused';
}
return 'invalid';
}
public async validateTransferToken(transferTokenArg: string) {
const result = this.transferToken === transferTokenArg;
if (this.data.invalidated || !this.data.transferTokenHash) {
return false;
}
if (
this.data.transferTokenExpiresAt &&
this.data.transferTokenExpiresAt < Date.now()
) {
this.data.transferTokenHash = null;
this.data.transferTokenExpiresAt = null;
await this.save();
return false;
}
const result =
this.data.transferTokenHash ===
(await LoginSession.hashSessionToken(transferTokenArg));
// a transfer token can only be used once, so we invalidate it here
if (result) {
this.transferToken = null;
this.data.transferTokenHash = null;
this.data.transferTokenExpiresAt = null;
await this.save();
}
return result;
}
+99 -44
View File
@@ -1,5 +1,5 @@
import * as plugins from '../plugins.js';
import { LoginSession } from './classes.loginsession.js';
import { LoginSession, type TRefreshTokenValidationResult } from './classes.loginsession.js';
import { Reception } from './classes.reception.js';
import { logger } from './logging.js';
@@ -32,9 +32,6 @@ export class LoginSessionManager {
let user = await this.receptionRef.userManager.CUser.getInstance({
data: {
username: requestData.username,
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
requestData.password
),
},
});
@@ -42,33 +39,30 @@ export class LoginSessionManager {
user = await this.receptionRef.userManager.CUser.getInstance({
data: {
email: requestData.username,
passwordHash: await this.receptionRef.userManager.CUser.hashPassword(
requestData.password
),
},
});
}
if (user) {
// lets recheck
if (
(user.data.username !== requestData.username &&
user.data.email !== requestData.username) ||
user.data.passwordHash !==
(await this.receptionRef.userManager.CUser.hashPassword(requestData.password))
) {
throw new Error(
'database returned a user that does not match wanted criterea. CRITICAL!'
if (user && (await this.receptionRef.userManager.CUser.verifyPassword(
requestData.password,
user.data.passwordHash
))) {
if (this.receptionRef.userManager.CUser.shouldUpgradePasswordHash(user.data.passwordHash)) {
user.data.passwordHash = await this.receptionRef.userManager.CUser.hashPassword(
requestData.password
);
await user.save();
}
const loginSession = await LoginSession.createLoginSessionForUser(user);
this.loginSessions.add(loginSession);
const refreshToken = await loginSession.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
return {
status: 'ok',
refreshToken: refreshToken,
refreshToken,
twoFaNeeded: false,
};
} else {
@@ -109,12 +103,14 @@ export class LoginSessionManager {
} else {
logger.log('info', `loginWithEmail did not find user: ${requestDataArg.email}`);
}
const testOnlyToken =
process.env.TEST_MODE && existingUser
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
?.token
: undefined;
return {
status: 'ok',
testOnlyToken: process.env.TEST_MODE
? this.emailTokenMap.findSync((itemArg) => itemArg.email === existingUser.data.email)
.token
: null,
testOnlyToken,
};
}
)
@@ -133,10 +129,17 @@ export class LoginSessionManager {
email: requestArg.email,
},
});
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
const loginSession = await LoginSession.createLoginSessionForUser(user);
this.loginSessions.add(loginSession);
const refreshToken = await loginSession.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
return {
refreshToken: await loginSession.getRefreshToken(),
refreshToken,
};
} else {
throw new plugins.typedrequest.TypedResponseError('Validation Token not found');
@@ -147,8 +150,11 @@ export class LoginSessionManager {
this.typedRouter.addTypedHandler<plugins.idpInterfaces.request.ILogoutRequest>(
new plugins.typedrequest.TypedHandler('logout', async (requestDataArg) => {
const loginSession = await this.CLoginSession.getLoginSessionByRefreshToken(requestDataArg.refreshToken);
await loginSession.invalidate();
const sessionLookup = await this.findLoginSessionByRefreshToken(requestDataArg.refreshToken);
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
throw new plugins.typedrequest.TypedResponseError('Invalid refresh token');
}
await sessionLookup.loginSession.invalidate();
return {}
})
);
@@ -158,31 +164,39 @@ export class LoginSessionManager {
'exchangeRefreshTokenAndTransferToken',
async (requestDataArg) => {
switch (true) {
case !!requestDataArg.refreshToken:
const loginSession = await this.loginSessions.find(async (loginSessionArg) => {
return loginSessionArg.validateRefreshToken(requestDataArg.refreshToken);
});
if (!loginSession) {
case !!requestDataArg.refreshToken: {
const sessionLookup = await this.findLoginSessionByRefreshToken(
requestDataArg.refreshToken
);
if (!sessionLookup || sessionLookup.validationStatus !== 'current') {
if (sessionLookup?.validationStatus === 'reused') {
await sessionLookup.loginSession.invalidate();
}
throw new plugins.typedrequest.TypedResponseError('your refresh token is invalid');
}
return {
transferToken: await loginSession.getTransferToken(),
transferToken: await sessionLookup.loginSession.getTransferToken(),
};
break;
case !!requestDataArg.transferToken:
let transferToken: string;
const loginSession2 = await this.loginSessions.find(async (loginSessionArg) => {
return loginSessionArg.validateTransferToken(requestDataArg.transferToken);
});
}
case !!requestDataArg.transferToken: {
const loginSession2 = await this.findLoginSessionByTransferToken(
requestDataArg.transferToken
);
if (!loginSession2) {
throw new plugins.typedrequest.TypedResponseError(
'Your transfer token is not valid.'
);
}
const refreshToken = await loginSession2.getRefreshToken();
if (!refreshToken) {
throw new plugins.typedrequest.TypedResponseError('Could not create login session');
}
return {
refreshToken: await loginSession2.getRefreshToken(),
refreshToken,
};
break;
}
default:
throw new plugins.typedrequest.TypedResponseError('Invalid token exchange request');
}
}
)
@@ -271,8 +285,7 @@ export class LoginSessionManager {
throw new plugins.typedrequest.TypedResponseError('Invalid JWT');
}
// Get the current session's refresh token to identify the current session
const currentRefreshToken = jwt.data.refreshToken;
const currentLoginSession = await jwt.getLoginSession();
// Get all sessions for this user
const sessions = await this.CLoginSession.getInstances({
@@ -290,7 +303,7 @@ export class LoginSessionManager {
ip: session.data.deviceInfo?.ip || 'Unknown',
lastActive: session.data.lastActive || session.data.createdAt || Date.now(),
createdAt: session.data.createdAt || Date.now(),
isCurrent: session.data.refreshToken === currentRefreshToken,
isCurrent: session.id === currentLoginSession?.id,
})),
};
}
@@ -317,8 +330,10 @@ export class LoginSessionManager {
throw new plugins.typedrequest.TypedResponseError('Session not found');
}
const currentLoginSession = await jwt.getLoginSession();
// Don't allow revoking the current session via this method
if (sessionToRevoke.data.refreshToken === jwt.data.refreshToken) {
if (sessionToRevoke.id === currentLoginSession?.id) {
throw new plugins.typedrequest.TypedResponseError(
'Cannot revoke current session. Use logout instead.'
);
@@ -338,4 +353,44 @@ export class LoginSessionManager {
)
);
}
public async findLoginSessionByRefreshToken(refreshTokenArg: string): Promise<{
loginSession: LoginSession;
validationStatus: TRefreshTokenValidationResult;
} | null> {
const directMatch = await this.CLoginSession.getLoginSessionByRefreshToken(refreshTokenArg);
if (directMatch) {
return {
loginSession: directMatch,
validationStatus: await directMatch.validateRefreshToken(refreshTokenArg),
};
}
const loginSessions = await this.CLoginSession.getInstances({});
for (const loginSession of loginSessions) {
const validationStatus = await loginSession.validateRefreshToken(refreshTokenArg);
if (validationStatus !== 'invalid') {
return {
loginSession,
validationStatus,
};
}
}
return null;
}
public async findLoginSessionByTransferToken(transferTokenArg: string) {
const transferTokenHash = await LoginSession.hashSessionToken(transferTokenArg);
const loginSession = await this.CLoginSession.getInstance({
'data.transferTokenHash': transferTokenHash,
});
if (!loginSession) {
return null;
}
const isValid = await loginSession.validateTransferToken(transferTokenArg);
return isValid ? loginSession : null;
}
}
+10 -11
View File
@@ -95,9 +95,8 @@ export class OidcManager {
/**
* Handle the authorization endpoint request
*/
public async handleAuthorize(request: Request): Promise<Response> {
const url = new URL(request.url);
const params = url.searchParams;
public async handleAuthorize(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
const params = ctx.url.searchParams;
// Extract authorization request parameters
const clientId = params.get('client_id');
@@ -196,21 +195,21 @@ export class OidcManager {
/**
* Handle the token endpoint request
*/
public async handleToken(request: Request): Promise<Response> {
public async handleToken(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
// Parse form data
const contentType = request.headers.get('content-type');
const contentType = ctx.headers.get('content-type');
if (!contentType?.includes('application/x-www-form-urlencoded')) {
return this.tokenErrorResponse('invalid_request', 'Content-Type must be application/x-www-form-urlencoded');
}
const formData = await request.formData();
const formData = await ctx.formData();
const grantType = formData.get('grant_type') as string;
// Extract client credentials from Basic auth or form
let clientId = formData.get('client_id') as string;
let clientSecret = formData.get('client_secret') as string;
const authHeader = request.headers.get('authorization');
const authHeader = ctx.headers.get('authorization');
if (authHeader?.startsWith('Basic ')) {
const base64 = authHeader.substring(6);
const decoded = Buffer.from(base64, 'base64').toString('utf-8');
@@ -469,9 +468,9 @@ export class OidcManager {
/**
* Handle the userinfo endpoint
*/
public async handleUserInfo(request: Request): Promise<Response> {
public async handleUserInfo(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
// Get access token from Authorization header
const authHeader = request.headers.get('authorization');
const authHeader = ctx.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response(JSON.stringify({ error: 'invalid_token' }), {
status: 401,
@@ -575,8 +574,8 @@ export class OidcManager {
/**
* Handle the revocation endpoint
*/
public async handleRevoke(request: Request): Promise<Response> {
const formData = await request.formData();
public async handleRevoke(ctx: plugins.typedserver.IRequestContext): Promise<Response> {
const formData = await ctx.formData();
const token = formData.get('token') as string;
const tokenTypeHint = formData.get('token_type_hint') as string;
-2
View File
@@ -1,5 +1,4 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from './logging.js';
import { JwtManager } from './classes.jwtmanager.js';
@@ -30,7 +29,6 @@ export interface IReceptionOptions {
}
export class Reception {
public projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
public typedrouter = new plugins.typedrequest.TypedRouter();
public serviceQenv = new plugins.qenv.Qenv('./', './.nogit');
public szPlatformClient = new plugins.szPlatformClient.SzPlatformClient();
-1
View File
@@ -10,7 +10,6 @@ export class ReceptionDb {
}
public async start() {
console.log(this.receptionRef.options.mongoDescriptor);
this.smartdataDb = new plugins.smartdata.SmartdataDb(this.receptionRef.options.mongoDescriptor);
await this.smartdataDb.init();
}
+21 -3
View File
@@ -17,7 +17,7 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
const newUser = new User();
newUser.id = plugins.smartunique.shortId();
newUser.data = {
connectedOrgs: null,
connectedOrgs: [],
status: 'new',
name: userDataArg.name,
username: userDataArg.username,
@@ -31,8 +31,26 @@ export class User extends plugins.smartdata.SmartDataDbDoc<
return newUser;
}
public static hashPassword(passwordArg: string) {
return plugins.smarthash.sha256FromString(passwordArg);
public static async hashPassword(passwordArg: string) {
return plugins.argon2.hash(passwordArg);
}
public static isLegacyPasswordHash(passwordHashArg?: string) {
return !!passwordHashArg && !passwordHashArg.startsWith('$argon2');
}
public static shouldUpgradePasswordHash(passwordHashArg?: string) {
return this.isLegacyPasswordHash(passwordHashArg);
}
public static async verifyPassword(passwordArg: string, passwordHashArg?: string) {
if (!passwordHashArg) {
return false;
}
if (this.isLegacyPasswordHash(passwordHashArg)) {
return passwordHashArg === (await plugins.smarthash.sha256FromString(passwordArg));
}
return plugins.argon2.verify(passwordHashArg, passwordArg);
}
// INSTANCE
+11 -3
View File
@@ -23,6 +23,9 @@ export class UserManager {
new plugins.typedrequest.TypedHandler('getRolesAndOrganizationsForUserId', async reqArg => {
console.log('user manager: getting roles and orgs');
const user = await this.getUserByJwtValidation(reqArg.jwt);
if (!user) {
throw new plugins.typedrequest.TypedResponseError('User not found');
}
const organizations = await this.receptionRef.organizationmanager.getAllOrganizationsForUser(
user
);
@@ -49,8 +52,7 @@ export class UserManager {
email: user.data.email,
mobileNumber: user.data.mobileNumber,
connectedOrgs: user.data.connectedOrgs,
status: null,
password: null,
status: user.data.status,
isGlobalAdmin: user.data.isGlobalAdmin,
} as plugins.idpInterfaces.data.IUser['data']
}
@@ -64,6 +66,9 @@ export class UserManager {
*/
public async getUserByJwt(jwtString: string) {
const jwtInstance = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtString);
if (!jwtInstance) {
return null;
}
const user = await this.CUser.getInstance({
id: jwtInstance.data.userId
});
@@ -75,7 +80,10 @@ export class UserManager {
* faster than the "getUserByJwt"
*/
public async getUserByJwtValidation(jwtStringArg: string) {
const jwtDataArg: plugins.idpInterfaces.data.IJwt = await this.receptionRef.jwtManager.smartjwtInstance.verifyJWTAndGetData(jwtStringArg);
const jwtDataArg = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwtStringArg);
if (!jwtDataArg) {
return null;
}
const resultingUser = await this.CUser.getInstance({
id: jwtDataArg.data.userId
});
-3
View File
@@ -1,6 +1,3 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
const projectinfoNpm = new plugins.projectinfo.ProjectinfoNpm(paths.packageDir);
export const logger = new plugins.smartlog.ConsoleLog();
+1
View File
@@ -193,6 +193,7 @@ export class IdpCli {
this.storeCredentials({
...credentials,
jwt: response.jwt,
refreshToken: response.refreshToken || credentials.refreshToken,
});
return response.jwt;
}
+195
View File
@@ -0,0 +1,195 @@
# @idp.global/cli
Command-line interface for interacting with the idp.global Identity Provider. A Node.js CLI tool that provides authentication, user management, and organization administration from the terminal.
## Overview
The IdpCli module provides a complete command-line interface for managing your idp.global account and organizations. It uses file-based credential storage and WebSocket connections for real-time communication with the IdP server.
## Installation
```bash
npm install -g @idp.global/cli
# or
pnpm add -g @idp.global/cli
```
## Quick Start
```bash
# Login with email and password
idp login
# Check current user
idp whoami
# List your organizations
idp orgs
# Logout
idp logout
```
## Commands
### Authentication
| Command | Description |
|---------|-------------|
| `idp login` | Interactive login with email and password |
| `idp login-token` | Login with an API token |
| `idp logout` | Clear stored credentials and end session |
### User Information
| Command | Description |
|---------|-------------|
| `idp whoami` | Display current user information |
| `idp sessions` | List all active sessions |
| `idp revoke --session <id>` | Revoke a specific session |
### Organization Management
| Command | Description |
|---------|-------------|
| `idp orgs` | List all organizations you belong to |
| `idp orgs-create` | Create a new organization (interactive) |
| `idp members --org <id>` | List members of an organization |
| `idp invite --org <id> --email <email>` | Invite a user to an organization |
### Admin Commands (Global Admins Only)
| Command | Description |
|---------|-------------|
| `idp admin-check` | Check if you are a global admin |
| `idp admin-apps` | List all global apps with connection stats |
| `idp admin-suspend --user <id>` | Suspend a user account |
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `IDP_URL` | Override the IdP server URL | `https://idp.global` |
### Credential Storage
Credentials are stored in `~/.idp-global/credentials.json`. This file contains your refresh token and JWT for persistent authentication across CLI sessions.
## Programmatic Usage
You can also use the IdpCli class programmatically:
```typescript
import { IdpCli } from '@idp.global/cli';
const cli = new IdpCli({
idpBaseUrl: 'https://idp.global',
configDir: '/custom/config/path', // optional
});
// Login
await cli.loginWithPassword('user@example.com', 'password');
// Get current user
const user = await cli.whoami();
console.log('Logged in as:', user.data.name);
// Get organizations
const { organizations, roles } = await cli.getOrganizations();
for (const org of organizations) {
console.log(`- ${org.data.name} (${org.id})`);
}
// Disconnect when done
await cli.disconnect();
```
### IdpCli Class Methods
**Authentication:**
- `loginWithPassword(email, password)` - Login with credentials
- `loginWithApiToken(token)` - Login with API token
- `refreshJwt()` - Refresh the current JWT
- `logout()` - Clear credentials and end session
**User:**
- `whoami()` - Get current user info
- `getSessions()` - Get active sessions
- `revokeSession(sessionId)` - Revoke a session
**Organizations:**
- `getOrganizations()` - List user's organizations
- `createOrganization(name, slug, mode)` - Create new organization
- `getOrgMembers(orgId)` - Get organization members
- `inviteMember(orgId, email, roles)` - Invite a user
**Admin:**
- `checkGlobalAdmin()` - Check admin status
- `getGlobalAppStats()` - Get app statistics
- `suspendUser(userId)` - Suspend a user
## Examples
### Create an Organization
```bash
$ idp orgs-create
Organization Name: My Company
Organization Slug: my-company
Organization created successfully!
ID: org_abc123
Name: My Company
```
### Invite Team Members
```bash
$ idp invite --org org_abc123 --email colleague@example.com
Invitation sent to colleague@example.com
```
### View Active Sessions
```bash
$ idp sessions
Active Sessions:
- sess_xyz789
Device: MacBook Pro
Browser: Chrome
OS: macOS
Last Active: 1/29/2025, 2:30:00 PM
Current: Yes
```
## Dependencies
- `@api.global/typedrequest` - Type-safe API requests
- `@api.global/typedsocket` - WebSocket communication
- `@push.rocks/smartcli` - CLI framework
- `@push.rocks/smartinteract` - Interactive prompts
- `@idp.global/interfaces` - TypeScript interfaces
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
+4
View File
@@ -0,0 +1,4 @@
{
"name": "@idp.global/cli",
"order": 4
}
+71 -24
View File
@@ -29,9 +29,9 @@ export class IdpClient {
appDataArg = {
id: '', // TODO
appUrl: `https://${window.location.host}/`,
description: null,
logoUrl: null,
name: null,
description: '',
logoUrl: '',
name: '',
};
}
this.appData = appDataArg;
@@ -67,10 +67,14 @@ export class IdpClient {
await this.storeJwt(jwtStringArg);
}
public async setRefreshToken(refreshTokenArg: string) {
await this.storeRefreshToken(refreshTokenArg);
}
/**
* a typedsocket for going reactive
*/
public typedsocket: plugins.typedsocket.TypedSocket;
public typedsocket!: plugins.typedsocket.TypedSocket;
/**
* a typed router to go reactive
@@ -89,16 +93,30 @@ export class IdpClient {
await this.ssoStore.set('idpJwt', jwtString);
}
public async storeRefreshToken(refreshToken: string) {
await this.ssoStore.set('idpRefreshToken', refreshToken);
}
public async getJwt(): Promise<string> {
return await this.ssoStore.get('idpJwt');
}
public async getRefreshToken(): Promise<string> {
return await this.ssoStore.get('idpRefreshToken');
}
public async getJwtData(): Promise<plugins.idpInterfaces.data.IJwt> {
return this.helpers.extractDataFromJwtString(await this.getJwt());
}
public async deleteJwt() {
await this.ssoStore.delete('idpJwt');
console.log('removed jwt');
}
public async deleteRefreshToken() {
await this.ssoStore.delete('idpRefreshToken');
}
public async clearAuthState() {
await Promise.all([this.deleteJwt(), this.deleteRefreshToken()]);
}
/**
@@ -115,47 +133,63 @@ export class IdpClient {
if (extractedJwt.data.refreshFrom < Date.now() && Date.now() < extractedJwt.data.validUntil) {
jwt = await this.refreshJwt();
} else if (Date.now() > extractedJwt.data.validUntil) {
this.deleteJwt();
await this.deleteJwt();
jwt = await this.refreshJwt();
}
return jwt;
}
public async refreshJwt(refreshTokenArg?: string): Promise<string> {
let extractedJwt: plugins.idpInterfaces.data.IJwt;
public async refreshJwt(refreshTokenArg?: string): Promise<string | null> {
const refreshToken = refreshTokenArg || (await this.getRefreshToken());
if (!refreshTokenArg) {
extractedJwt = await this.helpers.extractDataFromJwtString(await this.getJwt());
if (!refreshToken) {
return null;
}
await this.typedsocketDeferred.promise;
const refreshJwtReq =
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
'refreshJwt'
);
const response = await refreshJwtReq.fire({
refreshToken: refreshTokenArg || extractedJwt.data.refreshToken,
const response = await refreshJwtReq
.fire({
refreshToken,
})
.catch(async () => {
await this.clearAuthState();
return null;
});
if (response.jwt) {
await this.storeJwt(response.jwt);
} else {
await this.deleteJwt();
if (!response?.jwt) {
await this.clearAuthState();
this.statusObservable.next(response?.status || 'loggedOut');
return null;
}
if (response.refreshToken) {
await this.storeRefreshToken(response.refreshToken);
}
await this.storeJwt(response.jwt);
this.statusObservable.next(response.status);
return await this.getJwt();
return response.jwt;
}
/**
* can be used to switch between pages
*/
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string> {
const jwt = await this.performJwtHousekeeping();
const extractedJwt = await this.helpers.extractDataFromJwtString(jwt);
public async getTransferToken(appDataArg?: plugins.idpInterfaces.data.IAppLegacy): Promise<string | null> {
await this.performJwtHousekeeping();
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
return null;
}
await this.typedsocketDeferred.promise;
const getTransferToken =
this.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_ExchangeRefreshTokenAndTransferToken>(
'exchangeRefreshTokenAndTransferToken'
);
const response = await getTransferToken.fire({
refreshToken: extractedJwt.data.refreshToken,
refreshToken,
appData: appDataArg || this.appData,
});
return response.transferToken;
@@ -230,6 +264,13 @@ export class IdpClient {
const jwt = await this.performJwtHousekeeping();
return !!jwt;
} else {
const refreshToken = await this.getRefreshToken();
if (refreshToken) {
const jwt = await this.refreshJwt(refreshToken);
if (jwt) {
return true;
}
}
const transferTokenResult = await this.processTransferToken();
if (transferTokenResult) {
// we are in the clear
@@ -258,12 +299,18 @@ export class IdpClient {
*/
public async logout() {
const idpLogoutUrl = this.parsedReceptionUrl.clone().set('path', '/logout');
const refreshToken = await this.getRefreshToken();
if (!globalThis.location.href.startsWith(idpLogoutUrl.origin)) {
// we are somewhere in an app
await this.deleteJwt();
await this.clearAuthState();
globalThis.location.href = idpLogoutUrl.toString();
} else {
// we are in the sso page
if (!refreshToken) {
await this.clearAuthState();
window.location.href = this.parsedReceptionUrl.origin;
return;
}
await this.enableTypedSocket();
console.log(`logging out against ${this.parsedReceptionUrl.toString()}`);
const logoutTr =
@@ -271,9 +318,9 @@ export class IdpClient {
'logout'
);
await logoutTr.fire({
refreshToken: (await this.getJwtData()).data.refreshToken,
refreshToken,
});
await this.deleteJwt();
await this.clearAuthState();
const appData = await this.getAppDataOnSsoDomain();
if (appData) {
console.log(`redirecting to app after logout: ${appData.appUrl}`);
+389
View File
@@ -0,0 +1,389 @@
# @idp.global/idpclient
A TypeScript client library for integrating with the idp.global Identity Provider. Works in both browser and Node.js environments.
## Overview
The IdpClient provides a complete API for authentication, session management, and organization operations. It uses WebSocket connections via TypedSocket for real-time, type-safe communication with the IdP server.
## Installation
```bash
npm install @idp.global/idpclient
# or
pnpm add @idp.global/idpclient
```
## Quick Start
```typescript
import { IdpClient } from '@idp.global/idpclient';
// Initialize the client
const idpClient = new IdpClient('https://idp.global');
// Enable WebSocket connection
await idpClient.enableTypedSocket();
// Check login status
const isLoggedIn = await idpClient.determineLoginStatus();
if (isLoggedIn) {
const userInfo = await idpClient.whoIs();
console.log('Logged in as:', userInfo.user.data.name);
}
```
## Core Features
### Authentication
#### Password Login
```typescript
const response = await idpClient.requests.loginWithUserNameAndPassword.fire({
username: 'user@example.com',
password: 'securepassword',
});
if (response.refreshToken) {
await idpClient.refreshJwt(response.refreshToken);
console.log('Login successful!');
} else if (response.twoFaNeeded) {
console.log('2FA verification required');
}
```
#### Magic Link Login
```typescript
// Request magic link
await idpClient.requests.loginWithEmail.fire({
email: 'user@example.com',
});
// After clicking the email link
const result = await idpClient.requests.loginWithEmailAfterToken.fire({
email: 'user@example.com',
token: 'token-from-email-link',
});
if (result.refreshToken) {
await idpClient.refreshJwt(result.refreshToken);
}
```
#### API Token Login
```typescript
const result = await idpClient.requests.loginWithApiToken.fire({
apiToken: 'your-api-token',
});
if (result.jwt) {
await idpClient.setJwt(result.jwt);
}
```
### Session Management
```typescript
// Get current JWT
const jwt = await idpClient.getJwt();
// Get parsed JWT data
const jwtData = await idpClient.getJwtData();
console.log('User ID:', jwtData.id);
// Refresh JWT (automatic housekeeping)
await idpClient.performJwtHousekeeping();
// Manual refresh
await idpClient.refreshJwt();
// Logout
await idpClient.logout();
```
### User Information
```typescript
// Get current user details
const whoIsResponse = await idpClient.whoIs();
console.log('Name:', whoIsResponse.user.data.name);
console.log('Email:', whoIsResponse.user.data.email);
// Get user data
const userData = await idpClient.requests.getUserData.fire({
jwt: await idpClient.getJwt(),
userId: jwtData.id,
});
// Update user data
await idpClient.requests.setUserData.fire({
jwt: await idpClient.getJwt(),
userId: jwtData.id,
name: 'New Name',
});
```
### Organization Management
```typescript
// Get user's organizations and roles
const orgsAndRoles = await idpClient.getRolesAndOrganizations();
console.log('Organizations:', orgsAndRoles.organizations);
console.log('Roles:', orgsAndRoles.roles);
// Create a new organization
const result = await idpClient.createOrganization(
'My Company', // name
'my-company', // slug
'manifest' // mode: 'checkAvailability' or 'manifest'
);
if (result.resultingOrganization) {
console.log('Created:', result.resultingOrganization.id);
}
// Get organization details
const orgDetails = await idpClient.requests.getOrganizationById.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
});
```
### Member & Invitation Management
```typescript
// Get organization members
const members = await idpClient.requests.getOrgMembers.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
});
// Invite a new member
await idpClient.requests.createInvitation.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
email: 'newmember@example.com',
roles: ['member'],
});
// Bulk invite members
await idpClient.requests.bulkCreateInvitations.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
invitations: [
{ email: 'user1@example.com', roles: ['member'] },
{ email: 'user2@example.com', roles: ['admin'] },
],
});
// Accept an invitation
await idpClient.requests.acceptInvitation.fire({
jwt: await idpClient.getJwt(),
invitationToken: 'token-from-invite-email',
});
// Remove a member
await idpClient.requests.removeMember.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
userId: 'user-id',
});
// Transfer ownership
await idpClient.requests.transferOwnership.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
newOwnerId: 'new-owner-user-id',
});
```
### Password Management
```typescript
// Request password reset
await idpClient.requests.resetPassword.fire({
email: 'user@example.com',
});
// Set new password (with token from email)
await idpClient.requests.setNewPassword.fire({
email: 'user@example.com',
tokenArg: 'reset-token',
newPassword: 'newsecurepassword',
});
// Change password (when logged in)
await idpClient.requests.setNewPassword.fire({
email: 'user@example.com',
oldPassword: 'currentpassword',
newPassword: 'newsecurepassword',
});
```
### Session & Device Management
```typescript
// Get active sessions
const sessions = await idpClient.requests.getUserSessions.fire({
jwt: await idpClient.getJwt(),
userId: jwtData.id,
});
// Revoke a session
await idpClient.requests.revokeSession.fire({
jwt: await idpClient.getJwt(),
sessionId: 'session-id',
});
// Get device ID
const deviceInfo = await idpClient.requests.obtainDeviceId.fire({});
// Attach device to session
await idpClient.requests.attachDeviceId.fire({
jwt: await idpClient.getJwt(),
deviceId: deviceInfo.deviceId.id,
});
```
### Cross-Domain Authentication
```typescript
// Get transfer token for SSO between apps
const transferToken = await idpClient.getTransferToken();
// Switch to another app with authentication
await idpClient.getTransferTokenAndSwitchToLocation('https://app.example.com/');
// Process incoming transfer token (in target app)
const success = await idpClient.processTransferToken();
if (success) {
console.log('Cross-domain login successful');
}
```
### Billing Integration
```typescript
// Get billing plan for an organization
const billingPlan = await idpClient.requests.getBillingPlan.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
});
// Get Paddle configuration
const paddleConfig = await idpClient.requests.getPaddleConfig.fire({
jwt: await idpClient.getJwt(),
});
// Update payment method
await idpClient.updatePaddleCheckoutId('org-id', 'checkout-id');
```
### Admin Operations (Global Admins Only)
```typescript
// Check if user is global admin
const isAdmin = await idpClient.requests.checkGlobalAdmin.fire({
jwt: await idpClient.getJwt(),
});
// Get platform statistics
const stats = await idpClient.requests.getGlobalAppStats.fire({
jwt: await idpClient.getJwt(),
});
// Create a global app
await idpClient.requests.createGlobalApp.fire({
jwt: await idpClient.getJwt(),
name: 'My App',
description: 'App description',
});
// Suspend a user
await idpClient.requests.suspendUser.fire({
jwt: await idpClient.getJwt(),
userId: 'user-id',
});
```
## Reactive Subscriptions
The client provides RxJS subjects for reactive updates:
```typescript
// Subscribe to login status changes
idpClient.statusObservable.subscribe((status) => {
console.log('Login status changed:', status);
});
// Subscribe to roles updates
idpClient.rolesReplaySubject.subscribe((roles) => {
console.log('Roles updated:', roles);
});
// Subscribe to organizations updates
idpClient.organizationsReplaySubject.subscribe((orgs) => {
console.log('Organizations updated:', orgs);
});
```
## API Reference
### IdpClient Class
| Method | Description |
|--------|-------------|
| `enableTypedSocket()` | Initialize WebSocket connection |
| `determineLoginStatus(requireLogin?)` | Check if user is logged in |
| `getJwt()` | Get stored JWT string |
| `getJwtData()` | Get parsed JWT data |
| `setJwt(jwt)` | Store JWT |
| `deleteJwt()` | Remove stored JWT |
| `refreshJwt(refreshToken?)` | Refresh the JWT |
| `performJwtHousekeeping()` | Auto-refresh JWT if needed |
| `logout()` | End session and redirect |
| `whoIs()` | Get current user info |
| `getRolesAndOrganizations()` | Get user's orgs and roles |
| `createOrganization(name, slug, mode)` | Create new organization |
| `getTransferToken(appData?)` | Get SSO transfer token |
| `processTransferToken()` | Process incoming transfer token |
| `stop()` | Close WebSocket connection |
### IdpRequests Class
Access via `idpClient.requests.*`:
**Authentication**: `loginWithUserNameAndPassword`, `loginWithEmail`, `loginWithEmailAfterToken`, `loginWithApiToken`, `resetPassword`, `setNewPassword`
**User**: `getUserData`, `setUserData`, `getUserSessions`, `revokeSession`, `getUserActivity`
**Organization**: `getOrganizationById`, `updateOrganization`, `createInvitation`, `bulkCreateInvitations`, `getOrgMembers`, `getOrgInvitations`, `acceptInvitation`, `cancelInvitation`, `resendInvitation`, `removeMember`, `updateMemberRoles`, `transferOwnership`
**Billing**: `getBillingPlan`, `getPaddleConfig`
**Admin**: `checkGlobalAdmin`, `getGlobalAppStats`, `createGlobalApp`, `updateGlobalApp`, `deleteGlobalApp`, `suspendUser`, `deleteSuspendedUser`
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
+1
View File
@@ -1,3 +1,4 @@
{
"name": "@idp.global/client",
"order": 3
}
+7 -2
View File
@@ -10,6 +10,11 @@ export interface IJwt {
*/
userId: string;
/**
* the login session backing this jwt
*/
sessionId?: string;
/**
* the latest point of
*/
@@ -24,9 +29,9 @@ export interface IJwt {
refreshEvery: number;
/**
* the refresh token to obtain a new jwt for a session
* legacy field kept for compatibility with already-issued jwt documents
*/
refreshToken: string;
refreshToken?: string;
/**
* just for looks/debugging
@@ -1,15 +1,22 @@
export interface ILoginSession {
id: string;
data: {
userId: string;
userId: string | null;
validUntil: number;
invalidated: boolean;
refreshToken: string;
/**
* legacy plaintext refresh token field kept so existing sessions can migrate on first use
*/
refreshToken?: string | null;
refreshTokenHash?: string | null;
rotatedRefreshTokenHashes?: string[];
transferTokenHash?: string | null;
transferTokenExpiresAt?: number | null;
/**
* a device id that can be used to share the login session
* in different contexts on the same device
*/
deviceId: string;
deviceId?: string | null;
/**
* Device metadata for session display
*/
@@ -18,7 +25,7 @@ export interface ILoginSession {
browser: string;
os: string;
ip: string;
};
} | null;
/**
* When this session was created
*/
+329
View File
@@ -0,0 +1,329 @@
# @idp.global/interfaces
TypeScript interfaces and type definitions for the idp.global Identity Provider platform.
## Overview
This package provides the complete type system for idp.global, including data models, API request/response interfaces, and OIDC definitions. Use this package when building applications that integrate with idp.global or when you need type-safe interactions with the IdP API.
## Installation
```bash
npm install @idp.global/interfaces
# or
pnpm add @idp.global/interfaces
```
## Usage
```typescript
import { data, request, tags } from '@idp.global/interfaces';
// Data interfaces
const user: data.IUser = {
id: 'user_123',
data: {
name: 'John Doe',
username: 'johndoe',
email: 'john@example.com',
status: 'active',
connectedOrgs: ['org_1', 'org_2'],
},
};
// Organization interface
const org: data.IOrganization = {
id: 'org_1',
data: {
name: 'Acme Corp',
slug: 'acme',
billingPlanId: 'plan_free',
roleIds: ['role_admin', 'role_member'],
},
};
```
## Package Structure
```
ts_interfaces/
├── data/ # Data model interfaces
│ ├── loint-reception.user.ts # User profiles
│ ├── loint-reception.organization.ts # Organizations
│ ├── loint-reception.role.ts # RBAC roles
│ ├── loint-reception.app.ts # OAuth applications
│ ├── loint-reception.oidc.ts # OIDC tokens & flows
│ ├── loint-reception.jwt.ts # JWT structures
│ ├── loint-reception.loginsession.ts # Login sessions
│ ├── loint-reception.billingplan.ts # Billing plans
│ ├── loint-reception.device.ts # Device management
│ ├── loint-reception.activity.ts # Activity logs
│ ├── loint-reception.userinvitation.ts # Invitations
│ └── loint-reception.appconnection.ts # App connections
├── request/ # API request/response interfaces
│ ├── loint-reception.login.ts # Authentication
│ ├── loint-reception.registration.ts # User registration
│ ├── loint-reception.user.ts # User management
│ ├── loint-reception.organization.ts # Org management
│ ├── loint-reception.jwt.ts # JWT operations
│ ├── loint-reception.apitoken.ts # API tokens
│ ├── loint-reception.app.ts # App management
│ ├── loint-reception.billingplan.ts # Billing
│ └── loint-reception.admin.ts # Admin operations
└── tags/ # Tag definitions
```
## Data Interfaces
### User (`IUser`)
```typescript
interface IUser {
id: string;
data: {
name: string;
username: string;
email: string;
mobileNumber?: string;
password?: string; // Only during initial setting
passwordHash?: string; // For validation
status: 'new' | 'active' | 'deleted' | 'suspended';
connectedOrgs: string[]; // Organization IDs
isGlobalAdmin?: boolean; // Platform admin flag
};
}
```
### Organization (`IOrganization`)
```typescript
interface IOrganization {
id: string;
data: {
name: string;
slug: string;
billingPlanId: string;
roleIds: string[];
};
}
```
### Role (`IRole`)
```typescript
interface IRole {
id: string;
data: {
name: string;
organizationId: string;
userId: string;
permissions: string[];
};
}
```
### OAuth Application Types
```typescript
// Global platform apps (maintained by platform admins)
interface IGlobalApp {
id: string;
type: 'globalApp';
data: {
name: string;
description: string;
iconBase64?: string;
oauthCredentials?: IOAuthCredentials;
};
}
// Partner apps (third-party integrations)
interface IPartnerApp {
id: string;
type: 'partnerApp';
data: {
name: string;
description: string;
ownerOrganizationId: string;
oauthCredentials?: IOAuthCredentials;
};
}
// Custom OIDC clients
interface ICustomOidcApp {
id: string;
type: 'customOidcApp';
data: {
name: string;
description: string;
ownerOrganizationId: string;
oauthCredentials: IOAuthCredentials;
};
}
```
### OAuth Credentials
```typescript
interface IOAuthCredentials {
clientId: string;
clientSecretHash: string;
redirectUris: string[];
scopes: string[];
grantTypes: ('authorization_code' | 'refresh_token' | 'client_credentials')[];
}
```
## OIDC Interfaces
### Authorization Code
```typescript
interface IAuthorizationCode {
code: string;
clientId: string;
userId: string;
scopes: string[];
redirectUri: string;
codeChallenge?: string;
codeChallengeMethod?: 'S256';
expiresAt: number;
used: boolean;
}
```
### Token Response
```typescript
interface ITokenResponse {
access_token: string;
token_type: 'Bearer';
expires_in: number;
refresh_token?: string;
id_token?: string;
scope: string;
}
```
### UserInfo Response
```typescript
interface IUserInfoResponse {
sub: string;
name?: string;
preferred_username?: string;
email?: string;
email_verified?: boolean;
organizations?: Array<{
id: string;
name: string;
slug: string;
roles: string[];
}>;
roles?: string[];
}
```
### ID Token Claims
```typescript
interface IIdTokenClaims {
iss: string; // Issuer
sub: string; // Subject (user ID)
aud: string; // Audience (client ID)
exp: number; // Expiration time
iat: number; // Issued at
nonce?: string; // Replay protection
name?: string;
email?: string;
email_verified?: boolean;
organizations?: Array<{...}>;
roles?: string[];
}
```
## Request Interfaces
All API requests follow the TypedRequest pattern:
```typescript
interface IReq_LoginWithEmailOrUsernameAndPassword {
method: 'loginWithEmailOrUsernameAndPassword';
request: {
username: string;
password: string;
};
response: {
refreshToken?: string;
twoFaNeeded: boolean;
};
}
```
### Authentication Requests
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_LoginWithEmailOrUsernameAndPassword` | `loginWithEmailOrUsernameAndPassword` | Password login |
| `IReq_LoginWithEmail` | `loginWithEmail` | Magic link request |
| `IReq_LoginWithEmailAfterEmailTokenAquired` | `loginWithEmailAfterEmailTokenAquired` | Magic link verification |
| `IReq_LoginWithApiToken` | `loginWithApiToken` | API token login |
| `IReq_RefreshJwt` | `refreshJwt` | Refresh access token |
| `ILogoutRequest` | `logout` | End session |
### User Management Requests
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetUserData` | `getUserData` | Get current user |
| `IReq_SetUserData` | `setUserData` | Update user profile |
| `IReq_GetUserSessions` | `getUserSessions` | List active sessions |
| `IReq_ResetPassword` | `resetPassword` | Request password reset |
| `IReq_SetNewPassword` | `setNewPassword` | Set new password |
### Organization Requests
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_CreateOrganization` | `createOrganization` | Create new org |
| `IReq_GetOrgMembers` | `getOrgMembers` | List org members |
| `IReq_CreateInvitation` | `createInvitation` | Invite user |
| `IReq_AcceptInvitation` | `acceptInvitation` | Accept invite |
### JWT Operations
| Interface | Method | Description |
|-----------|--------|-------------|
| `IReq_GetPublicKeyForValidation` | `getPublicKeyForValidation` | Get JWT public key |
| `IReq_GetJwtIdBlocklist` | `getJwtIdBlocklist` | Get revoked token IDs |
## Supported OIDC Scopes
| Scope | Description |
|-------|-------------|
| `openid` | Required for OIDC flows |
| `profile` | User's name and username |
| `email` | User's email address |
| `organizations` | User's organization memberships |
| `roles` | User's roles within organizations |
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
@@ -87,7 +87,8 @@ export interface IReq_RefreshJwt
};
response: {
status: data.TLoginStatus;
jwt: string;
jwt?: string;
refreshToken?: string;
};
}
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@idp.global/idp.global',
version: '1.13.0',
version: '1.17.0',
description: 'An identity provider software managing user authentications, registrations, and sessions.'
}
+1 -1
View File
@@ -19,7 +19,7 @@ import { accountDesignTokens } from './sharedstyles.js';
import * as views from './views/index.js';
import * as accountstate from '../../states/accountstate.js';
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
declare global {
+1 -1
View File
@@ -17,7 +17,7 @@ import { accountDesignTokens } from './sharedstyles.js';
import { CreateOrgModal } from './create-org-modal.js';
import { OrgSelectModal } from './org-select-modal.js';
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
import { commitinfo } from '../../../ts/00_commitinfo_data.js';
declare global {
interface HTMLElementTagNameMap {
+13 -7
View File
@@ -30,6 +30,19 @@ export const cardStyles = css`
}
`;
/**
* Base styles for all view components
* Provides consistent background and foreground colors
*/
export const viewBaseStyles = css`
:host {
display: block;
min-height: 100%;
background: var(--background);
color: var(--foreground);
}
`;
/**
* Typography styles for consistent text hierarchy
*/
@@ -108,10 +121,3 @@ export const navigationStyles = css`
}
`;
/**
* Legacy export for backwards compatibility
*/
export default css`
${accountDesignTokens}
${typographyStyles}
`;
+3 -9
View File
@@ -11,7 +11,7 @@ import {
} from '@design.estate/dees-element';
import { IdpState } from '../../../states/idp.state.js';
import { accountDesignTokens } from '../sharedstyles.js';
import * as sharedStyles from '../sharedstyles.js';
declare global {
interface HTMLElementTagNameMap {
@@ -43,15 +43,9 @@ export class AdminView extends DeesElement {
public static styles = [
cssManager.defaultStyles,
accountDesignTokens,
sharedStyles.accountDesignTokens,
sharedStyles.viewBaseStyles,
css`
:host {
display: block;
min-height: 100%;
background: var(--background);
color: var(--foreground);
}
.container {
max-width: 1200px;
margin: 0 auto;
+5 -5
View File
@@ -9,7 +9,7 @@ import {
state,
} from '@design.estate/dees-element';
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
import * as sharedStyles from '../sharedstyles.js';
import * as accountState from '../../../states/accountstate.js';
import { IdpState } from '../../../states/idp.state.js';
@@ -45,12 +45,12 @@ export class AppsView extends DeesElement {
public static styles = [
cssManager.defaultStyles,
accountDesignTokens,
cardStyles,
typographyStyles,
sharedStyles.accountDesignTokens,
sharedStyles.viewBaseStyles,
sharedStyles.cardStyles,
sharedStyles.typographyStyles,
css`
:host {
display: block;
padding: 48px;
max-width: 1000px;
margin: 0 auto;
+3 -9
View File
@@ -10,7 +10,7 @@ import {
type TemplateResult,
} from '@design.estate/dees-element';
import { accountDesignTokens } from '../sharedstyles.js';
import * as sharedStyles from '../sharedstyles.js';
import * as accountStateModule from '../../../states/accountstate.js';
import { IdpState } from '../../../states/idp.state.js';
@@ -59,15 +59,9 @@ export class BaseView extends DeesElement {
public static styles = [
cssManager.defaultStyles,
accountDesignTokens,
sharedStyles.accountDesignTokens,
sharedStyles.viewBaseStyles,
css`
:host {
display: block;
min-height: 100%;
background: var(--background);
color: var(--foreground);
}
.container {
max-width: 1000px;
margin: 0 auto;
+3 -8
View File
@@ -10,7 +10,7 @@ import {
type TemplateResult,
} from '@design.estate/dees-element';
import { accountDesignTokens } from '../sharedstyles.js';
import * as sharedStyles from '../sharedstyles.js';
import * as accountStateModule from '../../../states/accountstate.js';
import { IdpState } from '../../../states/idp.state.js';
@@ -41,14 +41,9 @@ export class OrgView extends DeesElement {
public static styles = [
cssManager.defaultStyles,
accountDesignTokens,
sharedStyles.accountDesignTokens,
sharedStyles.viewBaseStyles,
css`
:host {
display: block;
min-height: 100%;
background: var(--background);
color: var(--foreground);
}
.container {
max-width: 1000px;
+5 -5
View File
@@ -9,7 +9,7 @@ import {
} from '@design.estate/dees-element';
import * as plugins from '../../../plugins.js';
import sharedStyles from '../sharedstyles.js';
import * as sharedStyles from '../sharedstyles.js';
import * as state from '../../../states/accountstate.js';
import { IdpState } from '../../../states/idp.state.js';
@@ -23,13 +23,13 @@ declare global {
export class PaddleSetupView extends DeesElement {
public static styles = [
cssManager.defaultStyles,
sharedStyles,
sharedStyles.accountDesignTokens,
sharedStyles.viewBaseStyles,
css`
:host {
display: block;
padding: 48px;
max-width: 900px;
margin: auto;
color: ${cssManager.bdTheme('#333', '#fff')};
margin: 0 auto;
}
`,
];
@@ -8,7 +8,7 @@ import {
css,
} from '@design.estate/dees-element';
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
import * as sharedStyles from '../sharedstyles.js';
import * as state from '../../../states/accountstate.js';
@@ -46,12 +46,12 @@ export class SubscriptionView extends DeesElement {
public static styles = [
cssManager.defaultStyles,
accountDesignTokens,
cardStyles,
typographyStyles,
sharedStyles.accountDesignTokens,
sharedStyles.viewBaseStyles,
sharedStyles.cardStyles,
sharedStyles.typographyStyles,
css`
:host {
display: block;
padding: 48px;
max-width: 900px;
margin: 0 auto;
+5 -5
View File
@@ -9,7 +9,7 @@ import {
type TemplateResult,
} from '@design.estate/dees-element';
import sharedStyles, { accountDesignTokens, cardStyles, typographyStyles } from '../sharedstyles.js';
import * as sharedStyles from '../sharedstyles.js';
import * as accountState from '../../../states/accountstate.js';
import { IdpState } from '../../../states/idp.state.js';
import { BulkInviteModal } from '../bulk-invite-modal.js';
@@ -83,12 +83,12 @@ export class UsersView extends DeesElement {
public static styles = [
cssManager.defaultStyles,
accountDesignTokens,
cardStyles,
typographyStyles,
sharedStyles.accountDesignTokens,
sharedStyles.viewBaseStyles,
sharedStyles.cardStyles,
sharedStyles.typographyStyles,
css`
:host {
display: block;
padding: 48px;
max-width: 1000px;
margin: 0 auto;
+1 -1
View File
@@ -11,7 +11,7 @@ import {
query,
} from '@design.estate/dees-element';
import { commitinfo } from '../../dist_ts/00_commitinfo_data.js';
import { commitinfo } from '../../ts/00_commitinfo_data.js';
import { IdpState } from '../states/idp.state.js';
declare global {
+4 -11
View File
@@ -207,21 +207,14 @@ export class IdpRegistrationPrompt extends DeesElement {
}
public async handleRefreshToken(refreshTokenArg: string, delayDispatchMillisArg = 0) {
// a refreshToken binds directly to a session.
// the refresh token is used on a continuous basis to get fresh and short-lived jwts
const idpState = await IdpState.getSingletonInstance();
const refreshJwt = idpState.idpClient.typedsocket.createTypedRequest<plugins.idpInterfaces.request.IReq_RefreshJwt>(
'refreshJwt'
);
const responseJwt = await refreshJwt.fire({
refreshToken: refreshTokenArg,
});
const jwt = await idpState.idpClient.refreshJwt(refreshTokenArg);
if (responseJwt.jwt) {
if (jwt) {
this.domtools.convenience.smartdelay.delayFor(delayDispatchMillisArg).then(() => {
this.dispatchJwt(responseJwt.jwt);
this.dispatchJwt(jwt);
});
return responseJwt.jwt;
return jwt;
} else {
return null;
}
+6 -6
View File
@@ -488,15 +488,15 @@ export class IdpRegistrationStepper extends DeesElement {
username: this.storedData.email,
password: eventArg.detail.data.password,
});
this.storedData.refreshToken = loginResponse.refreshToken;
deesForm.setStatus('pending', 'Obtaining JWT...');
const jwtResponse = await idpState.idpClient.requests.obtainJwt.fire({
refreshToken: this.storedData.refreshToken,
});
const jwt = await idpState.idpClient.refreshJwt(loginResponse.refreshToken);
if (!jwt) {
deesForm.setStatus('error', 'Failed to establish a login session.');
return;
}
deesForm.setStatus('success', 'Ok! Lets Go!');
await idpState.idpClient.setJwt(jwtResponse.jwt);
idpState.domtools.router.pushUrl('/account');
}, { signal });
},
+273
View File
@@ -0,0 +1,273 @@
# @idp.global/web
Web Components and UI elements for the idp.global Identity Provider platform. Built with `@design.estate/dees-element` and the dees-catalog component library.
## Overview
This package provides the complete web interface for idp.global, including authentication flows, account management, and organization administration. All components are built as Web Components using the Lit-based `dees-element` framework.
## Installation
```bash
npm install @idp.global/web
# or
pnpm add @idp.global/web
```
## Architecture
```
ts_web/
├── index.ts # Application entry point
├── plugins.ts # Plugin imports
├── views/
│ ├── viewcontainer.ts # Main view router
│ └── index.ts
├── elements/ # Web Components
│ ├── idp-loginprompt.ts # Login form
│ ├── idp-registerprompt.ts # Registration form
│ ├── idp-registration-stepper.ts # Multi-step registration
│ ├── idp-centercontainer.ts # Centered layout container
│ ├── idp-transfermanager.ts # SSO transfer handling
│ ├── idp-welcome.ts # Welcome/landing page
│ └── account/ # Account dashboard components
│ ├── content.ts # Main account layout
│ ├── navigation.ts # Sidebar navigation
│ ├── org-select-modal.ts # Organization switcher
│ ├── create-org-modal.ts # Create organization dialog
│ ├── bulk-invite-modal.ts # Bulk member invite dialog
│ └── views/ # Account sub-views
│ ├── baseview.ts # Base view class
│ ├── usersview.ts # User profile view
│ ├── orgview.ts # Organization details
│ ├── orgsetup.ts # Organization setup
│ ├── appsview.ts # Connected apps
│ ├── adminview.ts # Global admin panel
│ ├── subscriptions.ts # Billing subscriptions
│ └── paddlesetup.ts # Payment setup
└── states/
├── idp.state.ts # Main application state
└── accountstate.ts # Account dashboard state
```
## Components
### Authentication Components
#### `<idp-loginprompt>`
Login form supporting password and magic link authentication.
```html
<idp-loginprompt></idp-loginprompt>
```
Features:
- Email/username + password login
- Magic link (passwordless) authentication
- Automatic button text based on password presence
- Form validation and error handling
- Redirect to registration
#### `<idp-registerprompt>`
Initial registration form for new users.
```html
<idp-registerprompt></idp-registerprompt>
```
#### `<idp-registration-stepper>`
Multi-step registration wizard for completing user profile.
```html
<idp-registration-stepper></idp-registration-stepper>
```
Steps include:
- Profile information
- Email verification
- Mobile verification (optional)
- Password setup
### Layout Components
#### `<idp-viewcontainer>`
Main view container that handles routing between views.
```html
<idp-viewcontainer></idp-viewcontainer>
```
Supported views:
- `welcome` - Landing page
- `login` - Login form
- `register` - Registration form
- `finishregistration` - Registration stepper
- `account` - Account dashboard
#### `<idp-centercontainer>`
Centered container with animation support for forms.
```html
<idp-centercontainer>
<h2>Your Content</h2>
<form>...</form>
</idp-centercontainer>
```
Methods:
- `show()` - Animate container into view
- `hide()` - Animate container out of view
### Account Dashboard Components
#### `<idp-account-content>`
Main account dashboard layout with navigation.
```html
<idp-account-content></idp-account-content>
```
#### Navigation Views
| Component | Route | Description |
|-----------|-------|-------------|
| `<idp-usersview>` | `/account/users` | User profile management |
| `<idp-orgview>` | `/account/org` | Organization details |
| `<idp-orgsetup>` | `/account/orgsetup` | Organization configuration |
| `<idp-appsview>` | `/account/apps` | Connected applications |
| `<idp-adminview>` | `/account/admin` | Global admin panel |
| `<idp-subscriptions>` | `/account/subscriptions` | Billing management |
| `<idp-paddlesetup>` | `/account/paddle` | Payment method setup |
### Modal Components
#### `<idp-org-select-modal>`
Organization switcher modal for users with multiple organizations.
#### `<idp-create-org-modal>`
Dialog for creating new organizations with slug validation.
#### `<idp-bulk-invite-modal>`
Bulk invitation dialog for inviting multiple members at once.
## State Management
### IdpState
Central application state using `@push.rocks/smartstate`.
```typescript
import { IdpState } from '@idp.global/web';
const idpState = await IdpState.getSingletonInstance();
// Access IdP client
const isLoggedIn = await idpState.idpClient.determineLoginStatus();
// Access router
idpState.domtools.router.pushUrl('/login');
// Subscribe to view changes
idpState.mainStatePart.select(s => s.view).subscribe(view => {
console.log('Current view:', view);
});
```
### AccountState
State for the account dashboard section.
```typescript
import { AccountState } from '@idp.global/web';
const accountState = await AccountState.getSingletonInstance();
// Access current organization
const currentOrg = accountState.currentOrganization;
// Access user roles
const roles = accountState.userRoles;
```
## Styling
Components use CSS custom properties for theming:
```css
:host {
--foreground: hsl(0 0% 98%);
--muted-foreground: hsl(240 5% 64.9%);
--background-accent: #303f9f;
}
```
All components include:
- Dark mode by default
- Geist Sans font family
- Smooth animations
- Responsive layouts
## Dependencies
- `@design.estate/dees-element` - Web Component base class
- `@design.estate/dees-catalog` - UI component library
- `@design.estate/dees-domtools` - DOM utilities and routing
- `@idp.global/idpclient` - IdP client library
- `@idp.global/interfaces` - TypeScript interfaces
- `@push.rocks/smartstate` - State management
- `@uptime.link/webwidget` - Status widget
## Views and Routes
| Route | View | Component |
|-------|------|-----------|
| `/` | `welcome` | `IdpWelcome` |
| `/login` | `login` | `IdpLoginPrompt` |
| `/register` | `register` | `IdpRegistrationPrompt` |
| `/finishregistration` | `finishregistration` | `IdpRegistrationStepper` |
| `/account` | `account` | `IdpAccountContent` |
| `/logout` | - | Logout handler |
## Building
The web module is bundled using `@git.zone/tsbundle`:
```bash
# Development with hot reload
pnpm watch
# Production build
pnpm build
```
The bundled output is served from `dist_ts_web/` by the TypedServer.
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
+1 -1
View File
@@ -1,3 +1,3 @@
{
"order": 4
"order": 5
}
+3 -1
View File
@@ -4,7 +4,9 @@
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
"verbatimModuleSyntax": true,
"types": ["node"],
"strict": false
},
"exclude": [
"dist_*/**/*.d.ts"