diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index de188017..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Bug report 🐞 -description: >- - Something is broken and you have a reliable reproduction? Let us know here. - For questions, please post in GitHub Discussion. -title: "[Bug]: " -type: bug -labels: [] -body: - - type: textarea - id: description - attributes: - label: Bug Description - description: A clear and concise description of what the bug is. - placeholder: Describe what's broken or not working as expected... - validations: - required: true - - - type: textarea - id: repro-steps - attributes: - label: Steps to Reproduce - description: Provide detailed steps to reproduce the issue. - placeholder: | - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - validations: - required: true - - - type: textarea - id: expected-behavior - attributes: - label: Expected Behavior - description: What did you expect to happen? - placeholder: Describe what should happen instead... - validations: - required: true - - - type: textarea - id: actual-behavior - attributes: - label: Actual Behavior - description: What actually happened? Include error messages if applicable. - placeholder: Describe what actually happens, including any error messages... - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 3ba13e0c..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1 +0,0 @@ -blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index 3c01f0c4..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Feature request ✨ -description: >- - Suggest a new feature or enhancement for this project. - For questions, please post in GitHub Discussion. -title: "[Feature]: " -type: feature -labels: [] -body: - - type: textarea - id: problem-statement - attributes: - label: Problem Statement - description: >- - What problem does this feature solve? Describe the pain point or limitation you're experiencing. - Help us understand the "why" behind this request. - placeholder: >- - Example: "Currently, users cannot [action] which means they have to [workaround], causing [impact]." - validations: - required: true - - - type: textarea - id: proposed-solution - attributes: - label: Proposed Solution - description: What would you like to happen? Describe your ideal solution clearly and concisely. - validations: - required: true - - - type: textarea - id: technical-implementation - attributes: - label: Technical Implementation - description: >- - If you have thoughts on implementation, share them here. - You can use the checklist format below or describe in your own words. - placeholder: >- - - [ ] API/endpoint changes needed - - [ ] Database schema modifications required - - [ ] UI/UX changes involved - - [ ] Breaking changes or migration path needed - - [ ] Documentation updates required - - - type: textarea - id: additional-context - attributes: - label: Additional Context - description: >- - Add any mockups, screenshots, code examples, or links to related discussions - that help illustrate this feature. - placeholder: Add supporting information here... - - - type: textarea - id: acceptance-criteria - attributes: - label: Acceptance Criteria - description: How will we know this is complete? List specific, testable outcomes. - placeholder: >- - - [ ] Specific testable outcome - - [ ] Another measurable result diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 25f05bec..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,260 +0,0 @@ -# GitHub Copilot Instructions - -## Project Overview - -This is the Lunogram platform - a multi-service monorepo for customer engagement and messaging. - -### Architecture - -- **cmd/lunogram** - Single binary entry point -- **internal** - Go backend packages (API, embedded console, workers) -- **internal/http/console** - Embedded React frontend (built from `console/`) -- **console** - React TypeScript frontend source (Vite, React Router 7) - -### Build - -The console frontend is built and embedded into the Go binary: - -```bash -make build # Builds WASM modules + console, copies dist to internal/http/console/dist -make console # Builds only the console -``` - -The embedded console is served at the root path (`/`) by the management HTTP server. - -## Code Style & Conventions - -### Go (internal) - -#### General Principles - -- Use `golangci-lint` for linting - follow all configured rules -- Format code with `gofmt` -- Generate code with `make generate` after OpenAPI changes -- Use `sqlx` for database operations with struct tags - -#### API Development - -1. **OpenAPI First**: Always define endpoints in `oapi/resources.yml` before implementation -2. **Use oapi-codegen types**: Never create custom types when generated types exist -3. **Simple schemas**: Prefer simple object types with `additionalProperties` over complex `oneOf` discriminated unions -4. **Flexible data fields**: Use `json.RawMessage` with `x-go-type` for type-specific data - -```yaml -data: - type: object - additionalProperties: true - x-go-type: json.RawMessage -``` - -5. **Derive relationships**: Get type/state from parent resources rather than duplicating in request bodies -6. **Clean up unused schemas**: Remove schemas that aren't referenced -7. **Required vs nullable**: Use the `required` array to specify required fields instead of `nullable: true` for optional fields. Only use `nullable: true` when a field can explicitly be set to `null` - -```yaml -# ✅ Good - use required array -properties: - name: - type: string - description: - type: string -required: - - name - -# ❌ Bad - don't use nullable for optional fields -properties: - name: - type: string - description: - type: string - nullable: true -``` - -#### Testing - -- Write tests in `_test.go` files alongside implementation -- Use `testify` for assertions -- Use unexported field names for test types (lowercase) -- Use `testcontainers` for database integration tests -- Follow table-driven test patterns -- Include both success and error cases -- Test status codes and response structure -- Avoid obvious comments - code should be self-explanatory -- **Always use named test types** instead of anonymous structs in test tables: - -```go -// ✅ Good -type test struct { - input string - expected int - code int -} - -tests := map[string]test{ - "success": { - input: "hello", - expected: 5, - code: 200, - }, -} - -// ❌ Bad -tests := map[string]struct{ - input string - expected int -}{ - "success": { - input: "hello", - expected: 5, - }, -} -``` - -#### Store Layer - -- Separate concerns: controllers handle HTTP, stores handle database -- Use transactions for multi-step operations -- Implement soft deletes with `deleted_at` timestamp -- Always filter by `project_id` for multi-tenant isolation -- Use `COUNT(*) OVER ()` for pagination total counts - -```go -SELECT id, name, COUNT(*) OVER () AS total_count -FROM campaigns -WHERE project_id = $1 AND deleted_at IS NULL -LIMIT $2 OFFSET $3 -``` - -#### Controllers - -- Log all operations with structured logging (zap) -- Check resource existence before operations -- Return appropriate HTTP status codes: - - 200 OK - successful GET/PATCH - - 201 Created - successful POST - - 204 No Content - successful DELETE - - 404 Not Found - resource doesn't exist - - 500 Internal Server Error - unexpected errors -- Use `problem` package for consistent error responses -- Validate input using generated OAPI types - -### TypeScript (console) - -- Use TypeScript strictly - no `any` types -- Follow React hooks best practices -- Use React Router 7 loaders for data fetching -- Keep API client (`api.ts`) in sync with backend OpenAPI spec -- Update types when backend changes - -### Database - -- Always use migrations (internal/store/migrations) -- Use UUIDs for primary keys -- Include `created_at`, `updated_at` timestamps -- Use `deleted_at` for soft deletes -- Create proper indexes for foreign keys and frequently queried columns - -## Workflow - -### Adding New Endpoints - -1. Define in `oapi/resources.yml` (OpenAPI spec) -2. Run `make generate` to generate types -3. Implement store methods if needed -4. Implement controller handler -5. Write tests with named test types -6. Update frontend API client and types -7. Verify all tests pass - -### Making OpenAPI Changes - -1. Edit `oapi/resources.yml` -2. Run `make generate` -3. Update controller implementations -4. Update tests -5. Check for errors with `go build` - -### Testing - -```bash -# Run all tests -make test - -# Run specific package tests -go test ./internal/http/controllers/v1/... -v - -# Run specific test -go test ./internal/http/controllers/v1/... -v -run TestCampaignCreation -``` - -### Verifying Compilation - -When verifying that code compiles after making changes, use the linter which checks compilation without creating build artifacts: - -```bash -# ✅ Good - runs linters and verifies compilation -make lint - -# ❌ Bad - leaves binary in directory -go build -``` - -The linter will catch compilation errors and style issues without creating binary files that could be accidentally committed to git. - -## Common Patterns - -### Resource Nesting - -Templates are nested under campaigns: -- ✅ `POST /projects/{projectID}/campaigns/{campaignID}/templates` -- ❌ `POST /projects/{projectID}/templates` (with campaign_id in body) - -### Type Derivation - -Derive types from parent resources instead of request bodies: - -```go -// Template type is derived from campaign.Channel -campaign, _ := store.GetCampaign(ctx, projectID, campaignID) -templateType := campaign.Channel // "email", "sms", "push" -``` - -### Pagination - -Always include pagination parameters: - -```yaml -parameters: - - $ref: '#/components/parameters/Limit' - - $ref: '#/components/parameters/Offset' -``` - -Return pagination metadata: - -```json -{ - "data": [...], - "total": 42, - "limit": 20, - "offset": 0 -} -``` - -## Don't Do - -- ❌ Don't use anonymous types in test tables -- ❌ Don't create endpoints that bypass resource hierarchy -- ❌ Don't skip migrations for schema changes -- ❌ Don't use discriminated unions if simple objects work -- ❌ Don't duplicate parent resource data in child creation -- ❌ Don't forget to run `make generate` after OpenAPI changes -- ❌ Don't hardcode UUIDs in tests - use generated ones -- ❌ Don't expose internal errors to API responses -- ❌ Don't add obvious comments explaining what code does - write self-documenting code instead - -## References - -- OpenAPI Spec: `oapi/resources.yml` -- Database Schema: `internal/store/migrations/` -- API Client: `console/src/api.ts` -- Example Tests: `internal/http/controllers/v1/*_test.go` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18939e57..1728d403 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,24 @@ jobs: - name: Build Console run: cd console && pnpm build + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v4 + with: + version: ${{ vars.PNPM_VERSION }} + - name: Set-up Node + uses: actions/setup-node@v5 + with: + node-version: ${{ vars.NODEJS_VERSION }} + cache: "pnpm" + cache-dependency-path: docs/pnpm-lock.yaml + - name: Install Dependencies + run: cd docs && pnpm install + - name: Build Docs + run: cd docs && pnpm build + generate: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 00000000..9b67b61c --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,49 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +permissions: + actions: write + contents: read + pull-requests: write + statuses: write + +jobs: + CLAssistant: + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.SIGNBOT9000_APP_ID }} + private-key: ${{ secrets.SIGNBOT9000_SECRET }} + owner: lunogram + repositories: cla + + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: cla-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ steps.app-token.outputs.token }} + with: + path-to-signatures: "signatures.json" + path-to-document: "https://github.com/lunogram/cla/blob/main/CLA.md" # e.g. a CLA or a DCO document + # branch should not be protected + branch: "main" + allowlist: jeroenrinzema,Copilot,copilot,bot*,*[bot] + + #below are the optional inputs - If the optional inputs are not given, then default values will be taken + remote-organization-name: lunogram + remote-repository-name: cla + #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' + #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' + #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' + #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' + #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' + #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3e5c6640..e0b4cd32 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,65 +1,52 @@ -# Workflow disabled - blog is unused atm -# name: Docs -# -# on: -# push: -# branches: -# - main -# -# permissions: -# contents: read -# pages: write -# id-token: write -# -# concurrency: -# group: "pages" -# cancel-in-progress: true -# -# jobs: -# build: -# name: Build for GitHub Pages -# runs-on: ubuntu-latest -# defaults: -# run: -# working-directory: ./docs -# steps: -# - uses: actions/checkout@v5 -# - uses: pnpm/action-setup@v4 -# with: -# version: ${{ vars.PNPM_VERSION }} -# - name: Set-up Node -# uses: actions/setup-node@v5 -# with: -# node-version: ${{ vars.NODEJS_VERSION }} -# cache: 'pnpm' -# - name: Install Dependencies -# run: pnpm install +name: Docs - # - name: Setup Pages - # uses: actions/configure-pages@v3 - # - uses: actions/setup-node@v3 - # with: - # node-version: 18 - # cache: npm - # - name: Install dependencies - # run: npm ci - # - name: Build website - # run: npm run build - # - name: List files - # run: ls - # - name: Upload artifact - # uses: actions/upload-pages-artifact@v3 - # with: - # path: docs/build +on: + push: + branches: + - main + paths: + - "docs/**" + workflow_dispatch: - # deploy: - # name: Deploy to GitHub Pages - # environment: - # name: github-pages - # url: ${{ steps.deployment.outputs.page_url }} - # runs-on: ubuntu-latest - # needs: build - # steps: - # - name: Deploy to GitHub Pages - # id: deployment - # uses: actions/deploy-pages@v4 \ No newline at end of file +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: pnpm/action-setup@v4 + with: + version: ${{ vars.PNPM_VERSION }} + - name: Set-up Node + uses: actions/setup-node@v5 + with: + node-version: ${{ vars.NODEJS_VERSION }} + cache: "pnpm" + cache-dependency-path: docs/pnpm-lock.yaml + - name: Install Dependencies + run: cd docs && pnpm install + - name: Build Docs + run: cd docs && pnpm build + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/out + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index db55a296..09d1e500 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,24 +7,122 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} + +permissions: + contents: write + packages: write jobs: - build-and-push: + build-assets: runs-on: ubuntu-latest - permissions: - contents: read - packages: write + steps: + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: ${{ vars.GO_VERSION }} + + - name: Set up TinyGo + uses: acifani/setup-tinygo@v2 + with: + tinygo-version: ${{ vars.TINYGO_VERSION }} + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: ${{ vars.NODEJS_VERSION }} + + - name: Set up pnpm + uses: pnpm/action-setup@v5 + with: + version: ${{ vars.PNPM_VERSION }} + - name: Build WASM modules + run: make modules + + - name: Build console + run: | + cd console && pnpm install --frozen-lockfile && pnpm build + cd .. + rm -rf internal/http/console/dist/* + cp -r console/dist/* internal/http/console/dist/ + + - name: Upload WASM modules + uses: actions/upload-artifact@v7 + with: + name: wasm-modules + path: | + internal/providers/modules/*.wasm + internal/actions/modules/*.wasm + retention-days: 1 + + - name: Upload console dist + uses: actions/upload-artifact@v7 + with: + name: console-dist + path: internal/http/console/dist/ + retention-days: 1 + + goreleaser: + needs: build-assets + runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v5 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: ${{ vars.GO_VERSION }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Download WASM modules + uses: actions/download-artifact@v8 + with: + name: wasm-modules + path: internal + + - name: Download console dist + uses: actions/download-artifact@v8 + with: + name: console-dist + path: internal/http/console/dist/ + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v7 + with: + version: "~> v2" + args: release --clean --skip=validate + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + renderer: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -32,24 +130,21 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/lunogram/renderer tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} - type=sha,prefix= - - name: Build and push - uses: docker/build-push-action@v6 + - name: Build and push renderer image + uses: docker/build-push-action@v7 with: - context: . + context: ./renderer push: true + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - build-args: | - VERSION=${{ github.ref_name }} - COMMIT=${{ github.sha }} - cache-from: type=gha - cache-to: type=gha,mode=max + cache-from: type=gha,scope=renderer + cache-to: type=gha,mode=max,scope=renderer diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..600c72e5 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,63 @@ +version: 2 +project_name: lunogram + +before: + hooks: + # Verify that pre-built embedded assets exist. + # WASM modules and console SPA are built by the CI workflow + # before goreleaser runs. + - sh -c 'test -f internal/providers/modules/resend.wasm || (echo "WASM modules not found — run build-assets first" && exit 1)' + - sh -c 'test -f internal/http/console/dist/index.html || (echo "Console dist not found — run build-assets first" && exit 1)' + +builds: + - id: lunogram + main: ./cmd/lunogram + binary: lunogram + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - -w -s + - -X github.com/lunogram/platform/internal/build.version={{.Version}} + - -X github.com/lunogram/platform/internal/build.commit={{.ShortCommit}} + +archives: + - id: lunogram + ids: [lunogram] + formats: [tar.gz] + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + +checksum: + name_template: "checksums.txt" + algorithm: sha256 + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + - "^ci:" + - "^chore:" + +dockers_v2: + - images: + - "ghcr.io/lunogram/platform" + tags: + - "{{ .Version }}" + - "{{ .Major }}.{{ .Minor }}" + - "{{ .Major }}" + - "latest" + dockerfile: Dockerfile.goreleaser + +release: + github: + owner: lunogram + name: platform + draft: false + prerelease: auto diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..510a0c0e --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,9 @@ +{ + "lsp": { + "eslint": { + "settings": { + "workingDirectories": ["./console"] + } + } + } +} diff --git a/Dockerfile b/Dockerfile index f37f7f94..1b0d4a0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,17 @@ FROM tinygo/tinygo:0.40.1 AS modules WORKDIR /src +USER root +RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm && rm -rf /var/lib/apt/lists/* + COPY go.mod go.sum ./ COPY Makefile ./ COPY modules/ ./modules/ COPY pkg/ ./pkg/ -RUN mkdir -p internal/providers/modules && make modules +RUN mkdir -p internal/providers/modules internal/actions/modules && make modules FROM node:24-alpine AS console -ARG VITE_CLERK_PUBLISHABLE_KEY WORKDIR /src RUN corepack enable @@ -28,6 +30,7 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . COPY --from=modules /src/internal/providers/modules/*.wasm ./internal/providers/modules/ +COPY --from=modules /src/internal/actions/modules/*.wasm ./internal/actions/modules/ COPY --from=console /src/console/dist ./internal/http/console/dist/ RUN VERSION=${VERSION} SHORT_COMMIT=${COMMIT} make lunogram diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser new file mode 100644 index 00000000..0ed5282b --- /dev/null +++ b/Dockerfile.goreleaser @@ -0,0 +1,8 @@ +# Minimal Dockerfile used by GoReleaser. +# GoReleaser cross-compiles the binary and copies it into this image. +FROM gcr.io/distroless/static-debian12:nonroot +WORKDIR /app +ARG TARGETPLATFORM +COPY $TARGETPLATFORM/lunogram /app/lunogram +EXPOSE 8080 8081 +ENTRYPOINT ["/app/lunogram"] diff --git a/LICENSE b/LICENSE index 7f73b8d3..a8441335 100644 --- a/LICENSE +++ b/LICENSE @@ -6,31 +6,42 @@ enterprise features. Every file is under copyright (c) Lunogram B.V. 2026 unless otherwise specified. Every file is under License AGPL unless otherwise specified or -belonging to one of the below cases: +belonging to one of the below cases. -The files under internal/ are BSL 1.1 Licensed, except any snippets of code under -the compile flag "enterprise". Those snippets and files are under a proprietary -and commercial license. The files under console/ are BSL 1.1 Licensed, except -any snippets of code that require a positive license check to be activated. -Those snippets and files are under a proprietary and commercial license. The -files under modules/ are AGPLv3 Licensed, except any snippets of code under the -compile flag "enterprise" or that require a positive license check to be -activated. Private and public forks MUST not include any of the above proprietary -and commercial code. The openapi files are Apache 2.0 Licensed. +"Enterprise Feature" means any code that is: (a) enclosed in a +//go:build enterprise build tag, (b) gated behind a Vite __ENTERPRISE__ +compile-time constant, or (c) activated exclusively by a positive response +from the Lunogram license validation service. Enterprise Features require a +separate commercial license from Lunogram B.V. regardless of the license +that otherwise applies to the file in which they appear. + +The files under internal/ are BSL 1.1 Licensed, except any Enterprise +Features. Those Enterprise Features are under a proprietary and commercial +license. The files under console/ are BSL 1.1 Licensed, except any +Enterprise Features. Those Enterprise Features are under a proprietary and +commercial license. The files under modules/ are AGPLv3 Licensed, except +any Enterprise Features. Those Enterprise Features are under a proprietary +and commercial license. Private and public forks MUST not include any of +the above proprietary and commercial code. The openapi files are Apache +2.0 Licensed. The binary compilable from source code in this repository without the -"enterprise" feature flag is open-source under the AGPLv3 License terms and -conditions. +"enterprise" feature flag is open-source under the AGPLv3 License terms +and conditions. -The "Community Edition" of Lunogram available in the docker images hosted under -ghcr.io/lunogram/platform and the github binary releases contains the files -under the AGPLv3 and Apache 2 sources but also includes proprietary and -non-public code and features which are not open source and under the following -terms: Lunogram B.V. grants a right to use all the features of the -"Community Edition" for free without restrictions other than the limits and -quotas set in the software and a right to distribute the community edition as is -but not to sell, resell, serve as a managed service, modify or wrap under any -form without an explicit agreement. +The "Community Edition" of Lunogram available in the docker images hosted +under ghcr.io/lunogram/platform and the github binary releases contains +the files under the AGPLv3 and Apache 2 sources but also includes +proprietary and non-public code and features which are not open source and +under the following terms: Lunogram B.V. grants a right to use all the +features of the "Community Edition" for free without restrictions other +than the limits and quotas set in the software and a right to distribute +the community edition as is but not to sell, resell, serve as a managed +service, modify or wrap under any form without an explicit agreement. +The Community Edition grants access only to features that are available +without a valid commercial license key. Enterprise Features included in +Community Edition binaries remain subject to a separate commercial license. -All third party components incorporated into the Lunogram Software are licensed -under the original license provided by the owner of the applicable component. +All third party components incorporated into the Lunogram Software are +licensed under the original license provided by the owner of the applicable +component. diff --git a/Makefile b/Makefile index a8fd302b..9291c918 100644 --- a/Makefile +++ b/Makefile @@ -28,8 +28,7 @@ $(BUILD_DIR): @mkdir -p $@ PROVIDER_MODULES := $(notdir $(wildcard ./modules/providers/*)) - - +ACTION_MODULES := $(notdir $(wildcard ./modules/actions/*)) # Tools $(BIN): @@ -60,10 +59,20 @@ lunogram: ; $(info $(M) building lunogram…) $Q CGO_ENABLED=0 $(GO) build -ldflags='$(LDFLAGS)' -o $(BIN)/lunogram ./cmd/lunogram .PHONY: modules -modules: ; $(info $(M) building provider modules…) @ ## Build all provider modules +modules: providers actions ## Build all WASM modules + +.PHONY: providers +providers: ; $(info $(M) building provider modules…) @ ## Build all provider modules $Q for module in $(PROVIDER_MODULES); do \ - echo "$(M) building $$module module…"; \ - cd modules/providers/$$module && $(TINYGO) build -target=wasi -buildmode c-shared -opt=2 -no-debug -o ../../../internal/providers/modules/$$module.wasm ./main.go && cd ../../..; \ + echo "$(M) building $$module provider…"; \ + $(MAKE) -C modules/providers/$$module wasm TINYGO=$(TINYGO) NODE=$(NODE); \ + done + +.PHONY: actions +actions: ; $(info $(M) building action modules…) @ ## Build all action modules + $Q for module in $(ACTION_MODULES); do \ + echo "$(M) building $$module action…"; \ + $(MAKE) -C modules/actions/$$module all TINYGO=$(TINYGO) NODE=$(NODE); \ done .PHONY: console @@ -80,7 +89,7 @@ lint: | $(EMBEDDED) $(GOLANGCI_LINT) $(BUF) ; $(info $(M) running linters…) @ .PHONY: test test: | $(EMBEDDED) ; $(info $(M) running tests) @ ## Run all tests - $Q $(GO) test $(PKGS) -timeout 300s -race -count 1 + $Q $(GO) test $(PKGS) -timeout 600s -race -p 8 .PHONY: test-short test-short: | $(EMBEDDED) ; $(info $(M) running short tests) @ ## Run all short tests diff --git a/README.md b/README.md index 07c0ea6c..d5ee1680 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@
-Engage your customers through effortless communication.
diff --git a/cmd/lunogram/main.go b/cmd/lunogram/main.go index 12f60202..3eaf1344 100644 --- a/cmd/lunogram/main.go +++ b/cmd/lunogram/main.go @@ -8,6 +8,7 @@ import ( "github.com/caarlos0/env/v10" "github.com/cloudproud/graceful" + "github.com/lunogram/platform/internal/actions" "github.com/lunogram/platform/internal/cluster" "github.com/lunogram/platform/internal/cluster/consensus" "github.com/lunogram/platform/internal/cluster/leader" @@ -17,11 +18,16 @@ import ( "github.com/lunogram/platform/internal/providers" "github.com/lunogram/platform/internal/pubsub" "github.com/lunogram/platform/internal/pubsub/consumer" + "github.com/lunogram/platform/internal/ratelimit" + "github.com/lunogram/platform/internal/rbac" + "github.com/lunogram/platform/internal/rbac/access" + iredis "github.com/lunogram/platform/internal/redis" "github.com/lunogram/platform/internal/storage" "github.com/lunogram/platform/internal/store" "github.com/lunogram/platform/internal/store/journey" "github.com/lunogram/platform/internal/store/management" - "github.com/lunogram/platform/internal/store/users" + "github.com/lunogram/platform/internal/store/subjects" + "go.uber.org/zap" ) @@ -56,15 +62,25 @@ func run() error { if migrate || conf.DatabaseMigrate { logger.Info("running database migrations...") + logger.Info("running management migration") if err := management.Migrate(conf.Store.ManagementURI); err != nil { return fmt.Errorf("management migration failed: %w", err) } - if err := users.Migrate(conf.Store.UsersURI); err != nil { - return fmt.Errorf("users migration failed: %w", err) + + logger.Info("running subjects migration") + if err := subjects.Migrate(conf.Store.SubjectsURI); err != nil { + return fmt.Errorf("subjects migration failed: %w", err) } + + logger.Info("running journey migration") if err := journey.Migrate(conf.Store.JourneyURI); err != nil { return fmt.Errorf("journey migration failed: %w", err) } + + logger.Info("running rbac migration") + if err := rbac.Migrate(conf.RBAC); err != nil { + return fmt.Errorf("rbac migration failed: %w", err) + } if migrate { return nil } @@ -79,7 +95,7 @@ func run() error { } managementStore := management.NewState(db.Management) - usersStore := users.NewState(db.Users) + usersStore := subjects.NewState(db.Subjects, logger) journeyStore := journey.NewState(db.Journey) logger.Info("initializing block storage") @@ -96,25 +112,52 @@ func run() error { return err } - err = consumer.Bootstrap(ctx, logger, jet) + err = consumer.Bootstrap(ctx, logger, jet, consumer.Namespace(conf.Nats.Namespace), consumer.WithManagedExternally(conf.Nats.ManagedExternally)) if err != nil { return err } logger.Info("initializing provider registry") - registry, err := providers.NewRegistry(ctx, conf.WASM, logger) + providersRegisrtry, err := providers.NewRegistry(ctx, conf.WASM, logger) if err != nil { return err } - defer registry.Close(ctx) + defer providersRegisrtry.Close(ctx) + + logger.Info("initializing action registry") + + actionRegistry, err := actions.NewRegistry(ctx, conf.WASM, logger) + if err != nil { + return err + } + defer actionRegistry.Close(ctx) + + pub := pubsub.NewPublisher(jet, conf.Nats.Namespace) + req := pubsub.NewCaller(jet, conf.Nats.Namespace) + ns := consumer.Namespace(conf.Nats.Namespace) + + trackingURL := conf.Link.TrackingBaseURL() + linkKey := conf.Link.SecretBytes() + + logger.Info("link wrapping enabled", zap.String("tracking_url", trackingURL)) - pub := pubsub.NewPublisher(jet) - consumer.Serve(ctx, jet, logger, db, managementStore, usersStore, journeyStore, registry) + logger.Info("initializing redis client") + + rclient, err := iredis.New(ctx, logger, conf.Redis.Address) + if err != nil { + return err + } + + limiter := ratelimit.New(rclient, conf.Redis.KeyPrefix, logger) + recomputeLocker := iredis.NewRecomputeLocker(rclient, conf.Redis.KeyPrefix) + schemaCache := iredis.NewSchemaCache(rclient, conf.Redis.KeyPrefix) + + consumer.Serve(ctx, jet, logger, ns, db, managementStore, usersStore, journeyStore, providersRegisrtry, actionRegistry, req, limiter, recomputeLocker, schemaCache, conf.PublicBaseURL(), linkKey, trackingURL) logger.Info("initializing cluster") - sched := scheduler.NewController(ctx, logger, conf, journeyStore, pub) + sched := scheduler.NewController(ctx, logger, conf, journeyStore, usersStore, managementStore, pub) lead := leader.NewHandler(sched) cons, err := consensus.NewCluster(ctx, logger, conf) if err != nil { @@ -126,9 +169,21 @@ func run() error { return err } + logger.Info("initializing rbac engine") + + rbacEngine, err := rbac.NewEngine(ctx, conf.RBAC) + if err != nil { + return fmt.Errorf("failed to initialize rbac engine: %w", err) + } + defer rbacEngine.Close() + + if err := access.BackfillProjectTuples(ctx, logger, rbacEngine, db.Management); err != nil { + return fmt.Errorf("failed to backfill rbac resource tuples: %w", err) + } + logger.Info("starting http server") - server, err := v1.NewServer(ctx, logger, conf, db, bucket, pub, registry) + server, err := v1.NewServer(ctx, logger, conf, db, bucket, jet, pub, req, providersRegisrtry, actionRegistry, rbacEngine) if err != nil { return err } diff --git a/console/.env.example b/console/.env.example index 6e8790cd..ca4c62e7 100644 --- a/console/.env.example +++ b/console/.env.example @@ -1,2 +1,3 @@ VITE_API_BASE_URL=http://localhost:3000/api -VITE_PROXY_URL=http://localhost:8080 \ No newline at end of file +VITE_PROXY_URL=http://localhost:8080 +VITE_COURIER_URL=http://localhost:8083/ diff --git a/console/.prettierrc b/console/.prettierrc new file mode 100644 index 00000000..0598ff9d --- /dev/null +++ b/console/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "tabWidth": 4, + "printWidth": 100, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true +} diff --git a/console/components.json b/console/components.json index 35f96225..b2e33aa1 100644 --- a/console/components.json +++ b/console/components.json @@ -18,5 +18,7 @@ "lib": "@/lib", "hooks": "@/hooks" }, - "registries": {} -} \ No newline at end of file + "registries": { + "@mapcn": "https://mapcn.dev/r/{name}.json" + } +} diff --git a/console/eslint.config.js b/console/eslint.config.js index a5f879e0..75aa3bf9 100644 --- a/console/eslint.config.js +++ b/console/eslint.config.js @@ -1,44 +1,54 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from "@eslint/js" +import globals from "globals" +import reactHooks from "eslint-plugin-react-hooks" +import reactRefresh from "eslint-plugin-react-refresh" +import prettier from "eslint-plugin-prettier" +import prettierConfig from "eslint-config-prettier" +import tseslint from "typescript-eslint" +import { defineConfig, globalIgnores } from "eslint/config" export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - rules: { - "@typescript-eslint/consistent-type-imports": ["error", { - "prefer": "type-imports", - "fixStyle": "separate-type-imports" - }], + globalIgnores(["dist", "src/oapi/management.generated.ts"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + prettierConfig, + ], + plugins: { + prettier, + }, + rules: { + "prettier/prettier": "warn", + "@typescript-eslint/consistent-type-imports": [ + "error", + { + prefer: "type-imports", + fixStyle: "separate-type-imports", + }, + ], - // 🧘 Relax noisy TS rules during migration - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-empty-object-type': 'off', - '@typescript-eslint/no-unsafe-function-type': 'off', - 'no-unused-vars': 'off', - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - 'react/react-in-jsx-scope': 'off', - 'react/prop-types': 'off', + // 🧘 Relax noisy TS rules during migration + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }], + "react/react-in-jsx-scope": "off", + "react/prop-types": "off", - // Disable React 19 compiler preflight rules (React 18 doesn’t use them) - 'react-hooks/immutability': 'off', - 'react-hooks/refs': 'off', - 'react-hooks/set-state-in-effect': 'off', - 'react-hooks/preserve-manual-memoization': 'off', + // Disable React 19 compiler preflight rules (React 18 doesn’t use them) + "react-hooks/immutability": "off", + "react-hooks/refs": "off", + "react-hooks/set-state-in-effect": "off", + "react-hooks/preserve-manual-memoization": "off", + }, + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, }, - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, ]) diff --git a/console/index.html b/console/index.html index 7f8c33ef..44c0f7f9 100644 --- a/console/index.html +++ b/console/index.html @@ -1,32 +1,31 @@ - + + + + + + - - - - - - - - - - - - + + + + + - - + + - - -