@zi2/relay-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ import { h as RelaySDK } from '../types-CPJUrmcy.js';
2
+ import 'ws';
3
+
4
+ /**
5
+ * Fastify plugin for ZI2 Relay SDK.
6
+ * Registers all relay REST routes and WebSocket upgrade handler.
7
+ *
8
+ * Requires @fastify/websocket for WebSocket support.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import Fastify from 'fastify';
13
+ * import websocket from '@fastify/websocket';
14
+ * import { relayPlugin } from '@zi2/relay-sdk/fastify';
15
+ *
16
+ * const app = Fastify();
17
+ * await app.register(websocket);
18
+ * await app.register(relayPlugin, { sdk, prefix: '/phone-relay' });
19
+ * ```
20
+ */
21
+ declare function relayPlugin(fastify: any, opts: {
22
+ sdk: RelaySDK;
23
+ prefix?: string;
24
+ }): Promise<void>;
25
+
26
+ export { relayPlugin };
@@ -0,0 +1,125 @@
1
+ // src/schemas.ts
2
+ import { z } from "zod";
3
+ var completePairingSchema = z.object({
4
+ pairingId: z.string().uuid(),
5
+ pairingToken: z.string().min(1),
6
+ devicePublicKey: z.string().min(44).max(5e3),
7
+ deviceName: z.string().min(1).max(200),
8
+ platform: z.enum(["android", "ios"]),
9
+ phoneNumber: z.string().max(30).optional()
10
+ });
11
+ var updatePhoneRelaySchema = z.object({
12
+ deviceName: z.string().min(1).max(200).optional(),
13
+ maxSmsPerMinute: z.number().int().min(1).max(60).optional(),
14
+ maxSmsPerHour: z.number().int().min(1).max(1e3).optional(),
15
+ maxSmsPerDay: z.number().int().min(1).max(1e4).optional()
16
+ });
17
+ var testRelaySmsSchema = z.object({
18
+ to: z.string().min(1),
19
+ body: z.string().min(1).max(160).optional()
20
+ });
21
+ var relayMessagesQuerySchema = z.object({
22
+ limit: z.coerce.number().int().min(1).max(100).default(50),
23
+ offset: z.coerce.number().int().min(0).default(0)
24
+ });
25
+
26
+ // src/server/routes.ts
27
+ function createRouteHandlers(sdk) {
28
+ return {
29
+ async initiatePairing(orgId, userId) {
30
+ const result = await sdk.initiatePairing(orgId, userId);
31
+ return result;
32
+ },
33
+ async completePairing(body) {
34
+ const parsed = completePairingSchema.parse(body);
35
+ const result = await sdk.completePairing(
36
+ parsed.pairingId,
37
+ parsed.pairingToken,
38
+ parsed.devicePublicKey,
39
+ parsed.deviceName,
40
+ parsed.platform,
41
+ parsed.phoneNumber
42
+ );
43
+ return result;
44
+ },
45
+ async listRelays(orgId) {
46
+ const relays = await sdk.listRelays(orgId);
47
+ return relays;
48
+ },
49
+ async getRelay(id, orgId) {
50
+ const relay = await sdk.getRelay(id, orgId);
51
+ return relay;
52
+ },
53
+ async updateRelay(id, orgId, body) {
54
+ const parsed = updatePhoneRelaySchema.parse(body);
55
+ const relay = await sdk.updateRelay(id, orgId, parsed);
56
+ return relay;
57
+ },
58
+ async revokeRelay(id, orgId) {
59
+ await sdk.revokeRelay(id, orgId);
60
+ },
61
+ async testSms(id, orgId, body) {
62
+ const parsed = testRelaySmsSchema.parse(body);
63
+ const testBody = parsed.body || "ZI2 Relay test message";
64
+ const result = await sdk.sendSMS(id, orgId, parsed.to, testBody);
65
+ return result;
66
+ },
67
+ async getMessages(id, orgId, query) {
68
+ const parsed = relayMessagesQuerySchema.parse(query);
69
+ const result = await sdk.getMessages(id, orgId, parsed.limit, parsed.offset);
70
+ return result;
71
+ }
72
+ };
73
+ }
74
+
75
+ // src/server/fastify-plugin.ts
76
+ async function relayPlugin(fastify, opts) {
77
+ const handlers = createRouteHandlers(opts.sdk);
78
+ const prefix = opts.prefix || "";
79
+ fastify.post(`${prefix}/pair/initiate`, async (request, reply) => {
80
+ const result = await handlers.initiatePairing(request.orgId, request.userId);
81
+ reply.send({ success: true, data: result });
82
+ });
83
+ fastify.post(`${prefix}/pair/complete`, async (request, reply) => {
84
+ const result = await handlers.completePairing(request.body);
85
+ reply.send({ success: true, data: result });
86
+ });
87
+ fastify.get(`${prefix}/`, async (request, reply) => {
88
+ const relays = await handlers.listRelays(request.orgId);
89
+ reply.send({ success: true, data: relays });
90
+ });
91
+ fastify.get(`${prefix}/:id`, async (request, reply) => {
92
+ const { id } = request.params;
93
+ const relay = await handlers.getRelay(id, request.orgId);
94
+ reply.send({ success: true, data: relay });
95
+ });
96
+ fastify.patch(`${prefix}/:id`, async (request, reply) => {
97
+ const { id } = request.params;
98
+ await handlers.updateRelay(id, request.orgId, request.body);
99
+ reply.send({ success: true });
100
+ });
101
+ fastify.delete(`${prefix}/:id`, async (request, reply) => {
102
+ const { id } = request.params;
103
+ await handlers.revokeRelay(id, request.orgId);
104
+ reply.send({ success: true });
105
+ });
106
+ fastify.post(`${prefix}/:id/test`, async (request, reply) => {
107
+ const { id } = request.params;
108
+ const result = await handlers.testSms(id, request.orgId, request.body);
109
+ reply.send({ success: true, data: result });
110
+ });
111
+ fastify.get(`${prefix}/:id/messages`, async (request, reply) => {
112
+ const { id } = request.params;
113
+ const result = await handlers.getMessages(id, request.orgId, request.query);
114
+ reply.send({ success: true, data: result });
115
+ });
116
+ fastify.get("/ws/relay", { websocket: true }, (socket, req) => {
117
+ opts.sdk.handleWebSocket(socket, {
118
+ ip: req.ip || req.socket?.remoteAddress || "unknown",
119
+ log: req.log
120
+ });
121
+ });
122
+ }
123
+ export {
124
+ relayPlugin
125
+ };
@@ -0,0 +1,288 @@
1
+ import { WebSocket } from 'ws';
2
+
3
+ interface PhoneRelayRecord {
4
+ id: string;
5
+ organizationId: string;
6
+ deviceName: string;
7
+ platform: string;
8
+ phoneNumber: string | null;
9
+ devicePublicKey: string;
10
+ sharedKeyEncrypted: string;
11
+ authTokenHash: string;
12
+ status: string;
13
+ keyVersion: number;
14
+ lastKeyRotation: Date;
15
+ lastSeenAt: Date | null;
16
+ lastIpAddress: string | null;
17
+ batteryLevel: number | null;
18
+ signalStrength: number | null;
19
+ totalSmsSent: number;
20
+ totalSmsFailed: number;
21
+ dailySmsSent: number;
22
+ dailyResetAt: Date;
23
+ maxSmsPerMinute: number;
24
+ maxSmsPerHour: number;
25
+ maxSmsPerDay: number;
26
+ pairedById: string;
27
+ createdAt: Date;
28
+ updatedAt: Date;
29
+ }
30
+ interface PhoneRelayPairingRecord {
31
+ id: string;
32
+ organizationId: string;
33
+ pairingToken: string;
34
+ serverPublicKey: string;
35
+ serverPrivateKeyEncrypted: string;
36
+ status: string;
37
+ initiatedById: string;
38
+ expiresAt: Date;
39
+ createdAt: Date;
40
+ }
41
+ interface RelayMessageRecord {
42
+ id: string;
43
+ relayId: string;
44
+ organizationId: string;
45
+ encryptedPayload: string;
46
+ status: string;
47
+ attempts: number;
48
+ maxAttempts: number;
49
+ nativeMessageId: string | null;
50
+ errorMessage: string | null;
51
+ deliveryStatus: string | null;
52
+ sentToDeviceAt: Date | null;
53
+ ackedAt: Date | null;
54
+ expiresAt: Date;
55
+ createdAt: Date;
56
+ updatedAt: Date;
57
+ }
58
+ type CreateRelayInput = Omit<PhoneRelayRecord, 'id' | 'createdAt' | 'updatedAt' | 'keyVersion' | 'totalSmsSent' | 'totalSmsFailed' | 'dailySmsSent'>;
59
+ type CreatePairingInput = Omit<PhoneRelayPairingRecord, 'id' | 'createdAt'>;
60
+ type CreateMessageInput = Pick<RelayMessageRecord, 'relayId' | 'organizationId' | 'encryptedPayload' | 'expiresAt'>;
61
+ interface DatabaseAdapter {
62
+ findRelay(id: string, orgId?: string): Promise<PhoneRelayRecord | null>;
63
+ findRelayByTokenHash(id: string, tokenHash: string, statuses: string[]): Promise<PhoneRelayRecord | null>;
64
+ findRelays(orgId: string, excludeStatus?: string): Promise<PhoneRelayRecord[]>;
65
+ createRelay(data: CreateRelayInput): Promise<PhoneRelayRecord>;
66
+ updateRelay(id: string, data: Partial<PhoneRelayRecord>): Promise<void>;
67
+ updateRelayByOrg(id: string, orgId: string, data: Partial<PhoneRelayRecord>): Promise<void>;
68
+ deleteRelay(id: string): Promise<void>;
69
+ incrementRelayStat(id: string, field: 'totalSmsSent' | 'totalSmsFailed' | 'dailySmsSent', amount?: number): Promise<void>;
70
+ countRelays(orgId: string): Promise<number>;
71
+ markDegradedRelays(threshold: Date): Promise<number>;
72
+ resetDailyCounters(): Promise<number>;
73
+ findPairing(id: string): Promise<PhoneRelayPairingRecord | null>;
74
+ createPairing(data: CreatePairingInput): Promise<PhoneRelayPairingRecord>;
75
+ updatePairingStatus(id: string, status: string): Promise<void>;
76
+ deleteExpiredPairings(): Promise<number>;
77
+ findMessage(id: string): Promise<RelayMessageRecord | null>;
78
+ createMessage(data: CreateMessageInput): Promise<RelayMessageRecord>;
79
+ updateMessage(id: string, data: Partial<RelayMessageRecord>): Promise<void>;
80
+ findPendingMessages(relayId: string, limit: number): Promise<RelayMessageRecord[]>;
81
+ findMessages(relayId: string, limit: number, offset: number): Promise<{
82
+ messages: RelayMessageRecord[];
83
+ total: number;
84
+ }>;
85
+ deleteMessagesByRelay(relayId: string): Promise<number>;
86
+ expireStaleMessages(): Promise<number>;
87
+ createMessages(batch: CreateMessageInput[]): Promise<RelayMessageRecord[]>;
88
+ updateMessages(ids: string[], data: Partial<RelayMessageRecord>): Promise<number>;
89
+ }
90
+ interface EncryptionAdapter {
91
+ encrypt(plaintext: string): string;
92
+ decrypt(ciphertext: string): string;
93
+ }
94
+ interface BroadcastAdapter {
95
+ broadcast(orgId: string, message: Record<string, unknown>): void;
96
+ }
97
+ interface LoggerAdapter {
98
+ debug(msg: string, data?: Record<string, unknown>): void;
99
+ info(msg: string, data?: Record<string, unknown>): void;
100
+ warn(msg: string, data?: Record<string, unknown>): void;
101
+ error(msg: string, data?: Record<string, unknown>): void;
102
+ }
103
+ interface ConnectionBroker {
104
+ publish(channel: string, message: string): Promise<void>;
105
+ subscribe(channel: string, handler: (message: string) => void): Promise<void>;
106
+ unsubscribe(channel: string): Promise<void>;
107
+ }
108
+ /**
109
+ * Audit trail adapter for PCI DSS v4 Req 10 / SOC 2 CC7.2.
110
+ * Records all security-relevant actions for compliance logging.
111
+ */
112
+ interface AuditAdapter {
113
+ log(entry: AuditEntry): Promise<void>;
114
+ }
115
+ interface AuditEntry {
116
+ timestamp: Date;
117
+ action: 'pairing.initiated' | 'pairing.completed' | 'relay.revoked' | 'relay.keys_rotated' | 'sms.sent' | 'sms.failed' | 'sms.delivered' | 'relay.connected' | 'relay.disconnected' | 'relay.auth_failed' | 'relay.rate_limited' | 'relay.updated';
118
+ organizationId: string;
119
+ relayId?: string;
120
+ userId?: string;
121
+ ip?: string;
122
+ metadata?: Record<string, unknown>;
123
+ }
124
+ interface SmsSendResponse {
125
+ messageId?: string;
126
+ success: boolean;
127
+ }
128
+ interface SmsProviderAdapter {
129
+ readonly name: string;
130
+ initialize(credentials: Record<string, string>): void;
131
+ sendSms(to: string, body: string): Promise<SmsSendResponse>;
132
+ validateCredentials(credentials: Record<string, string>): Promise<boolean>;
133
+ }
134
+ interface RelaySDKConfig {
135
+ db: DatabaseAdapter;
136
+ encryption: EncryptionAdapter;
137
+ broadcast?: BroadcastAdapter;
138
+ logger?: LoggerAdapter;
139
+ /** Audit trail for PCI DSS v4 Req 10 / SOC 2 CC7.2 compliance */
140
+ audit?: AuditAdapter;
141
+ apiUrl?: string;
142
+ wsUrl?: string;
143
+ /** Enforce TLS-only WebSocket connections (PCI DSS v4 Req 4.2.1). Default: true */
144
+ enforceTls?: boolean;
145
+ /** Maximum auth attempts before IP blacklist (PCI DSS v4 Req 8.3.4). Default: 10 */
146
+ maxAuthFailuresPerIp?: number;
147
+ /** Called after pairing completes. Use to auto-create SmsProvider records. */
148
+ onPairingComplete?: (relay: {
149
+ relayId: string;
150
+ orgId: string;
151
+ deviceName: string;
152
+ phoneNumber?: string;
153
+ }) => Promise<void>;
154
+ /** Called after relay is revoked. Use to clean up SmsProvider records. */
155
+ onRelayRevoked?: (relay: {
156
+ relayId: string;
157
+ orgId: string;
158
+ }) => Promise<void>;
159
+ /** Override default rate limits. Uses same keys as RELAY_LIMITS constants. */
160
+ limits?: Partial<{
161
+ AUTH_TIMEOUT_MS: number;
162
+ HEARTBEAT_INTERVAL_MS: number;
163
+ PAIRING_EXPIRY_MINUTES: number;
164
+ MESSAGE_EXPIRY_HOURS: number;
165
+ QUEUE_DRAIN_DELAY_MS: number;
166
+ DEFAULT_SMS_TIMEOUT_MS: number;
167
+ MAX_SMS_PER_MINUTE: number;
168
+ MAX_SMS_PER_HOUR: number;
169
+ MAX_SMS_PER_DAY: number;
170
+ DEGRADED_THRESHOLD_MS: number;
171
+ }>;
172
+ }
173
+ interface PairingResult {
174
+ pairingId: string;
175
+ serverPublicKey: string;
176
+ pairingToken: string;
177
+ wsUrl: string;
178
+ }
179
+ interface PairingCompleteResult {
180
+ relayId: string;
181
+ authToken: string;
182
+ }
183
+ interface SendResult {
184
+ messageId: string;
185
+ status: string;
186
+ }
187
+ interface RelayListItem {
188
+ id: string;
189
+ deviceName: string;
190
+ platform: string;
191
+ phoneNumber: string | null;
192
+ status: string;
193
+ lastSeenAt: Date | null;
194
+ batteryLevel: number | null;
195
+ signalStrength: number | null;
196
+ totalSmsSent: number;
197
+ totalSmsFailed: number;
198
+ dailySmsSent: number;
199
+ maxSmsPerMinute: number;
200
+ maxSmsPerHour: number;
201
+ maxSmsPerDay: number;
202
+ isOnline: boolean;
203
+ createdAt: Date;
204
+ }
205
+ interface RelayDetail {
206
+ id: string;
207
+ organizationId: string;
208
+ deviceName: string;
209
+ platform: string;
210
+ phoneNumber: string | null;
211
+ status: string;
212
+ keyVersion: number;
213
+ lastKeyRotation: Date;
214
+ lastSeenAt: Date | null;
215
+ lastIpAddress: string | null;
216
+ batteryLevel: number | null;
217
+ signalStrength: number | null;
218
+ totalSmsSent: number;
219
+ totalSmsFailed: number;
220
+ dailySmsSent: number;
221
+ dailyResetAt: Date;
222
+ maxSmsPerMinute: number;
223
+ maxSmsPerHour: number;
224
+ maxSmsPerDay: number;
225
+ pairedById: string;
226
+ isOnline: boolean;
227
+ createdAt: Date;
228
+ updatedAt: Date;
229
+ }
230
+ interface MessageList {
231
+ messages: RelayMessageRecord[];
232
+ pagination: {
233
+ total: number;
234
+ limit: number;
235
+ offset: number;
236
+ };
237
+ }
238
+ interface FallbackConfig {
239
+ primary: SmsProviderAdapter;
240
+ fallbacks: SmsProviderAdapter[];
241
+ isOnline: () => boolean;
242
+ onFallback?: (provider: string) => void;
243
+ }
244
+ interface RelaySDK {
245
+ initiatePairing(orgId: string, userId: string): Promise<PairingResult>;
246
+ completePairing(input: {
247
+ pairingId: string;
248
+ pairingToken: string;
249
+ devicePublicKey: string;
250
+ deviceName: string;
251
+ platform: string;
252
+ phoneNumber?: string;
253
+ }): Promise<PairingCompleteResult>;
254
+ listRelays(orgId: string): Promise<RelayListItem[]>;
255
+ getRelay(id: string, orgId: string): Promise<RelayDetail | null>;
256
+ updateRelay(id: string, orgId: string, data: Record<string, unknown>): Promise<void>;
257
+ revokeRelay(id: string, orgId: string): Promise<void>;
258
+ rotateKeys(id: string, orgId: string): Promise<void>;
259
+ sendSMS(options: {
260
+ relayId: string;
261
+ orgId: string;
262
+ to: string;
263
+ body: string;
264
+ timeoutMs?: number;
265
+ }): Promise<SendResult>;
266
+ getMessages(relayId: string, orgId: string, limit?: number, offset?: number): Promise<MessageList>;
267
+ isRelayOnline(relayId: string): boolean;
268
+ getRelaySocket(relayId: string): WebSocket | undefined;
269
+ handleWebSocket(socket: WebSocket, request: {
270
+ ip: string;
271
+ log?: LoggerAdapter;
272
+ }): void;
273
+ createProvider(relayId: string, orgId: string): SmsProviderAdapter;
274
+ createFallbackProvider(config: FallbackConfig): SmsProviderAdapter;
275
+ onRelayOnline(handler: (relayId: string, orgId: string) => void): () => void;
276
+ onRelayOffline(handler: (relayId: string, orgId: string) => void): () => void;
277
+ onMessageDelivered(handler: (messageId: string, relayId: string) => void): () => void;
278
+ onMessageFailed(handler: (messageId: string, relayId: string, error: string) => void): () => void;
279
+ runHealthCheck(): Promise<{
280
+ expiredMessages: number;
281
+ degradedRelays: number;
282
+ cleanedPairings: number;
283
+ }>;
284
+ startHealthService(): void;
285
+ stopHealthService(): void;
286
+ }
287
+
288
+ export type { AuditAdapter as A, BroadcastAdapter as B, ConnectionBroker as C, DatabaseAdapter as D, EncryptionAdapter as E, FallbackConfig as F, LoggerAdapter as L, MessageList as M, PhoneRelayRecord as P, RelayMessageRecord as R, SmsProviderAdapter as S, AuditEntry as a, SmsSendResponse as b, CreateRelayInput as c, PhoneRelayPairingRecord as d, CreatePairingInput as e, CreateMessageInput as f, RelaySDKConfig as g, RelaySDK as h, PairingCompleteResult as i, PairingResult as j, RelayDetail as k, RelayListItem as l, SendResult as m };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@zi2/relay-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Enterprise SMS relay SDK with E2E encryption, provider fallback, and PCI DSS v4 compliance",
5
+ "author": "Zenith Intelligence Technologies <dev@zisquare.app>",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "homepage": "https://zisquare.app/dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/nicad-tech/zi2-relay-sdk"
11
+ },
12
+ "keywords": ["sms", "relay", "e2e-encryption", "websocket", "pci-dss", "soc2", "twilio-alternative", "sms-gateway"],
13
+ "type": "module",
14
+ "files": ["dist/", "prisma/", "README.md", "CHANGELOG.md", "LICENSE"],
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "types": "./dist/index.d.ts"
21
+ },
22
+ "./client": {
23
+ "import": "./dist/client/index.js",
24
+ "types": "./dist/client/index.d.ts"
25
+ },
26
+ "./adapters/prisma": {
27
+ "import": "./dist/adapters/prisma-adapter.js",
28
+ "types": "./dist/adapters/prisma-adapter.d.ts"
29
+ },
30
+ "./fastify": {
31
+ "import": "./dist/server/fastify-plugin.js",
32
+ "types": "./dist/server/fastify-plugin.d.ts"
33
+ }
34
+ },
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "clean": "rm -rf dist .turbo"
42
+ },
43
+ "dependencies": {
44
+ "zod": "^3.24.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@prisma/client": ">=5.0.0",
48
+ "fastify": ">=4.0.0",
49
+ "ws": ">=8.0.0",
50
+ "ioredis": ">=5.0.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "@prisma/client": {
54
+ "optional": true
55
+ },
56
+ "fastify": {
57
+ "optional": true
58
+ },
59
+ "ws": {
60
+ "optional": true
61
+ },
62
+ "ioredis": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "devDependencies": {
67
+ "@capacitor/core": "^8.3.0",
68
+ "@capacitor/preferences": "^6.0.0",
69
+ "@types/ws": "^8.5.0",
70
+ "@zi2/tsconfig": "workspace:*",
71
+ "tsup": "^8.3.0",
72
+ "typescript": "^5.7.0",
73
+ "vitest": "^2.1.0"
74
+ }
75
+ }
@@ -0,0 +1,89 @@
1
+ // ============================================================================
2
+ // @zi2/relay-sdk — Reference Prisma Schema
3
+ //
4
+ // These are the relay-specific models required by the SDK. Copy them into your
5
+ // own schema.prisma (adjusting the datasource and any relations to your app's
6
+ // models such as Organization). The PrismaAdapter expects these tables to exist.
7
+ // ============================================================================
8
+
9
+ generator client {
10
+ provider = "prisma-client-js"
11
+ }
12
+
13
+ datasource db {
14
+ provider = "postgresql"
15
+ url = env("DATABASE_URL")
16
+ }
17
+
18
+ model PhoneRelay {
19
+ id String @id @default(uuid()) @db.Uuid
20
+ organizationId String @map("organization_id") @db.Uuid
21
+ deviceName String @map("device_name") @db.VarChar(200)
22
+ platform String @db.VarChar(20)
23
+ phoneNumber String? @map("phone_number") @db.VarChar(30)
24
+ devicePublicKey String @map("device_public_key") @db.Text
25
+ sharedKeyEncrypted String @map("shared_key_encrypted") @db.Text
26
+ authTokenHash String @map("auth_token_hash") @db.VarChar(128)
27
+ status String @default("active") @db.VarChar(20)
28
+ keyVersion Int @default(1) @map("key_version")
29
+ lastKeyRotation DateTime? @map("last_key_rotation")
30
+ lastSeenAt DateTime? @map("last_seen_at")
31
+ lastIpAddress String? @map("last_ip_address") @db.VarChar(45)
32
+ batteryLevel Int? @map("battery_level")
33
+ signalStrength Int? @map("signal_strength")
34
+ totalSmsSent Int @default(0) @map("total_sms_sent")
35
+ totalSmsFailed Int @default(0) @map("total_sms_failed")
36
+ dailySmsSent Int @default(0) @map("daily_sms_sent")
37
+ dailyResetAt DateTime? @map("daily_reset_at")
38
+ maxSmsPerMinute Int @default(20) @map("max_sms_per_minute")
39
+ maxSmsPerHour Int @default(200) @map("max_sms_per_hour")
40
+ maxSmsPerDay Int @default(1000) @map("max_sms_per_day")
41
+ pairedById String @map("paired_by_id") @db.Uuid
42
+ createdAt DateTime @default(now()) @map("created_at")
43
+ updatedAt DateTime @updatedAt @map("updated_at")
44
+
45
+ messages RelayMessageQueue[]
46
+
47
+ @@index([organizationId])
48
+ @@index([authTokenHash])
49
+ @@map("phone_relays")
50
+ }
51
+
52
+ model PhoneRelayPairing {
53
+ id String @id @default(uuid()) @db.Uuid
54
+ organizationId String @map("organization_id") @db.Uuid
55
+ pairingToken String @unique @map("pairing_token") @db.VarChar(128)
56
+ serverPublicKey String @map("server_public_key") @db.Text
57
+ serverPrivateKeyEncrypted String @map("server_private_key_encrypted") @db.Text
58
+ status String @default("pending") @db.VarChar(20)
59
+ initiatedById String @map("initiated_by_id") @db.Uuid
60
+ expiresAt DateTime @map("expires_at")
61
+ createdAt DateTime @default(now()) @map("created_at")
62
+
63
+ @@index([pairingToken])
64
+ @@map("phone_relay_pairings")
65
+ }
66
+
67
+ model RelayMessageQueue {
68
+ id String @id @default(uuid()) @db.Uuid
69
+ relayId String @map("relay_id") @db.Uuid
70
+ organizationId String @map("organization_id") @db.Uuid
71
+ encryptedPayload String @map("encrypted_payload") @db.Text
72
+ status String @default("pending") @db.VarChar(20)
73
+ attempts Int @default(0)
74
+ maxAttempts Int @default(3) @map("max_attempts")
75
+ nativeMessageId String? @map("native_message_id") @db.VarChar(200)
76
+ errorMessage String? @map("error_message") @db.Text
77
+ deliveryStatus String? @map("delivery_status") @db.VarChar(20)
78
+ sentToDeviceAt DateTime? @map("sent_to_device_at")
79
+ ackedAt DateTime? @map("acked_at")
80
+ expiresAt DateTime @map("expires_at")
81
+ createdAt DateTime @default(now()) @map("created_at")
82
+ updatedAt DateTime @updatedAt @map("updated_at")
83
+
84
+ relay PhoneRelay @relation(fields: [relayId], references: [id], onDelete: Cascade)
85
+
86
+ @@index([relayId, status])
87
+ @@index([expiresAt])
88
+ @@map("relay_message_queue")
89
+ }