Skip to content

Commit d3e47eb

Browse files
Merge branch 'main' into feat/Payment
2 parents d64461a + 02ee94d commit d3e47eb

67 files changed

Lines changed: 5936 additions & 40 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ RATE_LIMIT_ENTERPRISE_PER_MINUTE=10000
4848
RATE_LIMIT_WINDOW_MS=60000
4949
RATE_LIMIT_BURST_MULTIPLIER=1.2
5050

51+
# Distributed rate limiting (Redis-backed) — complements the per-tier limits above
52+
# Default strategy: "token-bucket" or "sliding-window"
53+
RATE_LIMIT_DEFAULT_STRATEGY=token-bucket
54+
# Redis key prefix for rate-limit entries
55+
RATE_LIMIT_REDIS_KEY_PREFIX=alian:rl:
56+
# When false, requests are rejected (not in-memory fallback) if Redis is down
57+
RATE_LIMIT_FALLBACK_TO_MEMORY=true
58+
5159
# ── Logging ────────────────────────────────────────────────────────────────
5260
# Minimum log level emitted to all transports.
5361
# Values: verbose | debug | info | warn | error | fatal

.env.production.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ RATE_LIMIT_ENTERPRISE_PER_MINUTE=10000
6969
RATE_LIMIT_WINDOW_MS=60000
7070
RATE_LIMIT_BURST_MULTIPLIER=1.2
7171

72+
# Distributed rate limiting (Redis-backed)
73+
RATE_LIMIT_DEFAULT_STRATEGY=token-bucket
74+
RATE_LIMIT_REDIS_KEY_PREFIX=alian:rl:
75+
RATE_LIMIT_FALLBACK_TO_MEMORY=false
76+
7277
# Security Headers
7378
HSTS_MAX_AGE=31536000
7479

.eslintrc.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ module.exports = {
1414
node: true,
1515
jest: true,
1616
},
17+
overrides: [
18+
{
19+
files: [
20+
'test/modules/**/*.ts',
21+
'test/alerts/alert-preferences.e2e-spec.ts',
22+
],
23+
parserOptions: {
24+
project: 'test/tsconfig.json',
25+
},
26+
},
27+
],
1728
ignorePatterns: ['.eslintrc.js'],
1829
rules: {
1930
'@typescript-eslint/interface-name-prefix': 'off',
@@ -23,4 +34,4 @@ module.exports = {
2334
"@typescript-eslint/no-namespace": "off",
2435
"import/no-relative-parent-imports": "error",
2536
},
26-
};
37+
};

.vscode/settings.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
{
2-
"snyk.advanced.autoSelectOrganization": true
3-
}
2+
"snyk.advanced.autoSelectOrganization": true,
3+
"typescript.tsdk": "node_modules/typescript/lib",
4+
"typescript.enablePromptUseWorkspaceTsdk": true
5+
}

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ RUN addgroup -S app && adduser -S app -G app
4343
# Copy only what's needed for runtime
4444
COPY --from=builder /app/dist ./dist
4545
COPY --from=builder /app/node_modules ./node_modules
46+
COPY --from=builder /app/modules ./modules
47+
COPY --from=builder /app/package.json ./package.json
4648

4749
ENV NODE_ENV=${NODE_ENV}
4850
ENV PORT=3000

MODULE_REVIEW_CHECKLIST.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Module review checklist
2+
3+
Use this checklist when GrantFox, Stella, or a SourceXXL maintainer reviews a new
4+
module pull request.
5+
6+
## Manifest and compatibility
7+
8+
- [ ] `module.manifest.json` is present and passes registry validation.
9+
- [ ] `name` is stable and unique, and `version` is valid semver.
10+
- [ ] The declared core compatibility range is accurate and tested.
11+
- [ ] Every lifecycle hook marked `true` is implemented by the entry point.
12+
- [ ] The published/runtime entry point is compiled JavaScript and loads without
13+
development-only TypeScript hooks.
14+
15+
## Scope and architecture
16+
17+
- [ ] The module does not directly modify or couple itself to core files.
18+
- [ ] Public behavior is exposed through documented module boundaries.
19+
- [ ] Database changes include safe, reversible migration hooks.
20+
- [ ] Lifecycle hooks are idempotent and fail without leaving partial state.
21+
22+
## Quality and isolation
23+
24+
- [ ] Unit and integration tests are included and passing.
25+
- [ ] Per-tenant enablement, disablement, and configuration isolation are tested.
26+
- [ ] Global-default behavior is documented and does not overwrite tenant states.
27+
- [ ] Effective-state tests cover tenant override, global fallback, and implicit
28+
disabled behavior.
29+
- [ ] Logs and errors provide enough context without exposing sensitive data.
30+
- [ ] No API keys, credentials, private URLs, or other secrets are hardcoded.
31+
- [ ] Registry installation and upgrade operations remain restricted to
32+
administrators.
33+
34+
## Governance and documentation
35+
36+
- [ ] Dependency and source licenses are compatible with Apache-2.0.
37+
- [ ] The module's purpose, setup, configuration, and operational limits are documented.
38+
- [ ] Upgrade, rollback, and uninstall expectations are documented.
39+
- [ ] User-facing API or configuration changes include examples.
40+
- [ ] The PR is limited to the module and necessary registry integration.

docs/module-registry.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Module registry and versioning
2+
3+
The module registry installs pluggable packages without adding them to the core
4+
application module graph. It validates manifests, checks compatibility with the
5+
running API version, stores one current version per module name, and isolates
6+
enablement and configuration by tenant.
7+
8+
## Author a module
9+
10+
Create a `module.manifest.json` next to the module entry point:
11+
12+
```json
13+
{
14+
"name": "portfolio-exporter",
15+
"version": "1.0.0",
16+
"core": ">=0.1.0 <1.0.0",
17+
"hooks": {
18+
"onInstall": true,
19+
"onUpgrade": true,
20+
"onUninstall": false
21+
},
22+
"entryPoint": "@organization/portfolio-exporter"
23+
}
24+
```
25+
26+
The schema requires:
27+
28+
- `name`: lowercase module identifier.
29+
- `version`: valid semantic version.
30+
- `core`: semantic-version range supported by the module.
31+
- `hooks`: booleans declaring `onInstall`, `onUpgrade`, and `onUninstall`.
32+
- `entryPoint`: installed package reference or a runtime JavaScript entry point
33+
inside the root `modules/` directory. Local entry points outside that directory
34+
are rejected so manifests cannot load core source files.
35+
36+
The machine-readable schema is in
37+
`src/modules/registry/module-manifest.schema.json`. Runtime validation uses the
38+
equivalent class-validator DTO plus `semver`, so invalid versions and ranges are
39+
rejected even when a client does not use the JSON Schema.
40+
41+
Local modules must expose JavaScript that Node can load without `ts-node`.
42+
TypeScript authors should compile their package before registration and ship
43+
declarations that implement `ModuleLifecycle`. Published npm package entry points
44+
are resolved normally from `node_modules`.
45+
46+
The entry point must default-export a lifecycle class or object implementing:
47+
48+
```ts
49+
interface ModuleLifecycle {
50+
onInstall?(): Promise<void>;
51+
onUpgrade?(fromVersion: string, toVersion: string): Promise<void>;
52+
onUninstall?(): Promise<void>;
53+
}
54+
```
55+
56+
A hook marked `true` must be implemented. Hooks execute inside the registry's
57+
database transaction where possible. A thrown error rolls back the registry
58+
version and status. Because external services cannot participate in the database
59+
transaction, hooks must be idempotent and compensate for external side effects.
60+
61+
## Register and upgrade
62+
63+
Registry endpoints require an authenticated administrator. Registry management
64+
bypasses the general user KYC guard because it is an administrative control-plane
65+
operation; the global authentication and role guards still apply.
66+
67+
POST the manifest and its metadata to `POST /api/v1/modules`:
68+
69+
```json
70+
{
71+
"manifest": {
72+
"name": "portfolio-exporter",
73+
"version": "1.0.0",
74+
"core": ">=0.1.0 <1.0.0",
75+
"hooks": {
76+
"onInstall": true,
77+
"onUpgrade": true,
78+
"onUninstall": false
79+
},
80+
"entryPoint": "@organization/portfolio-exporter"
81+
},
82+
"description": "Exports tenant portfolios",
83+
"author": "Organization"
84+
}
85+
```
86+
87+
The API reads the core version from the root `package.json`. Registration and
88+
enablement fail with a message containing the required range and actual version
89+
when the module is incompatible.
90+
91+
Posting a new name runs `onInstall`. Posting the same name with a strictly newer
92+
version runs `onUpgrade` and updates the existing registry row. Equal versions and
93+
downgrades are rejected.
94+
95+
## Tenant and global enablement
96+
97+
Enable a module with `POST /api/v1/modules/:id/enable` and disable it with
98+
`POST /api/v1/modules/:id/disable`:
99+
100+
```json
101+
{
102+
"tenantId": "tenant-123",
103+
"config": { "format": "csv" }
104+
}
105+
```
106+
107+
Each operation changes only that tenant's `TenantModuleState`. The project has no
108+
canonical tenant entity, so `tenantId` is an opaque identifier supplied by the
109+
caller. Omitting `tenantId` creates or updates the nullable global-default state;
110+
it does not alter any explicit tenant row. An explicit tenant state therefore
111+
remains isolated from the default.
112+
113+
Resolve the effective state for a tenant with:
114+
115+
```text
116+
GET /api/v1/modules/:id/state?tenantId=tenant-123
117+
```
118+
119+
Resolution uses the explicit tenant row first, then the global-default row. When
120+
neither exists the result is an implicit disabled state. A disabled explicit row
121+
therefore overrides an enabled global default.
122+
123+
A module can be removed with `DELETE /api/v1/modules/:id` only when every tenant
124+
and global-default state is disabled. Deregistration runs `onUninstall` when it is
125+
declared.
126+
127+
## Run the example locally
128+
129+
The working example is in `modules/example-grant-module`. Its CommonJS entry
130+
point is loadable by the development server, the bundled application, and the
131+
production Docker image; `index.d.ts` declares the lifecycle TypeScript contract.
132+
133+
```bash
134+
npm install
135+
npm run migration:run
136+
npm run start:dev
137+
MODULE_REGISTRY_TOKEN=<admin-token> npm run module:example:register
138+
```
139+
140+
`MODULE_REGISTRY_TOKEN` must contain an administrator bearer token. Set
141+
`MODULE_REGISTRY_URL` to use a non-default API URL. Inspect the registered module
142+
with `GET /api/v1/modules`, then use its returned UUID in the enable, disable, and
143+
state-resolution endpoints.

docs/stellar-reconciliation.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Stellar reconciliation
2+
3+
The reconciliation module records confirmed Stellar payments, matches them to internal invoices, and preserves every decision in an audit trail.
4+
5+
## Testnet polling
6+
7+
Set `STELLAR_RECONCILIATION_ACCOUNT` to the Stellar destination account monitored by the service. The service polls `STELLAR_HORIZON_URL` every 60 seconds and defaults to `https://horizon-testnet.stellar.org`. Horizon paging tokens provide idempotent continuation. Duplicate transaction hashes are ignored and recorded as retry-safe audit entries.
8+
9+
For webhook or queue consumers, send a confirmed payment to `POST /reconcile/stellar/transactions` with its transaction hash, destination account, amount, asset, and memo. The endpoint accepts the same payload shape as the Horizon adapter.
10+
11+
## Invoice lifecycle
12+
13+
Register an invoice with `POST /reconcile/stellar/invoice`.
14+
15+
```json
16+
{
17+
"invoiceId": "INV-2026-0001",
18+
"expectedAmount": "125.5000000",
19+
"destinationAccount": "GABC...DEST",
20+
"paymentReference": "order-0001",
21+
"assetCode": "XLM"
22+
}
23+
```
24+
25+
Matching requires destination account and asset equality. When an invoice has a payment reference, the incoming memo must match it. The invoice moves from `open` to `partial` or `paid`. Transactions without a matching invoice remain `unmatched`.
26+
27+
## Lookup and operations
28+
29+
`GET /reconcile/stellar/tx/:txid` returns the transaction and its decisions. `GET /reconcile/stellar/invoice/:invoiceId` returns the invoice and its decisions. `POST /reconcile/stellar/invoice/:invoiceId/reconcile` retries matching for accounting staff. Administrators can inspect unmatched transactions with `GET /reconcile/stellar/admin/unmatched` and audit records with `GET /reconcile/stellar/admin/audit`.
30+
31+
The admin endpoints require an authenticated administrator with verified two-factor authentication. All decision records include the invoice, transaction, decision, reason, attempt, and relevant matching metadata.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Example grant module
2+
3+
This package is a minimal runtime-safe module that demonstrates the registry's
4+
install and upgrade lifecycle. It lives outside `src/`, does not modify core
5+
files, and exports a CommonJS entry point that works in development, the bundled
6+
Nest build, and the production Docker image.
7+
8+
`index.d.ts` declares that the runtime class implements `ModuleLifecycle`.
9+
`index.cjs` records lifecycle calls in memory so the e2e test can verify them.
10+
11+
Start the API, provide an admin bearer token, and register the module:
12+
13+
```bash
14+
npm run start:dev
15+
MODULE_REGISTRY_TOKEN=<admin-token> npm run module:example:register
16+
```
17+
18+
Override the endpoint with `MODULE_REGISTRY_URL` when the API is not available at
19+
`http://localhost:3001/api/v1/modules`.
20+
21+
Submitting the same manifest with a version newer than `0.1.0` calls
22+
`onUpgrade(fromVersion, toVersion)`.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"use strict";
2+
3+
/** @type {string[]} */
4+
const exampleGrantModuleEvents = [];
5+
6+
/**
7+
* Runtime implementation of the registry's ModuleLifecycle contract.
8+
* The adjacent index.d.ts provides the compile-time TypeScript contract.
9+
*/
10+
class ExampleGrantModuleLifecycle {
11+
async onInstall() {
12+
exampleGrantModuleEvents.push("installed");
13+
}
14+
15+
async onUpgrade(fromVersion, toVersion) {
16+
exampleGrantModuleEvents.push(`upgraded:${fromVersion}->${toVersion}`);
17+
}
18+
}
19+
20+
module.exports = {
21+
default: ExampleGrantModuleLifecycle,
22+
exampleGrantModuleEvents,
23+
};

0 commit comments

Comments
 (0)