open-index/open-npm huggingface dataset:
npm package: @meridianjs/activity
Description: Meridian activity module — Audit log for all domain events
Latest version: 1.0.1

# @meridianjs/activity

Audit log module for MeridianJS. Records every significant action taken on domain entities — who did what, when, and what changed.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Service: `activityModuleService`

```typescript
const svc = req.scope.resolve("activityModuleService") as any
```

### Methods

```typescript
// Record an audit entry — fire-and-forget safe (never throws)
await svc.recordActivity({
  entity_type:  "issue",
  entity_id:    issueId,
  actor_id:     req.user.id,
  action:       "status_changed",
  workspace_id: req.user.workspaceId,
  changes: {
    status: { from: "Todo", to: "In Progress" },
  },
})

// List all activity for a specific entity
const log = await svc.listActivityForEntity("issue", issueId)

// List all activity in a workspace
const log = await svc.listActivityByWorkspace(workspaceId)

// Standard CRUD
await svc.listActivities(filters?)
await svc.retrieveActivity(id)
await svc.deleteActivity(id)
```

## Data Model

### Activity

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `entity_type` | `text` | Entity kind (e.g. `"issue"`, `"project"`) |
| `entity_id` | `text` | ID of the affected entity |
| `actor_id` | `text` | User who performed the action |
| `action` | `text` | Action key (e.g. `"created"`, `"status_changed"`, `"assigned"`) |
| `workspace_id` | `text` | Owning workspace |
| `changes` | `json` | Before/after values for changed fields (nullable) |
| `created_at` | `datetime` | When the action occurred |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET` | `/admin/issues/:id/activity` | Activity log for an issue |
| `GET` | `/admin/workspaces/:id/activity` | Workspace-wide activity feed |

## License

MIT
npm package: @meridianjs/app-role
Description: Meridian app-role module — application-wide custom roles and permissions
Latest version: 2.0.0

# @meridianjs/app-role

Custom RBAC roles module for MeridianJS. Allows administrators to define named roles with arbitrary permission strings, then assign them to users. Permissions flow into the JWT and are available via `req.user.permissions` and the `requirePermission()` guard.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Overview

The built-in roles (`super-admin`, `admin`, `member`) are broad — they grant or restrict access to entire route groups. `AppRole` lets you go further: define custom roles with precise permission strings and guard individual endpoints with `requirePermission()`.

## Service: `appRoleModuleService`

```typescript
const svc = req.scope.resolve("appRoleModuleService") as any
```

### Methods

```typescript
// Get the permission array for a role
const permissions = await svc.getPermissionsForRole(roleId)
// → ["issues:create", "issues:delete", "sprints:manage"]

// Standard CRUD
await svc.listAppRoles(filters?)
await svc.retrieveAppRole(id)
await svc.createAppRole({ name: "Developer", permissions: ["issues:create", "issues:update"] })
await svc.updateAppRole(id, { permissions: [...] })
await svc.deleteAppRole(id)
```

## Assigning Roles to Users

```typescript
// Assign a custom role to a user
const userSvc = container.resolve("userModuleService") as any
await userSvc.updateUser(userId, { app_role_id: roleId })
```

When the user authenticates, their permissions are loaded from the assigned `AppRole` and included in the JWT under `permissions: string[]`.

## Guarding Routes

```typescript
import { requirePermission } from "@meridianjs/auth"
import { authenticateJWT } from "@meridianjs/auth"

export const DELETE = [
  authenticateJWT,
  requirePermission("issues:delete"),
  async (req: any, res: Response) => {
    // Only users with the "issues:delete" permission reach here
  },
]
```

## Data Model

### AppRole

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `name` | `text` | Display name (e.g. `"Developer"`) |
| `permissions` | `json` | Array of permission strings |
| `created_at` | `datetime` | — |
| `updated_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET/POST` | `/admin/roles` | List / create custom roles |
| `GET/PUT/DELETE` | `/admin/roles/:id` | Get / update / delete a role |
| `POST` | `/admin/users/:id/role` | Assign a role to a user |

## License

MIT
npm package: @meridianjs/email-resend
Description: Resend email provider module for Meridian
Latest version: 1.0.1

# @meridianjs/email-resend

[Resend](https://resend.com) email provider for MeridianJS. Registers an `emailService` in the DI container that implements the `IEmailService` interface.

## Installation

```bash
npm install @meridianjs/email-resend
```

## Configuration

```typescript
// meridian.config.ts
export default defineConfig({
  modules: [
    {
      resolve: "@meridianjs/email-resend",
      options: {
        apiKey:      process.env.RESEND_API_KEY ?? "",
        fromAddress: process.env.EMAIL_FROM ?? "noreply@example.com",
      },
    },
  ],
})
```

Add to your `.env`:

```bash
RESEND_API_KEY=re_your_api_key
EMAIL_FROM=noreply@yourdomain.com
```

## Usage

Resolve `emailService` from the container in a subscriber, route, or job:

```typescript
import type { IEmailService } from "@meridianjs/types"

const emailSvc = container.resolve("emailService") as IEmailService

await emailSvc.send({
  to:      "alice@example.com",
  subject: "You have been added to a workspace",
  html:    "<p>Welcome to <strong>Acme Corp</strong>!</p>",
  text:    "Welcome to Acme Corp!",
})
```

## Notes

- Both `html` and `text` are optional — pass at least one.
- Errors from the Resend API are rethrown as `Error` instances.

## License

MIT
npm package: @meridianjs/email-sendgrid
Description: SendGrid email provider module for Meridian
Latest version: 1.0.1

# @meridianjs/email-sendgrid

[SendGrid](https://sendgrid.com) email provider for MeridianJS. Registers an `emailService` in the DI container that implements the `IEmailService` interface.

## Installation

```bash
npm install @meridianjs/email-sendgrid
```

## Configuration

```typescript
// meridian.config.ts
export default defineConfig({
  modules: [
    {
      resolve: "@meridianjs/email-sendgrid",
      options: {
        apiKey:      process.env.SENDGRID_API_KEY ?? "",
        fromAddress: process.env.EMAIL_FROM ?? "noreply@example.com",
      },
    },
  ],
})
```

Add to your `.env`:

```bash
SENDGRID_API_KEY=SG.your_api_key
EMAIL_FROM=noreply@yourdomain.com
```

## Usage

```typescript
import type { IEmailService } from "@meridianjs/types"

const emailSvc = container.resolve("emailService") as IEmailService

await emailSvc.send({
  to:      "alice@example.com",
  subject: "Sprint completed",
  html:    "<p>Sprint 3 has been completed.</p>",
  text:    "Sprint 3 has been completed.",
})
```

## Notes

- Only one email provider should be registered at a time — all three (`email-resend`, `email-sendgrid`, `email-ses`) register under the same `emailService` token.
- Both `html` and `text` are optional — pass at least one.

## License

MIT
npm package: @meridianjs/email-ses
Description: AWS SES email provider module for Meridian
Latest version: 1.0.1

# @meridianjs/email-ses

[AWS SES](https://aws.amazon.com/ses/) email provider for MeridianJS. Registers an `emailService` in the DI container using the AWS SDK v3.

## Installation

```bash
npm install @meridianjs/email-ses
```

## Configuration

```typescript
// meridian.config.ts
export default defineConfig({
  modules: [
    {
      resolve: "@meridianjs/email-ses",
      options: {
        fromAddress:     process.env.EMAIL_FROM ?? "noreply@example.com",
        region:          process.env.AWS_SES_REGION ?? "us-east-1",
        accessKeyId:     process.env.AWS_SES_ACCESS_KEY_ID ?? "",
        secretAccessKey: process.env.AWS_SES_SECRET_ACCESS_KEY ?? "",
      },
    },
  ],
})
```

Add to your `.env`:

```bash
EMAIL_FROM=noreply@yourdomain.com
AWS_SES_REGION=us-east-1
AWS_SES_ACCESS_KEY_ID=AKIA...
AWS_SES_SECRET_ACCESS_KEY=...
```

If running on an EC2 instance or ECS task with an IAM role attached, you can omit `accessKeyId` and `secretAccessKey` — the SDK will use the instance profile credentials automatically.

## Usage

```typescript
import type { IEmailService } from "@meridianjs/types"

const emailSvc = container.resolve("emailService") as IEmailService

await emailSvc.send({
  to:      "alice@example.com",
  subject: "Welcome to Acme Corp",
  html:    "<p>You've been invited to join <strong>Acme Corp</strong>.</p>",
  text:    "You've been invited to join Acme Corp.",
})
```

## Notes

- Only one email provider should be registered at a time.
- The `fromAddress` must be verified in AWS SES. In sandbox mode, `to` addresses must also be verified.

## License

MIT
npm package: @meridianjs/event-bus-local
Description: Local in-process event bus for Meridian (development/testing)
Latest version: 1.0.1

# @meridianjs/event-bus-local

In-process event bus for MeridianJS, backed by Node.js `EventEmitter`. Designed for local development and single-process deployments.

For production multi-process deployments, use [`@meridianjs/event-bus-redis`](../event-bus-redis) instead.

## Installation

```bash
npm install @meridianjs/event-bus-local
```

## Configuration

Register in `meridian.config.ts`:

```typescript
export default defineConfig({
  modules: [
    { resolve: "@meridianjs/event-bus-local" },
  ],
})
```

No options required. The event bus is registered under the `eventBus` token and used automatically by all domain modules.

## Behaviour

- Events are dispatched via `setImmediate`, making emission non-blocking for the caller while preserving per-event ordering.
- Up to 500 listeners per event name (configurable internally).
- Errors thrown inside a subscriber are caught and logged — they do not crash the process or affect other subscribers.
- `unsubscribe()` removes **all** listeners for the event name (a limitation of anonymous wrapper functions). Use `@meridianjs/event-bus-redis` if you need fine-grained handler removal.

## Switching to Redis in Production

The event bus interface (`IEventBus`) is identical between the two implementations. Switching is a single line change:

```typescript
// development
{ resolve: "@meridianjs/event-bus-local" }

// production
{ resolve: "@meridianjs/event-bus-redis", options: { url: process.env.REDIS_URL } }
```

## Direct Usage

If you need to emit or subscribe programmatically:

```typescript
import type { IEventBus } from "@meridianjs/types"

const eventBus = container.resolve("eventBus") as IEventBus

// Emit
await eventBus.emit({ name: "issue.created", data: { issueId: "..." } })

// Subscribe
eventBus.subscribe("issue.created", async ({ event, container }) => {
  console.log("Issue created:", event.data)
})
```

## License

MIT
npm package: @meridianjs/event-bus-redis
Description: Meridian Redis event bus — BullMQ-backed IEventBus for production
Latest version: 1.0.1

# @meridianjs/event-bus-redis

Production-grade event bus for MeridianJS, backed by [BullMQ](https://docs.bullmq.io/) + Redis. Provides durable, at-least-once, async event delivery across multiple processes or servers.

For local development without Redis, use [`@meridianjs/event-bus-local`](../event-bus-local) instead.

## Installation

```bash
npm install @meridianjs/event-bus-redis
```

## Configuration

Register in `meridian.config.ts`:

```typescript
export default defineConfig({
  modules: [
    {
      resolve: "@meridianjs/event-bus-redis",
      options: { url: process.env.REDIS_URL },
    },
  ],
})
```

| Option | Type | Default | Description |
|---|---|---|---|
| `url` | `string` | — | Redis connection URL (required) |
| `queueName` | `string` | `"meridian:events"` | BullMQ queue name |
| `concurrency` | `number` | `5` | Worker concurrency |

## Behaviour

- All events are published to a single persistent BullMQ queue.
- A worker processes jobs and fans out to all in-process subscribers for the event name.
- Failed jobs are retried up to 3 times with exponential back-off (1 s base).
- Completed jobs are retained for the last 1 000 entries; failed jobs for 500.
- The queue, worker, and Redis connection are all gracefully closed when the application stops.

## Switching from Local

The event bus interface is the same across both implementations:

```typescript
// development
{ resolve: "@meridianjs/event-bus-local" }

// production
{ resolve: "@meridianjs/event-bus-redis", options: { url: process.env.REDIS_URL } }
```

## Direct Usage

```typescript
import type { IEventBus } from "@meridianjs/types"

const eventBus = container.resolve("eventBus") as IEventBus

await eventBus.emit([
  { name: "issue.created",       data: { issueId: "..." } },
  { name: "activity.recorded",   data: { actorId: "..." } },
])
```

## License

MIT
npm package: @meridianjs/google-oauth
Description: Google OAuth 2.0 sign-in module for Meridian
Latest version: 1.0.1

# @meridianjs/google-oauth

Google OAuth 2.0 module for MeridianJS. Adds `GET /auth/google` and `GET /auth/google/callback` routes that authenticate users via Google, then issue a MeridianJS JWT.

## Installation

```bash
npm install @meridianjs/google-oauth
```

## Configuration

```typescript
// meridian.config.ts
export default defineConfig({
  modules: [
    {
      resolve: "@meridianjs/google-oauth",
      options: {
        clientId:     process.env.GOOGLE_CLIENT_ID ?? "",
        clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
        callbackUrl:  process.env.GOOGLE_REDIRECT_URI ?? "",
        frontendUrl:  process.env.APP_URL,
      },
    },
  ],
})
```

Add to your `.env`:

```bash
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:9000/auth/google/callback
APP_URL=http://localhost:5174
```

`GOOGLE_REDIRECT_URI` must also be registered as an **Authorised redirect URI** in the [Google Cloud Console](https://console.cloud.google.com/) for your OAuth 2.0 client.

## How It Works

1. User visits `GET /auth/google` → redirected to Google consent screen.
2. After consent, Google redirects to `GOOGLE_REDIRECT_URI`.
3. The framework exchanges the code for a Google profile.
4. If a user with the matching Google ID exists, they are logged in. Otherwise, a new account is created using the Google profile data.
5. If the callback URL included an `invite` query parameter, the user is automatically added to the invited workspace.
6. A MeridianJS JWT is issued and the user is redirected to `APP_URL` with the token in the query string.

## Production

In production, update your `.env` with the public URLs:

```bash
GOOGLE_REDIRECT_URI=https://api.yourdomain.com/auth/google/callback
APP_URL=https://app.yourdomain.com
```

And update the authorised redirect URIs in the Google Cloud Console to match.

## License

MIT
npm package: @meridianjs/invitation
Description: Meridian invitation module — workspace invitation management
Latest version: 2.0.0

# @meridianjs/invitation

Workspace invitation module for MeridianJS. Generates secure token-based invites that allow new or existing users to join a workspace with a pre-assigned role.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## How Invitations Work

1. An admin calls `POST /admin/workspaces/:id/invitations` with an email and role.
2. The framework creates an `Invitation` record with a UUID token and sends a link to the invitee.
3. The invitee visits the link and calls `POST /auth/invite/:token`.
4. If the email is new, an account is created. If it already exists, the user is logged in.
5. A `WorkspaceMember` record is created with the invited role.
6. The invitation status is set to `"accepted"`.

## Service: `invitationModuleService`

```typescript
const svc = req.scope.resolve("invitationModuleService") as any
```

### Methods

```typescript
// Create an invitation with an auto-generated UUID token
const invite = await svc.createInvitationWithToken({
  workspace_id:   workspaceId,
  email:          "bob@example.com",  // optional — omit for open link invitations
  role:           "member",           // "super-admin" | "admin" | "member"
  app_role_id:    null,               // optional: assign a custom AppRole on join
  created_by:     req.user.id,
  expires_in_days: 7,                 // default: 7
})

// Revoke an invitation (sets status → "revoked")
await svc.revokeInvitation(inviteId)

// Standard CRUD
await svc.listInvitations(filters?)
await svc.retrieveInvitation(id)
```

## Data Model

### Invitation

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `workspace_id` | `text` | Target workspace |
| `email` | `text` | Invitee email (nullable for open links) |
| `role` | `text` | Role assigned on acceptance |
| `app_role_id` | `text` | Optional custom RBAC role |
| `token` | `text` | Unique UUID token (in the invite URL) |
| `status` | `text` | `"pending"` \| `"accepted"` \| `"revoked"` |
| `expires_at` | `datetime` | Expiry timestamp |
| `created_by` | `text` | Inviting user's ID |
| `created_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `POST` | `/admin/workspaces/:id/invitations` | Create and send an invitation |
| `GET` | `/auth/invite/:token` | Validate a token, return invite info |
| `POST` | `/auth/invite/:token` | Accept an invitation |

## License

MIT
npm package: @meridianjs/job-queue-local
Description: Meridian local job queue — in-process scheduler for development
Latest version: 1.0.1

# @meridianjs/job-queue-local

In-process cron scheduler for MeridianJS, backed by [`node-cron`](https://github.com/node-cron/node-cron). Designed for local development and testing.

Jobs are registered in-memory and lost on process restart. For production use, replace this with [`@meridianjs/job-queue-redis`](../job-queue-redis).

## Installation

```bash
npm install @meridianjs/job-queue-local
```

## Configuration

Register in `meridian.config.ts`:

```typescript
export default defineConfig({
  modules: [
    { resolve: "@meridianjs/job-queue-local" },
  ],
})
```

No options required.

## Writing Jobs

Place job files under `src/jobs/`. Each file exports a default handler and a `config` object:

```typescript
// src/jobs/send-reminders.ts
import type { JobConfig } from "@meridianjs/types"

export default async function handler({ container }: { container: any }) {
  const logger = container.resolve("logger") as any
  logger.info("Running reminder job")
  // ... your logic here
}

export const config: JobConfig = {
  name: "send-reminders",
  schedule: "0 9 * * *",  // every day at 9 AM (cron expression)
}
```

Interval-based scheduling is also supported:

```typescript
export const config: JobConfig = {
  name: "cleanup",
  schedule: { interval: 60_000 },  // every 60 seconds
}
```

The framework auto-loads all files in `src/jobs/` at startup.

## Switching to Redis in Production

```typescript
// development
{ resolve: "@meridianjs/job-queue-local" }

// production
{ resolve: "@meridianjs/job-queue-redis", options: { url: process.env.REDIS_URL } }
```

## License

MIT
npm package: @meridianjs/job-queue-redis
Description: Meridian job queue — BullMQ-backed IScheduler for cron and interval jobs
Latest version: 1.0.1

# @meridianjs/job-queue-redis

Production-grade cron scheduler for MeridianJS, backed by [BullMQ](https://docs.bullmq.io/) + Redis. Jobs persist across restarts, run on a distributed schedule, and are retried automatically on failure.

For local development without Redis, use [`@meridianjs/job-queue-local`](../job-queue-local) instead.

## Installation

```bash
npm install @meridianjs/job-queue-redis
```

## Configuration

Register in `meridian.config.ts`:

```typescript
export default defineConfig({
  modules: [
    {
      resolve: "@meridianjs/job-queue-redis",
      options: { url: process.env.REDIS_URL },
    },
  ],
})
```

| Option | Type | Default | Description |
|---|---|---|---|
| `url` | `string` | — | Redis connection URL (required) |
| `prefix` | `string` | `"meridian:job"` | Prefix for all BullMQ queue names |

## Writing Jobs

Jobs are identical in both the local and Redis implementations — no code changes needed when switching:

```typescript
// src/jobs/daily-summary.ts
import type { JobConfig } from "@meridianjs/types"

export default async function handler({ container }: { container: any }) {
  const logger = container.resolve("logger") as any
  logger.info("Sending daily summary emails")
  // ... your logic here
}

export const config: JobConfig = {
  name: "daily-summary",
  schedule: "0 8 * * *",  // every day at 8 AM
}
```

Each job gets its own named BullMQ queue (`meridian:job:<name>`). On restart, existing repeatable jobs are cleaned up and re-registered to prevent duplicates.

## Behaviour

- Each job runs with `concurrency: 1` — jobs of the same name run sequentially.
- Completed jobs are retained for the last 100 entries; failed jobs for 50.
- Worker and queue connections are gracefully closed on `SIGTERM` / `SIGINT`.

## Switching from Local

```typescript
// development
{ resolve: "@meridianjs/job-queue-local" }

// production
{ resolve: "@meridianjs/job-queue-redis", options: { url: process.env.REDIS_URL } }
```

## License

MIT
npm package: @meridianjs/notification
Description: Meridian notification module — in-app notification records
Latest version: 2.0.0

# @meridianjs/notification

In-app notification module for MeridianJS. Creates notification records when domain events occur and exposes an API for reading and marking them as read.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## How Notifications Are Created

`@meridianjs/meridian` includes event subscribers that listen for domain events and call `notificationModuleService.createNotification()` automatically:

| Domain Event | Notification |
|---|---|
| `issue.created` | Notify workspace members |
| `issue.assigned` | Notify the assigned user |
| `issue.status_changed` | Notify the reporter / assignees |
| `comment.created` | Notify the issue's participants |
| `sprint.completed` | Notify project members |

## Service: `notificationModuleService`

```typescript
const svc = req.scope.resolve("notificationModuleService") as any
```

### Methods

```typescript
// Create a notification for a user
await svc.createNotification({
  user_id:      userId,
  entity_type:  "issue",
  entity_id:    issueId,
  action:       "assigned",
  message:      "You were assigned to SITE-42",
  workspace_id: workspaceId,
  metadata:     { issueTitle: "Fix login crash" },  // optional
})

// Standard CRUD
await svc.listNotifications(filters?)
await svc.listAndCountNotifications(filters?)
await svc.retrieveNotification(id)
await svc.updateNotification(id, data)
await svc.deleteNotification(id)
```

## Data Model

### Notification

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `user_id` | `text` | Recipient user ID |
| `entity_type` | `text` | Entity kind (e.g. `"issue"`, `"comment"`) |
| `entity_id` | `text` | ID of the related entity |
| `action` | `text` | Action key (e.g. `"assigned"`, `"commented"`) |
| `message` | `text` | Human-readable notification text (nullable) |
| `read` | `boolean` | Whether the user has read it |
| `workspace_id` | `text` | Owning workspace |
| `metadata` | `json` | Additional context (nullable) |
| `created_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET` | `/admin/notifications` | List notifications for the current user |
| `PATCH` | `/admin/notifications/:id/read` | Mark a notification as read |
| `POST` | `/admin/notifications/read-all` | Mark all notifications as read |

## License

MIT
npm package: @meridianjs/org-calendar
Description: Meridian org-calendar module — org-level working days and holidays
Latest version: 1.0.1

# @meridianjs/org-calendar

Organisation calendar module for MeridianJS. Stores the organisation's working-day schedule and public holidays. Used by the Gantt chart to calculate business-day durations and display non-working days.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Service: `orgCalendarModuleService`

```typescript
const svc = req.scope.resolve("orgCalendarModuleService") as any
```

### Methods

```typescript
// Get or create the singleton calendar record
const calendar = await svc.getOrCreateCalendar()
// → { id, working_days: { mon, tue, wed, thu, fri, sat, sun }, timezone }

// Update working days
await svc.updateOrgCalendar(calendar.id, {
  working_days: { mon: true, tue: true, wed: true, thu: true, fri: true, sat: false, sun: false },
  timezone: "America/New_York",
})

// List holidays for a given year (includes recurring holidays from any year)
const holidays = await svc.listHolidaysByYear(2025)

// Standard CRUD
await svc.createOrgHoliday({ name: "New Year", date: new Date("2025-01-01"), recurring: true })
await svc.updateOrgHoliday(id, data)
await svc.deleteOrgHoliday(id)
```

## Default Working Days

On first access, the calendar is seeded with:

```
Mon – Fri: working  ✓
Sat – Sun: off      ✗
```

## Data Models

### OrgCalendar

Singleton record — only one per organisation.

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `working_days` | `json` | Map of `{ mon, tue, wed, thu, fri, sat, sun }` booleans |
| `timezone` | `text` | IANA timezone string (nullable) |
| `created_at` | `datetime` | — |
| `updated_at` | `datetime` | — |

### OrgHoliday

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `name` | `text` | Holiday name (e.g. `"Christmas"`) |
| `date` | `datetime` | Date of the holiday |
| `recurring` | `boolean` | `true` = repeats every year on same month/day |
| `created_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET/PUT` | `/admin/org/calendar` | Get / update the working-day schedule |
| `GET/POST` | `/admin/org/holidays` | List / create holidays |
| `PUT/DELETE` | `/admin/org/holidays/:id` | Update / delete a holiday |

## License

MIT
npm package: @meridianjs/project-member
Description: Meridian project-member module — project access control via users and teams
Latest version: 2.0.0

# @meridianjs/project-member

Project access control module for MeridianJS. Manages which users and teams have access to individual projects, enabling fine-grained, project-level permissions on top of workspace membership.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Overview

Access to a project is determined by two tables:

- **`ProjectMember`** — a direct user ↔ project grant
- **`ProjectTeam`** — a team ↔ project grant (all team members inherit access)

`admin` and `super-admin` roles bypass both checks and see all projects.

## Service: `projectMemberModuleService`

```typescript
const svc = req.scope.resolve("projectMemberModuleService") as any
```

### Methods

```typescript
// Get all project IDs accessible to a user (direct + via teams)
const projectIds = await svc.getAccessibleProjectIds(userId, userTeamIds)

// List all direct members of a project
const members = await svc.listProjectMembers(projectId)
// → [{ id, user_id, role }]

// List all teams with access to a project
const teams = await svc.listProjectTeamIds(projectId)
// → [{ id, team_id }]

// Standard CRUD
await svc.createProjectMember({ project_id, user_id, role })
await svc.deleteProjectMember(id)
await svc.createProjectTeam({ project_id, team_id })
await svc.deleteProjectTeam(id)
```

### Access Filtering in Route Handlers

```typescript
const roles     = req.user?.roles ?? []
const isPrivileged = roles.includes("admin") || roles.includes("super-admin")

if (isPrivileged) {
  const projects = await projectSvc.listProjectsByWorkspace(workspaceId)
} else {
  const teamIds    = await teamMemberSvc.getUserTeamIds(req.user.id)
  const projectIds = await projectMemberSvc.getAccessibleProjectIds(req.user.id, teamIds)
  const projects   = await projectSvc.listProjects({ id: projectIds, workspace_id: workspaceId })
}
```

## Data Models

### ProjectMember

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `project_id` | `text` | Project |
| `user_id` | `text` | User |
| `role` | `text` | `"manager"` \| `"member"` \| `"viewer"` |
| `created_at` | `datetime` | — |

### ProjectTeam

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `project_id` | `text` | Project |
| `team_id` | `text` | Team |
| `created_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET` | `/admin/projects/:id/access` | Enriched member + team list |
| `POST` | `/admin/projects/:id/members` | Add a user to a project |
| `DELETE` | `/admin/projects/:id/members/:userId` | Remove a user |
| `POST` | `/admin/projects/:id/teams` | Grant a team access |
| `DELETE` | `/admin/projects/:id/teams/:teamId` | Revoke team access |

## License

MIT
npm package: @meridianjs/sprint
Description: Meridian sprint module — Sprint/Cycle management
Latest version: 1.0.1

# @meridianjs/sprint

Sprint (cycle) module for MeridianJS. Manages time-boxed sprints within projects, with explicit lifecycle transitions: `planned → active → completed`.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Service: `sprintModuleService`

```typescript
const svc = req.scope.resolve("sprintModuleService") as any
```

### Auto-generated CRUD

```typescript
await svc.listSprints(filters?, options?)
await svc.listAndCountSprints(filters?, options?)
await svc.retrieveSprint(id)
await svc.createSprint(data)
await svc.updateSprint(id, data)
await svc.deleteSprint(id)
```

### Custom Methods

```typescript
// List all sprints for a project
const sprints = await svc.listSprintsByProject(projectId)

// Start a sprint — transitions status: "planned" → "active"
// Sets start_date to now if not already set
// Throws 409 if already active or completed
const sprint = await svc.startSprint(sprintId)

// Complete a sprint — transitions status: "active" → "completed"
// Sets end_date to now if not already set
// Throws 409 if not active
const sprint = await svc.completeSprint(sprintId)
```

The `completeSprintWorkflow` (from `@meridianjs/meridian`) runs `completeSprint()` and moves any unfinished issues back to the backlog in one atomic operation.

## Data Model

### Sprint

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `name` | `text` | Sprint name (e.g. `"Sprint 1"`) |
| `project_id` | `text` | Owning project |
| `workspace_id` | `text` | Owning workspace |
| `status` | `text` | `"planned"` \| `"active"` \| `"completed"` |
| `goal` | `text` | Optional sprint goal description |
| `start_date` | `datetime` | Planned start (nullable) |
| `end_date` | `datetime` | Planned end (nullable) |
| `created_at` | `datetime` | — |
| `updated_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET/POST` | `/admin/projects/:id/sprints` | List / create sprints |
| `GET/PUT/DELETE` | `/admin/projects/:id/sprints/:sprintId` | Get / update / delete sprint |
| `POST` | `/admin/projects/:id/sprints/:sprintId/start` | Start a sprint |
| `POST` | `/admin/projects/:id/sprints/:sprintId/complete` | Complete a sprint |

## License

MIT
npm package: @meridianjs/storage-s3
Description: S3-compatible storage module for Meridian file uploads
Latest version: 1.0.1

# @meridianjs/storage-s3

AWS S3 (and S3-compatible) file storage provider for MeridianJS. Used for issue attachment uploads. Registers a `storageProvider` in the DI container that implements the `IStorageProvider` interface.

## Installation

```bash
npm install @meridianjs/storage-s3
```

## Configuration

```typescript
// meridian.config.ts
export default defineConfig({
  modules: [
    {
      resolve: "@meridianjs/storage-s3",
      options: {
        bucket:          process.env.AWS_S3_BUCKET ?? "",
        region:          process.env.AWS_S3_REGION ?? "us-east-1",
        accessKeyId:     process.env.AWS_S3_ACCESS_KEY_ID ?? "",
        secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY ?? "",
        // cloudfrontUrl: process.env.AWS_CLOUDFRONT_URL,  // optional — for CDN URLs
        // endpoint:      process.env.AWS_S3_ENDPOINT,     // optional — for R2 / MinIO
      },
    },
  ],
})
```

Add to your `.env`:

```bash
AWS_S3_BUCKET=my-meridian-uploads
AWS_S3_REGION=us-east-1
AWS_S3_ACCESS_KEY_ID=AKIA...
AWS_S3_SECRET_ACCESS_KEY=...
```

### Cloudflare R2 / MinIO

Set `endpoint` to your custom S3-compatible endpoint:

```bash
AWS_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
```

## Usage

The framework uses `storageProvider` automatically when handling file uploads. You can also use it directly:

```typescript
import type { IStorageProvider } from "@meridianjs/types"

const storage = container.resolve("storageProvider") as IStorageProvider

// Upload a file
const { url, key } = await storage.upload(
  {
    buffer:       fileBuffer,
    originalname: "screenshot.png",
    mimetype:     "image/png",
    size:         fileBuffer.length,
  },
  "issue-attachments"  // sub-directory within the bucket
)

// Delete a file by its URL or storage key
await storage.delete(url)
```

## Notes

- If `cloudfrontUrl` is set, returned `url` values use the CloudFront domain instead of the S3 URL.
- `delete()` accepts both the full URL and the storage key.
- When running on EC2/ECS with an IAM role, you can omit `accessKeyId` and `secretAccessKey`.

## License

MIT
npm package: @meridianjs/team-member
Description: Meridian team-member module — tracks team membership
Latest version: 2.0.0

# @meridianjs/team-member

Team membership module for MeridianJS. Tracks which users belong to which teams. Teams are workspace-scoped and can be granted access to projects as a group via `@meridianjs/project-member`.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Service: `teamMemberModuleService`

```typescript
const svc = req.scope.resolve("teamMemberModuleService") as any
```

### Methods

```typescript
// Get all user IDs in a team
const userIds = await svc.getTeamMemberUserIds(teamId)

// Get all team IDs a user belongs to
const teamIds = await svc.getUserTeamIds(userId)

// Check membership
const isMember = await svc.isMember(teamId, userId)  // → boolean

// Add a user to a team if not already a member
await svc.ensureMember(teamId, userId)

// Remove a user from a team by team + user ID
await svc.removeByTeamAndUser(teamId, userId)

// Standard CRUD
await svc.listTeamMembers(filters?)
await svc.createTeamMember({ team_id, user_id })
await svc.deleteTeamMember(id)
```

## Data Model

### TeamMember

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `team_id` | `text` | Team (from `@meridianjs/user`) |
| `user_id` | `text` | User |
| `created_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET/POST` | `/admin/workspaces/:id/teams/:teamId/members` | List / add team members |
| `DELETE` | `/admin/workspaces/:id/teams/:teamId/members/:userId` | Remove member from team |

## License

MIT
npm package: @meridianjs/types
Description: Shared TypeScript types and interfaces for Meridian
Latest version: 2.5.0

# @meridianjs/types

Shared TypeScript interfaces and types for the MeridianJS ecosystem. This package is a pure type declaration library — no runtime code, no dependencies.

## Installation

```bash
npm install @meridianjs/types
```

## Overview

All public contracts between MeridianJS packages live here. Consuming packages import from `@meridianjs/types` rather than from each other, keeping the dependency graph flat and preventing circular imports.

## Exported Types

### Container & DI

```typescript
import type { MeridianContainer } from "@meridianjs/types"

// The DI container interface — resolve tokens, register values, create child scopes
interface MeridianContainer {
  resolve<T = unknown>(token: string): T
  register(registrations: Record<string, unknown>): void
  createScope(): MeridianContainer
  dispose?(): Promise<void>
}
```

### Module System

```typescript
import type { ModuleDefinition, LoaderFn, LoaderOptions, LinkableConfig } from "@meridianjs/types"
```

### Configuration

```typescript
import type { MeridianConfig, ProjectConfig, ModuleConfig, PluginConfig } from "@meridianjs/types"
```

### Event Bus

```typescript
import type { IEventBus, EventMessage, SubscriberFn, SubscriberArgs, SubscriberConfig } from "@meridianjs/types"
```

### Scheduler

```typescript
import type { IScheduler, ScheduledJobConfig } from "@meridianjs/types"
```

### HTTP / Auth

```typescript
import type { AuthenticatedUser, MeridianRequestBase } from "@meridianjs/types"

// JWT payload shape attached to req.user
interface AuthenticatedUser {
  id: string
  workspaceId: string
  roles: string[]
  permissions: string[]
}
```

### Logger

```typescript
import type { ILogger } from "@meridianjs/types"
```

### Domain Enums

```typescript
import type {
  IssueType,       // "bug" | "feature" | "task" | "epic" | "story"
  IssuePriority,   // "urgent" | "high" | "medium" | "low" | "none"
  SprintStatus,    // "planned" | "active" | "completed"
  ProjectVisibility,
  WorkspacePlan,
} from "@meridianjs/types"
```

### Storage & Email

```typescript
import type { IStorageProvider, IEmailService, EmailSendOptions } from "@meridianjs/types"
```

### Plugin System

```typescript
import type { PluginRegistrationContext, PluginRegisterFn } from "@meridianjs/types"
```

## License

MIT
npm package: @meridianjs/user
Description: Meridian user module — User and Team domain models
Latest version: 2.0.0

# @meridianjs/user

User, Team, and session management module for MeridianJS. Provides the `User` and `Team` data models, auto-generated CRUD, and custom helpers for email/Google ID lookups, login tracking, and session management.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Service: `userModuleService`

Resolve from the DI container in a route handler:

```typescript
const svc = req.scope.resolve("userModuleService") as any
```

### Auto-generated CRUD

```typescript
// Users
await svc.listUsers(filters?, options?)
await svc.listAndCountUsers(filters?, options?)
await svc.retrieveUser(id)
await svc.createUser(data)
await svc.updateUser(id, data)
await svc.deleteUser(id)
await svc.softDeleteUser(id)

// Teams
await svc.listTeams(filters?, options?)
await svc.retrieveTeam(id)
await svc.createTeam(data)
await svc.updateTeam(id, data)
await svc.deleteTeam(id)
```

### Custom Methods

```typescript
// Find a user by email address (returns null if not found)
const user = await svc.retrieveUserByEmail("alice@example.com")

// Find a user by their Google OAuth ID
const user = await svc.retrieveUserByGoogleId(googleId)

// Update last_login_at timestamp
await svc.recordLogin(userId)

// Deactivate an account (sets is_active: false)
await svc.deactivateUser(userId)

// Total registered user count
const count = await svc.countUsers()

// Session management (for JWT revocation)
await svc.createSession(jti, userId, expiresAt)
await svc.retrieveValidSession(jti)
await svc.revokeSession(jti)
```

## Data Models

### User

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `email` | `text` | Unique email address |
| `password_hash` | `text` | bcrypt hash (nullable for OAuth users) |
| `first_name` | `text` | — |
| `last_name` | `text` | — |
| `google_id` | `text` | Google OAuth ID (nullable) |
| `picture` | `text` | Profile picture URL (nullable) |
| `is_active` | `boolean` | Account active flag |
| `last_login_at` | `datetime` | Last successful login |
| `app_role_id` | `text` | Optional custom RBAC role |
| `created_at` | `datetime` | — |
| `updated_at` | `datetime` | — |

### Team

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `name` | `text` | Team name |
| `workspace_id` | `text` | Owning workspace |
| `created_at` | `datetime` | — |

## License

MIT
npm package: @meridianjs/workspace
Description: Meridian workspace module — multi-tenant workspace management
Latest version: 1.2.1

# @meridianjs/workspace

Workspace module for MeridianJS. Provides the `Workspace` model and service for multi-tenant project management, including slug generation and lookup.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Service: `workspaceModuleService`

```typescript
const svc = req.scope.resolve("workspaceModuleService") as any
```

### Auto-generated CRUD

```typescript
await svc.listWorkspaces(filters?, options?)
await svc.listAndCountWorkspaces(filters?, options?)
await svc.retrieveWorkspace(id)
await svc.createWorkspace(data)
await svc.updateWorkspace(id, data)
await svc.deleteWorkspace(id)
await svc.softDeleteWorkspace(id)
```

### Custom Methods

```typescript
// Find a workspace by its URL slug (returns null if not found)
const workspace = await svc.retrieveWorkspaceBySlug("acme-corp")

// Generate a URL-safe slug from a workspace name
const slug = svc.generateSlug("Acme Corp")  // → "acme-corp"
```

## Data Model

### Workspace

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `name` | `text` | Display name |
| `slug` | `text` | URL-safe identifier (unique) |
| `plan` | `enum` | `"free"` \| `"pro"` \| `"enterprise"` |
| `logo_url` | `text` | Workspace logo (nullable) |
| `created_at` | `datetime` | — |
| `updated_at` | `datetime` | — |

## API Routes

Provided by `@meridianjs/meridian`:

| Method | Path | Description |
|---|---|---|
| `GET` | `/admin/workspaces` | List workspaces (filtered by membership for non-admins) |
| `POST` | `/admin/workspaces` | Create workspace (auto-adds creator as admin member) |
| `GET` | `/admin/workspaces/:id` | Get workspace |
| `PUT` | `/admin/workspaces/:id` | Update workspace |
| `DELETE` | `/admin/workspaces/:id` | Delete workspace |

## License

MIT
npm package: @meridianjs/workspace-member
Description: Meridian workspace-member module — tracks workspace membership and roles
Latest version: 2.0.0

# @meridianjs/workspace-member

Workspace membership module for MeridianJS. Tracks which users belong to which workspaces and their workspace-level role (`admin` or `member`). Used by route handlers to enforce workspace isolation.

Auto-loaded by `@meridianjs/meridian` — you do not need to add this to `modules[]` yourself.

## Service: `workspaceMemberModuleService`

```typescript
const svc = req.scope.resolve("workspaceMemberModuleService") as any
```

### Methods

```typescript
// Get all workspace IDs a user belongs to
const workspaceIds = await svc.getWorkspaceIdsForUser(userId)

// Get a specific membership record (returns null if not a member)
const membership = await svc.getMembership(workspaceId, userId)

// Check if a user is a member of a workspace
const isMember = await svc.isMember(workspaceId, userId)  // → boolean

// Add a user to a workspace if not already a member
await svc.ensureMember(workspaceId, userId, "member")

// Standard CRUD
await svc.listWorkspaceMembers(filters?)
await svc.createWorkspaceMember({ workspace_id, user_id, role })
await svc.updateWorkspaceMember(id, { role })
await svc.deleteWorkspaceMember(id)
```

## Access Control Pattern

Route handlers use this service to filter data by membership:

```typescript
const roles = req.user?.roles ?? []
const isPrivileged = roles.includes("admin") || roles.includes("super-admin")

if (isPrivileged) {
  // Return all workspaces
  const workspaces = await workspaceSvc.listWorkspaces()
} else {
  // Return only workspaces the user belongs to
  const ids = await memberSvc.getWorkspaceIdsForUser(req.user.id)
  const workspaces = await workspaceSvc.listWorkspaces({ id: ids })
}
```

## Data Model

### WorkspaceMember

| Field | Type | Description |
|---|---|---|
| `id` | `uuid` | Primary key |
| `workspace_id` | `text` | Workspace |
| `user_id` | `text` | User |
| `role` | `text` | `"admin"` \| `"member"` |
| `created_at` | `datetime` | — |

## API Routes

| Method | Path | Description |
|---|---|---|
| `GET/POST` | `/admin/workspaces/:id/members` | List / add members |
| `PATCH` | `/admin/workspaces/:id/members/:userId` | Update member role |
| `DELETE` | `/admin/workspaces/:id/members/:userId` | Remove member |

## License

MIT