@glowlabs-org/events-sdk
v1.2.0
Published
Typed event SDK for Glow, powered by RabbitMQ and Zod.
Maintainers
Readme
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
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 | Rising Utah | | 3 | Shining Missouri | | 4 | Golden Colorado | | 5 | Rajasthan | | 6 | Florida | | 7 | Lebanon | | 8 | Noble Oklahoma | | 9 | Idaho | | 10 | Virgin Islands | | 11 | Shimmering Michigan | | 12 | Pristine Oregon | | 13 | Arizona | | 14 | New Mexico | | 15 | Maine | | 16 | Georgia |
Use
zoneId: 0(withzoneName: "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: 0andzoneName: "All Zones"everywhere for listeners, but not for emitters.Use the
getZoneIdutility to get the correct zoneId for a given zone name: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) |
| audit.pfees.paid | "v2-alpha" | AuditPfeesPaidV2AlphaPayload | Paid (by applicationId) + currency metadata |
| application.created | "v1" | ApplicationCreatedV1Payload | Emitted when an application is created |
| application.created | "v2-alpha" | ApplicationCreatedV2AlphaPayload | V2-alpha: adds protocol fee price, omits installer |
| audit.pushed | "v2" | AuditPushedV2Payload | Emitted when an audit is pushed (farmId is bytes16) |
| audit.pushed | "v2-alpha" | AuditPushedV2AlphaPayload | Same as v1 but without protocol fee |
| application.price.quote | "v2-alpha" | ApplicationPriceQuoteV2AlphaPayload | Price quote for application |
| gctl.minted | "v2-alpha" | GctlMintedV2AlphaPayload | GCTL minted amount and metadata |
| auditor.fees.paid | "v2-alpha" | AuditorFeesPaidV2AlphaPayload | Auditor fee paid (by applicationId) |
| fraction.created | "v2-alpha" | FractionCreatedV2AlphaPayload | Fraction created with token and ownership details |
| fraction.sold | "v2-alpha" | FractionSoldV2AlphaPayload | Fraction sold between creator and buyer |
| fraction.closed | "v2-alpha" | FractionClosedV2AlphaPayload | Fraction closed/cancelled |
| fraction.refunded | "v2-alpha" | FractionRefundedV2AlphaPayload | Fraction refunded to user |
Environment: All events include an
environmentfield that defaults to"production". You can set it to"staging"when creating an emitter.
Event Types Enum
The SDK provides a TypeScript constant for all supported event types to ensure type safety and avoid typos:
import { eventTypes, EventType } from "./src/event-types";
// Usage example:
const myEventType: EventType = eventTypes.auditPushed;eventTypesis a readonly object containing all event type strings.EventTypeis 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
export interface AuditPushedV1Payload {
farmId: string; // UUID string
protocolFeeUSDPrice_6Decimals: string; // uint256 (decimal) − 12 implied decimals
expectedProduction_12Decimals: string; // uint256 (decimal) − 12 implied decimals
txHash: string; // bytes32 hex string (0x...)
}Validation:
farmIdmust be a valid UUID string (e.g.,afbc56b6-0b16-4119-b144-025728067ba6).protocolFeeUSDPrice_6DecimalsandexpectedProduction_12Decimalsmust be decimal strings representing unsigned big integers.txHashmust be a 32-byte hex string (e.g.,0x...).
audit.slashed v1
export interface AuditSlashedV1Payload {
farmId: string; // bytes16 hex string (0x...)
slasher: string; // Ethereum address (0x...)
txHash: string; // bytes32 hex string (0x...)
}Validation:
farmIdmust be a 16-byte hex string (e.g.,0x...).slashermust be a valid Ethereum address (0x...40 hex chars).txHashmust be a 32-byte hex string (e.g.,0x...).
audit.pfees.paid v1 (by applicationId)
export interface AuditPfeesPaidV1Payload {
applicationId: string; // UUID string
payer: string; // Ethereum address (0x...)
amount_6Decimals: string; // uint256 (decimal) − 6 implied decimals
txHash: string; // bytes32 hex string (0x...)
}Validation:
applicationIdmust be a valid UUID string (e.g.,3ed964b1-4f02-475a-9789-fb74b3466c70).payermust be a valid Ethereum address (0x...40 hex chars).amount_6Decimalsmust be a decimal string representing an unsigned big integer (6 implied decimals).txHashmust be a 32-byte hex string (e.g.,0x...).
audit.pfees.paid v2 (by farmId)
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:
farmIdmust be a 16-byte hex string (e.g.,0x...).payermust be a valid Ethereum address (0x...40 hex chars).amount_12Decimalsmust be a decimal string representing an unsigned big integer (12 implied decimals).txHashmust be a 32-byte hex string (e.g.,0x...).
audit.pushed v2
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...)
}
### `audit.pushed` v2-alpha
```ts
export interface AuditPushedV2AlphaPayload {
farmId: string; // UUID string
expectedProduction_12Decimals: string; // uint256 (decimal) − 12 implied decimals
txHash: string; // bytes32 hex string (0x...)
}audit.pfees.paid v2-alpha
export type PaymentCurrency = "GCTL" | "USDC" | "USDG" | "GLW" | "SGCTL";
export interface AuditPfeesPaidV2AlphaPayload {
applicationId: string; // UUID string
payer: string; // Ethereum address (0x...)
amount_6Decimals: string; // uint256 (decimal) − 6 implied decimals
txHash: string; // bytes32 hex string (0x...), or `sgctl-delegation:<delegationId>` for an sGCTL fill that settled off-chain
paymentCurrency: PaymentCurrency;
paymentEventType: string;
isSponsored: boolean;
}application.price.quote v2-alpha
export interface ApplicationPriceQuoteV2AlphaPayload {
applicationId: string; // UUID string
createdAt: string; // ISO-8601 datetime string
prices: { GCTL: string; GLW: string; USDC: string; USDG: string }; // uint256 decimal strings
signature: string; // 65-byte ECDSA signature hex string (0x + 130 hex); a zeroed bytes32 is also accepted
gcaAddress: string; // Ethereum address (0x...)
}gctl.minted v2-alpha
export interface GctlMintedV2AlphaPayload {
gctlAmount_6Decimals: string; // uint256 (decimal) − 6 implied decimals
timestamp: string; // ISO-8601 datetime string
minter: string; // Ethereum address (0x...)
txHash: string; // bytes32 hex string (0x...)
}auditor.fees.paid v2-alpha
export interface AuditorFeesPaidV2AlphaPayload {
applicationId: string; // UUID string
payer: string; // Ethereum address (0x...)
amount_6Decimals: string; // uint256 (decimal) − 6 implied decimals
txHash: string; // bytes32 hex string (0x...)
}fraction.created v2-alpha
export interface FractionCreatedV2AlphaPayload {
fractionId: string;
transactionHash: string; // bytes32 hex string (0x...)
blockNumber: string; // uint256 (decimal)
logIndex: number;
token: string; // Ethereum address (0x...)
owner: string; // Ethereum address (0x...)
step: string; // uint256 (decimal)
totalSteps: string; // uint256 (decimal)
}fraction.sold v2-alpha
export interface FractionSoldV2AlphaPayload {
fractionId: string;
transactionHash: string; // bytes32 hex string (0x...)
blockNumber: string; // uint256 (decimal)
logIndex: number;
creator: string; // Ethereum address (0x...)
buyer: string; // Ethereum address (0x...)
step: string; // uint256 (decimal)
amount: string; // uint256 (decimal)
timestamp: number;
}fraction.closed v2-alpha
export interface FractionClosedV2AlphaPayload {
fractionId: string;
transactionHash: string; // bytes32 hex string (0x...)
blockNumber: string; // uint256 (decimal)
logIndex: number;
token: string; // Ethereum address (0x...)
owner: string; // Ethereum address (0x...)
timestamp: number;
}Validation:
fractionIdis a string identifier.transactionHashmust be a 32-byte hex string.blockNumber,step,totalSteps, andamountmust be decimal strings representing unsigned big integers.token,owner,creator, andbuyermust be valid Ethereum addresses.
export interface FractionSoldV2AlphaPayload {
fractionId: string;
transactionHash: string; // bytes32 hex string (0x...)
blockNumber: string; // uint256 (decimal)
logIndex: number;
creator: string; // Ethereum address (0x...)
buyer: string; // Ethereum address (0x...)
step: string; // uint256 (decimal)
amount: string; // uint256 (decimal)
timestamp: number;
}fraction.closed v2-alpha
export interface FractionClosedV2AlphaPayload {
fractionId: string;
transactionHash: string; // bytes32 hex string (0x...)
blockNumber: string; // uint256 (decimal)
logIndex: number;
token: string; // Ethereum address (0x...)
owner: string; // Ethereum address (0x...)
timestamp: number;
}fraction.refunded v2-alpha
export interface FractionRefundedV2AlphaPayload {
fractionId: string; // hex string
creator: string; // Ethereum address (0x...)
user: string; // Ethereum address (0x...)
refundTo: string; // Ethereum address (0x...)
amount: string; // uint256 (decimal)
timestamp: number;
blockNumber: string; // uint256 (decimal)
transactionHash: string; // bytes32 hex string (0x...)
logIndex: number;
}application.created v2-alpha
export interface ApplicationCreatedV2AlphaPayload {
gcaAddress: string; // Ethereum address (0x...)
lat: number;
lng: number;
estimatedCostOfPowerPerKWh: number;
estimatedKWhGeneratedPerYear: number;
estimatedProtocolFeeUSDPrice_6Decimals: string; // uint256 (decimal) − 6 implied decimals
}Validation:
gcaAddressmust be a valid Ethereum address (0x...40 hex chars).latandlngare numbers (coordinates).estimatedCostOfPowerPerKWhandestimatedKWhGeneratedPerYearare numbers.installerCompanyNameis a string.
✨ Usage Example
Listen to Specific Event Types/Versions
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_6Decimals
);
});
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();Reconnecting after a dropped connection
Since 1.2.0, a listener with an onDisconnect handler survives a dropped
RabbitMQ connection. When the connection or channel drops after start()
succeeded, it stops consuming and calls each handler once. Call start() again
to reconnect. stop() never calls the handlers. With a queueName, messages
wait in that queue meanwhile; the default exclusive queue is deleted with the
connection, so its messages are lost.
A listener with no onDisconnect handler keeps the old behavior: a drop ends
the process, so its supervisor (Railway, Docker) can restart it.
listener.onDisconnect((error) => {
console.error("Listener disconnected:", error?.message);
// Retry with backoff until start() succeeds.
});The emitter forgets a dropped connection and reconnects on the next emit().
Emit Events (Admin Only)
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
environment: "staging", // Optional: defaults to "production"
});
await emitter.emit({
eventType: "audit.pushed",
schemaVersion: "v1",
payload: {
farmId: "afbc56b6-0b16-4119-b144-025728067ba6", // UUID string
protocolFeeUSDPrice_6Decimals: "...",
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_6Decimals: "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.
schemaVersionis always a string (e.g., "v1", "v2", "v2-alpha").- You can override the RabbitMQ host using the
hostoption. Default isturntable.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
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
zoneNamethat does not match thezoneId, an error is thrown.zoneId: 0andzoneName: "All Zones"are a valid pairing. - If you emit or process an event with a
schemaVersionfor which no schema exists (e.g.,audit.pushedv2), 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 eventsroutingKey = "audit.pushed.v1": only audit.pushed v1 eventsroutingKey = "audit.pushed.*": all versions of audit.pushed
Example
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
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
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:
- Create a new schema in
src/schemas/. - Add the event type and version to
eventTypeRegistryinsrc/event-registry.ts. - Update the base event type in
src/base-event.tsif needed.
🏗️ Build & Publish to npm
To build and publish the SDK to npm:
make build
make publish
make clean- The first time, run
npm loginto authenticate with npm. - For scoped packages (like
@glowlabs-org/events-sdk), the Makefile uses--access publicfor publishing.
License
MIT
