# Glow Events SDK

A TypeScript-first SDK for consuming and emitting typed events on the Glow platform, powered by RabbitMQ (topic exchange). Provides runtime validation and type inference using Zod schemas.

---

## 🚀 Quick Start

### 1. Install

```bash
pnpm install @glowlabs-org/events-sdk
```

---

## 📚 Zones

The following zones are currently available:

| Zone ID | Zone Name          |
| ------- | ------------------ |
| 0       | All Zones          |
| 1       | Clean Grid Project |
| 2       | Coming Soon Zone   |

- Use `zoneId: 0` (with `zoneName: "All Zones"`) to listen to all zones. **Emitters must be constructed with a specific zoneId (not 0).**
- The SDK types and runtime validation now officially support `zoneId: 0` and `zoneName: "All Zones"` everywhere for listeners, but not for emitters.
- **Use the `getZoneId` utility to get the correct zoneId for a given zone name:**

  ```ts
  import { getZoneId } from "@glowlabs-org/events-sdk";
  const zoneId = getZoneId("CleanGridProject");
  ```

---

## 🛠️ Options for Listeners & Emitters

Both `createGlowEventListener` and `createGlowEventEmitter` accept the following options:

| Option           | Type   | Required | Description                                                        |
| ---------------- | ------ | -------- | ------------------------------------------------------------------ |
| `username`       | string | Yes      | RabbitMQ username                                                  |
| `password`       | string | Yes      | RabbitMQ password                                                  |
| `zoneId`         | number | Yes      | Zone ID (use `getZoneId("ZoneName")`)                              |
| `queueName`      | string | No       | (Listener only) Pre-created queue name                             |
| `exchangePrefix` | string | No       | Exchange prefix (default: `glow.zone-`)                            |
| `host`           | string | No       | **RabbitMQ host/port** (default: `turntable.proxy.rlwy.net:50784`) |

---

## 📦 Event Types & Versions

Currently supported event types and versions:

| Event Name            | Version | Payload Type                  | Description                                         |
| --------------------- | ------- | ----------------------------- | --------------------------------------------------- |
| `audit.pushed`        | "v1"    | `AuditPushedV1Payload`        | Emitted when an audit is pushed                     |
| `audit.slashed`       | "v1"    | `AuditSlashedV1Payload`       | Emitted when a farm is slashed                      |
| `audit.pfees.paid`    | "v1"    | `AuditPfeesPaidV1Payload`     | Paid (by applicationId)                             |
| `audit.pfees.paid`    | "v2"    | `AuditPfeesPaidV2Payload`     | Paid (by farmId)                                    |
| `application.created` | "v1"    | `ApplicationCreatedV1Payload` | Emitted when an application is created              |
| `audit.pushed`        | "v2"    | `AuditPushedV2Payload`        | Emitted when an audit is pushed (farmId is bytes16) |

---

### Event Types Enum

The SDK provides a TypeScript constant for all supported event types to ensure type safety and avoid typos:

```ts
import { eventTypes, EventType } from "./src/event-types";

// Usage example:
const myEventType: EventType = eventTypes.auditPushed;
```

- `eventTypes` is a readonly object containing all event type strings.
- `EventType` is a TypeScript type representing any valid event type string.

Use these in your code to avoid hardcoding event names and to benefit from autocompletion and type checking.

---

## 📝 Event Payload Schemas

### `audit.pushed` v1

```ts
export interface AuditPushedV1Payload {
  farmId: string; // UUID string
  protocolFeeUSDPrice_12Decimals: string; // uint256 (decimal) − 12 implied decimals
  expectedProduction_12Decimals: string; // uint256 (decimal) − 12 implied decimals
  txHash: string; // bytes32 hex string (0x...)
}
```

**Validation:**

- `farmId` must be a valid UUID string (e.g., `afbc56b6-0b16-4119-b144-025728067ba6`).
- `protocolFeeUSDPrice_12Decimals` and `expectedProduction_12Decimals` must be decimal strings representing unsigned big integers.
- `txHash` must be a 32-byte hex string (e.g., `0x...`).

### `audit.slashed` v1

```ts
export interface AuditSlashedV1Payload {
  farmId: string; // bytes16 hex string (0x...)
  slasher: string; // Ethereum address (0x...)
  txHash: string; // bytes32 hex string (0x...)
}
```

**Validation:**

- `farmId` must be a 16-byte hex string (e.g., `0x...`).
- `slasher` must be a valid Ethereum address (0x...40 hex chars).
- `txHash` must be a 32-byte hex string (e.g., `0x...`).

### `audit.pfees.paid` v1 (by applicationId)

```ts
export interface AuditPfeesPaidV1Payload {
  applicationId: string; // UUID string
  payer: string; // Ethereum address (0x...)
  amount_12Decimals: string; // uint256 (decimal) − 12 implied decimals
  txHash: string; // bytes32 hex string (0x...)
}
```

**Validation:**

- `applicationId` must be a valid UUID string (e.g., `3ed964b1-4f02-475a-9789-fb74b3466c70`).
- `payer` must be a valid Ethereum address (0x...40 hex chars).
- `amount_12Decimals` must be a decimal string representing an unsigned big integer (12 implied decimals).
- `txHash` must be a 32-byte hex string (e.g., `0x...`).

### `audit.pfees.paid` v2 (by farmId)

```ts
export interface AuditPfeesPaidV2Payload {
  farmId: string; // bytes16 hex string (0x...)
  payer: string; // Ethereum address (0x...)
  amount_12Decimals: string; // uint256 (decimal) − 12 implied decimals
  txHash: string; // bytes32 hex string (0x...)
}
```

**Validation:**

- `farmId` must be a 16-byte hex string (e.g., `0x...`).
- `payer` must be a valid Ethereum address (0x...40 hex chars).
- `amount_12Decimals` must be a decimal string representing an unsigned big integer (12 implied decimals).
- `txHash` must be a 32-byte hex string (e.g., `0x...`).

### `audit.pushed` v2

```ts
export interface AuditPushedV2Payload {
  farmId: string; // bytes16 hex string (0x...)
  protocolFeeUSDPrice_12Decimals: string; // uint256 (decimal) − 12 implied decimals
  expectedProduction_12Decimals: string; // uint256 (decimal) − 12 implied decimals
  txHash: string; // bytes32 hex string (0x...)
}
```

**Validation:**

- `farmId` must be a 16-byte hex string (e.g., `0x...`).
- `protocolFeeUSDPrice_12Decimals` and `expectedProduction_12Decimals` must be decimal strings representing unsigned big integers.
- `txHash` must be a 32-byte hex string (e.g., `0x...`).

### `application.created` v1

```ts
export interface ApplicationCreatedV1Payload {
  gcaAddress: string; // Ethereum address (0x...)
  lat: number;
  lng: number;
  estimatedCostOfPowerPerKWh: number;
  estimatedKWhGeneratedPerYear: number;
  installerCompanyName: string;
}
```

**Validation:**

- `gcaAddress` must be a valid Ethereum address (0x...40 hex chars).
- `lat` and `lng` are numbers (coordinates).
- `estimatedCostOfPowerPerKWh` and `estimatedKWhGeneratedPerYear` are numbers.
- `installerCompanyName` is a string.

---

## ✨ Usage Example

### Listen to Specific Event Types/Versions

```ts
import { createGlowEventListener, getZoneId } from "@glowlabs-org/events-sdk";

const listener = createGlowEventListener({
  username: "listener",
  password: "your-password-here",
  zoneId: getZoneId("CleanGridProject"),
  queueName: "my.precreated.queue",
  host: "my.rabbitmq.host:5672", // Optional: override the default host
});

listener.onEvent("audit.pushed", "v1", (event) => {
  // event: GlowEvent<AuditPushedV1Payload>
  console.log(
    "Received audit.pushed v1:",
    event.payload.farmId,
    event.zoneId,
    event.zoneName
  );
});

listener.onEvent("audit.slashed", "v1", (event) => {
  // event: GlowEvent<AuditSlashedV1Payload>
  console.log(
    "Received audit.slashed v1:",
    event.payload.farmId,
    event.payload.slasher
  );
});

listener.onEvent("audit.pfees.paid", "v1", (event) => {
  // event: GlowEvent<AuditPfeesPaidV1Payload>
  console.log(
    "Received audit.pfees.paid v1:",
    event.payload.applicationId,
    event.payload.payer,
    event.payload.amount_12Decimals
  );
});

listener.onEvent("audit.pfees.paid", "v2", (event) => {
  // event: GlowEvent<AuditPfeesPaidV2Payload>
  console.log(
    "Received audit.pfees.paid v2:",
    event.payload.farmId,
    event.payload.payer,
    event.payload.amount_12Decimals
  );
});

listener.onEvent("application.created", "v1", (event) => {
  // event: GlowEvent<ApplicationCreatedV1Payload>
  console.log(
    "Received application.created v1:",
    event.payload.gcaAddress,
    event.payload.lat,
    event.payload.lng,
    event.payload.estimatedCostOfPowerPerKWh,
    event.payload.estimatedKWhGeneratedPerYear,
    event.payload.installerCompanyName
  );
});

await listener.start();
// To stop listening:
// await listener.stop();
```

### Emit Events (Admin Only)

```ts
import { createGlowEventEmitter, getZoneId } from "@glowlabs-org/events-sdk";

// You must construct the emitter with a specific zoneId (not 0)
const emitter = createGlowEventEmitter({
  username: "admin",
  password: "your-password-here",
  zoneId: getZoneId("CleanGridProject"), // must be a specific zone
  host: "my.rabbitmq.host:5672", // Optional: override the default host
});

await emitter.emit({
  eventType: "audit.pushed",
  schemaVersion: "v1",
  payload: {
    farmId: "afbc56b6-0b16-4119-b144-025728067ba6", // UUID string
    protocolFeeUSDPrice_12Decimals: "...",
    expectedProduction_12Decimals: "...",
    txHash: "0x...",
  },
});

await emitter.emit({
  eventType: "audit.slashed",
  schemaVersion: "v1",
  payload: {
    farmId: "0x...",
    slasher: "0x...",
    txHash: "0x...",
  },
});

await emitter.emit({
  eventType: "audit.pfees.paid",
  schemaVersion: "v1",
  payload: {
    applicationId: "3ed964b1-4f02-475a-9789-fb74b3466c70", // UUID string
    payer: "0x...",
    amount_12Decimals: "1000000000000",
    txHash: "0x...",
  },
});

await emitter.emit({
  eventType: "audit.pfees.paid",
  schemaVersion: "v2",
  payload: {
    farmId: "0x...",
    payer: "0x...",
    amount_12Decimals: "1000000000000",
    txHash: "0x...",
  },
});

await emitter.emit({
  eventType: "application.created",
  schemaVersion: "v1",
  payload: {
    gcaAddress: "0x...",
    lat: 45.5017,
    lng: -73.5673,
    estimatedCostOfPowerPerKWh: 0.12,
    estimatedKWhGeneratedPerYear: 10000,
    installerCompanyName: "SolarCo",
  },
});

await emitter.disconnect();
```

> **Note:**
>
> - The emitter will automatically publish each event to both the global (zone 0) and the specific zone exchange.
> - You cannot construct an emitter for zoneId: 0, and you cannot specify zoneId per emit call.
> - `schemaVersion` is always a string (e.g., "v1", "v2").
> - **You can override the RabbitMQ host using the `host` option. Default is `turntable.proxy.rlwy.net:50784`.**

### 🌐 Listening to All Zones

You can listen to **all zones at once** by passing `zoneId: 0` and `zoneName: "All Zones"` to the listener. **Emitters must always use a specific zone.**

#### Listen to All Zones

```ts
import { createGlowEventListener } from "@glowlabs-org/events-sdk";

const listener = createGlowEventListener({
  username: "listener",
  password: "your-password-here",
  zoneId: 0, // special value for all zones
  queueName: "my.precreated.queue",
});

listener.onEvent("audit.pushed", "v1", (event) => {
  console.log(
    "Received audit.pushed v1 from any zone:",
    event.payload.farmId,
    event.zoneId,
    event.zoneName
  );
});

// ... other event handlers ...

await listener.start();
// To stop listening:
// await listener.stop();
```

---

## 🧪 Validation & Error Handling

- All events are validated at runtime using Zod schemas.
- If you emit or process an event with a `zoneName` that does not match the `zoneId`, an error is thrown. `zoneId: 0` and `zoneName: "All Zones"` are a valid pairing.
- If you emit or process an event with a `schemaVersion` for which no schema exists (e.g., `audit.pushed` v2), an error is thrown.
- If the payload does not match the schema, an error is thrown.

---

## 🔐 Permissions & Credentials

- **Listener credentials:** Can only subscribe to events. Cannot emit events or create new queues.
- **Admin credentials:** Can subscribe, emit events, and create/bind new queues and exchanges.

If you try to emit with listener credentials, the SDK will throw an error.

---

## 🛠️ Admin & Queue Management

The SDK exposes helpers for programmatically creating, binding, and deleting exchanges and queues (admin credentials required). Use these for pre-creating queues for listeners, bootstrapping environments, or advanced queue management.

### `createExchange(options)`

Creates a topic exchange (default: `exchangeType = "topic"`).

### `bindQueueToExchange(options)`

Binds a queue to a topic exchange. You can specify a `routingKey` for fine-grained event filtering:

- `routingKey = "#"` (default): all events
- `routingKey = "audit.pushed.v1"`: only audit.pushed v1 events
- `routingKey = "audit.pushed.*"`: all versions of audit.pushed

#### Example

```ts
import {
  createExchange,
  bindQueueToExchange,
  deleteExchange,
  deleteQueue,
} from "@glowlabs-org/events-sdk";

await createExchange({
  username: "admin",
  password: "your-password-here",
  exchange: "glow.zone-1.events",
});

await bindQueueToExchange({
  username: "admin",
  password: "your-password-here",
  exchange: "glow.zone-1.events",
  queue: "glow-listener-queue",
  routingKey: "audit.pushed.v1", // only audit.pushed v1 events
});
```

---

## 🔒 Strict Read-Only Listeners

If your listener credentials only have `read` permission (no `configure`), you must consume from a pre-created queue. This is the most secure pattern for production.

### 1. Admin: Pre-create and bind the queue

```ts
import { bindQueueToExchange } from "@glowlabs-org/events-sdk";

await bindQueueToExchange({
  username: "admin",
  password: "your-admin-password",
  exchange: "glow.zone-1.events",
  queue: "my.precreated.queue",
  routingKey: "audit.pushed.v1", // only audit.pushed v1 events
});
```

### 2. Listener: Consume from the pre-created queue

```ts
import { createGlowEventListener } from "@glowlabs-org/events-sdk";

const listener = createGlowEventListener({
  username: "listener",
  password: "your-listener-password",
  zoneId: 1,
  queueName: "my.precreated.queue",
});
```

- The listener will only consume from the pre-created queue and will not attempt to create or bind anything.
- This pattern is required for production environments with strict access control.

---

## 🧩 Advanced: Multiple Listeners/Emitters

You can create multiple listeners or emitters in the same process, each with its own configuration (e.g., for different credentials, exchanges, or RabbitMQ URLs). This is useful for multi-tenant, multi-topic, or advanced scenarios. **Every listener receives every event for the bound routing key(s).**

---

## 🧪 Extending Event Types

To add new event types or versions:

1. Create a new schema in `src/schemas/`.
2. Add the event type and version to `eventTypeRegistry` in `src/event-registry.ts`.
3. Update the base event type in `src/base-event.ts` if needed.

---

## 🏗️ Build & Publish to npm

To build and publish the SDK to npm:

```bash
make build
make publish
make clean
```

- The first time, run `npm login` to authenticate with npm.
- For scoped packages (like `@glowlabs-org/events-sdk`), the Makefile uses `--access public` for publishing.

---

## License

MIT
