@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.
- package/CHANGELOG.md +22 -0
- package/LICENSE +64 -0
- package/README.md +855 -0
- package/dist/adapters/prisma-adapter.d.ts +57 -0
- package/dist/adapters/prisma-adapter.js +181 -0
- package/dist/client/index.d.ts +63 -0
- package/dist/client/index.js +298 -0
- package/dist/index.d.ts +592 -0
- package/dist/index.js +2537 -0
- package/dist/server/fastify-plugin.d.ts +26 -0
- package/dist/server/fastify-plugin.js +125 -0
- package/dist/types-CPJUrmcy.d.ts +288 -0
- package/package.json +75 -0
- package/prisma/schema.prisma +89 -0
|
@@ -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
|
+
}
|