sms-panel-manager 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-11-06
4
+
5
+ - Added typed actions and matching runtime validation.
6
+ - Added credential encryption and privacy fingerprints.
7
+ - Added Mongoose configuration, operation, attempt, TTL, and idempotency support.
8
+ - Added one-attempt orchestration and parameter bindings.
9
+ - Added offline-tested Ghasedak, Kavenegar, IPPanel, and FarazSMS adapters.
10
+ - Added optional guarded NestJS integration.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sms-panel-manager contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # sms-panel-manager
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Backend-only, strongly typed transactional SMS orchestration for Node.js 20+.
4
+
5
+ Actions live in version-controlled TypeScript, use the same definitions for runtime validation, and map semantic parameters to provider templates. The package includes secure Mongoose persistence and optional NestJS integration. It does not ship a frontend, authentication system, campaign engine, or complete OTP/session flow.
6
+
7
+ ## Status
8
+
9
+ Version `0.1.0` is the initial development foundation release. Normal tests never contact an SMS provider.
10
+
11
+ | Adapter | Binding model | Safe credential check |
12
+ | ---------------------- | -------------------------- | --------------------- |
13
+ | Ghasedak | Ordered `param1`–`param10` | Account information |
14
+ | Kavenegar | Fixed token slots | Account information |
15
+ | IPPanel Edge | Named pattern parameters | Token/profile check |
16
+ | FarazSMS / IranPayamak | Named attributes | Account balance |
17
+
18
+ Delivery polling is declared as a capability but is not yet exposed as a manager operation. MeliPayamak, SMS.ir, and Asanak are deferred until their API generation or authoritative public contract is locked.
19
+
20
+ ## Typed actions
21
+
22
+ ```ts
23
+ import { createSmsManager, defineSmsActions, sms } from "sms-panel-manager";
24
+
25
+ const actions = defineSmsActions({
26
+ package_posted: {
27
+ title: "ارسال مرسوله",
28
+ params: {
29
+ customerName: sms.string({ title: "نام مشتری", minLength: 1 }),
30
+ trackingCode: sms.string({ title: "کد رهگیری" }),
31
+ note: sms.string({ title: "یادداشت" }).optional(),
32
+ },
33
+ },
34
+ });
35
+
36
+ const manager = createSmsManager({
37
+ actions,
38
+ executor: {
39
+ async execute() {
40
+ return { operationId: "example", status: "submitted" };
41
+ },
42
+ },
43
+ });
44
+
45
+ await manager.send(
46
+ "package_posted",
47
+ {
48
+ recipient: "09123456789",
49
+ customerName: "علی",
50
+ trackingCode: "123456789",
51
+ },
52
+ { idempotencyKey: "shipment:123:posted:v1" },
53
+ );
54
+ ```
55
+
56
+ The first argument determines the exact second-argument type. The same definition produces strict Zod validation and safe administration metadata. V1 parameters are string, number, integer, boolean, date, and enum.
57
+
58
+ ## Sending behavior
59
+
60
+ `SmsOrchestrator` performs one provider attempt. It reserves the logical operation before dispatch, fingerprints the request and recipient with HMAC-SHA-256, applies named/ordered/fixed-slot bindings, and never automatically falls back. An unexpected adapter exception becomes `unknown`, because retrying an ambiguous submission could send a duplicate SMS.
61
+
62
+ Reusing an idempotency key with the same request returns its operation; using it with different data throws `idempotency_conflict`.
63
+
64
+ ## Mongoose persistence
65
+
66
+ ```ts
67
+ import mongoose from "mongoose";
68
+ import {
69
+ createSmsMongooseModels,
70
+ MongooseSendOperationStore,
71
+ } from "sms-panel-manager/mongoose";
72
+
73
+ const models = createSmsMongooseModels(mongoose.connection);
74
+ const operations = new MongooseSendOperationStore(models);
75
+ ```
76
+
77
+ The caller owns the connection; no hidden global connection is opened. Models cover provider/action configuration, logical operations, and physical attempts. Unique indexes enforce action configuration and idempotency. Retention uses TTL indexes.
78
+
79
+ ## Credential security
80
+
81
+ Credentials use AES-256-GCM with unique nonces and authenticated configuration, adapter, scope, revision, schema, and key IDs.
82
+
83
+ ```ts
84
+ import { createEnvironmentKeyring } from "sms-panel-manager";
85
+
86
+ const keyring = createEnvironmentKeyring({
87
+ activeKeyId: "2026-01",
88
+ keys: { "2026-01": process.env.SMS_CONFIG_ENCRYPTION_KEY_BASE64! },
89
+ });
90
+ ```
91
+
92
+ Keys must decode to 32 bytes. Retain historical key IDs while old ciphertext exists, then re-encrypt. Never store the master key in MongoDB. Credentials, message bodies, semantic values, and raw provider responses are not retained by default.
93
+
94
+ ## Provider adapters
95
+
96
+ ```ts
97
+ import { GhasedakAdapter } from "sms-panel-manager/providers";
98
+ ```
99
+
100
+ Adapters use isolated Axios clients with HTTPS-only fixed base URLs, bounded timeouts, redirects disabled, abort signals, and Zod response parsing. Transport injection supports offline contract tests. Malformed success responses and transport exceptions become unknown outcomes rather than automatic retries.
101
+
102
+ ## NestJS
103
+
104
+ ```ts
105
+ import {
106
+ createSmsManagerToken,
107
+ SmsPanelModule,
108
+ type InjectedSmsManager,
109
+ } from "sms-panel-manager/nestjs";
110
+
111
+ const SMS_MANAGER = createSmsManagerToken(actions);
112
+ type ApplicationSmsManager = InjectedSmsManager<typeof SMS_MANAGER>;
113
+
114
+ SmsPanelModule.forRoot({ actions, token: SMS_MANAGER, executor });
115
+ ```
116
+
117
+ Administration controllers are absent by default. Enabling them without a host guard throws during registration:
118
+
119
+ ```ts
120
+ SmsPanelModule.forRoot({
121
+ actions,
122
+ token: SMS_MANAGER,
123
+ executor,
124
+ admin: { enabled: true, guard: ApplicationAdminGuard },
125
+ });
126
+ ```
127
+
128
+ The foundation currently exposes guarded `GET /admin/sms/actions` metadata only. The package never invents host roles.
129
+
130
+ ## Development
131
+
132
+ ```sh
133
+ npm install
134
+ npm run check
135
+ ```
136
+
137
+ Checks include strict TypeScript, ESLint, Vitest, a real local MongoDB concurrency test, ESM/CommonJS builds, declarations, and installed-tarball consumer checks. MongoDB integration is skipped if `/bin/mongod` is unavailable.
138
+
139
+ No live SMS is sent in unit or CI tests. Live smoke tests require separate explicit authorization for provider, recipient, credentials, and cost.
140
+
141
+ ## License
142
+
143
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,7 @@
1
+ # Security policy
2
+
3
+ Do not report provider credentials, customer phone numbers, message parameters, or private provider responses in a public issue. Rotate exposed credentials before using the repository owner's private security channel.
4
+
5
+ The package treats provider credentials as write-only and does not retain message bodies or semantic values by default. Applications remain responsible for authentication, authorization, rate limiting, key storage, MongoDB security, and lawful recipient consent.
6
+
7
+ Live provider tests and real SMS sends are outside the normal suite and require explicit operator authorization.
@@ -0,0 +1,119 @@
1
+ import { h as SmsSendExecutor, g as SmsSendCommand, j as SmsSendResult } from './manager-B3b12wET.cjs';
2
+ import { S as SmsProviderAdapter, b as ProviderSubmissionResult } from './provider-QoH6qlAe.cjs';
3
+
4
+ type ParameterBinding = {
5
+ readonly kind: "named";
6
+ readonly providerParameter: string;
7
+ readonly sourceParameter: string;
8
+ } | {
9
+ readonly kind: "ordered";
10
+ readonly position: number;
11
+ readonly sourceParameter: string;
12
+ } | {
13
+ readonly kind: "fixed-slot";
14
+ readonly slot: string;
15
+ readonly sourceParameter: string;
16
+ };
17
+ declare class InvalidParameterBindingError extends TypeError {
18
+ readonly code = "template_binding_invalid";
19
+ constructor(message: string);
20
+ }
21
+ declare function applyParameterBindings(parameters: Readonly<Record<string, unknown>>, bindings: readonly ParameterBinding[]): Readonly<Record<string, unknown>> | readonly unknown[];
22
+
23
+ interface ResolvedSendConfiguration {
24
+ readonly id: string;
25
+ readonly version: number;
26
+ readonly adapter: SmsProviderAdapter<Record<string, unknown>>;
27
+ readonly credentials: Record<string, unknown>;
28
+ readonly template: unknown;
29
+ readonly bindings: readonly ParameterBinding[];
30
+ }
31
+ interface SendConfigurationResolver {
32
+ resolve(scopeKey: string, actionKey: string): Promise<ResolvedSendConfiguration>;
33
+ }
34
+ interface OperationSnapshot {
35
+ readonly id: string;
36
+ readonly state: "reserved" | "processing" | "submitted" | "delivered" | "failed" | "unknown";
37
+ readonly providerMessageId?: string;
38
+ }
39
+ interface SendOperationStore {
40
+ reserve(input: {
41
+ readonly scopeKey: string;
42
+ readonly actionKey: string;
43
+ readonly actionDefinitionVersion: string;
44
+ readonly configurationVersion: number;
45
+ readonly idempotencyKey: string;
46
+ readonly requestFingerprint: string;
47
+ readonly recipientFingerprint: string;
48
+ readonly expiresAt: Date;
49
+ }): Promise<{
50
+ readonly created: boolean;
51
+ readonly operation: OperationSnapshot;
52
+ }>;
53
+ beginAttempt(input: {
54
+ readonly operationId: string;
55
+ readonly providerConfigurationId: string;
56
+ readonly adapterId: string;
57
+ readonly expiresAt: Date;
58
+ }): Promise<void>;
59
+ completeAttempt(operationId: string, result: ProviderSubmissionResult): Promise<OperationSnapshot>;
60
+ }
61
+ interface SmsOrchestratorOptions {
62
+ readonly scopeKey?: string;
63
+ readonly actionDefinitionVersion: string;
64
+ readonly fingerprintSecret: Uint8Array;
65
+ readonly operationRetentionMs?: number;
66
+ readonly configurations: SendConfigurationResolver;
67
+ readonly operations: SendOperationStore;
68
+ readonly now?: () => Date;
69
+ }
70
+ declare class SmsOrchestrator implements SmsSendExecutor {
71
+ private readonly options;
72
+ private readonly scopeKey;
73
+ private readonly retentionMs;
74
+ private readonly now;
75
+ constructor(options: SmsOrchestratorOptions);
76
+ execute(command: SmsSendCommand): Promise<SmsSendResult>;
77
+ }
78
+
79
+ interface EncryptionKeyring {
80
+ readonly activeKeyId: string;
81
+ getKey(keyId: string): Promise<Uint8Array | undefined> | Uint8Array | undefined;
82
+ }
83
+ interface CredentialEncryptionContext {
84
+ readonly configurationId: string;
85
+ readonly adapterId: string;
86
+ readonly scopeKey: string;
87
+ readonly credentialRevision: number;
88
+ readonly schemaVersion: number;
89
+ }
90
+ interface EncryptedCredentialEnvelope {
91
+ readonly algorithm: string;
92
+ readonly keyId: string;
93
+ readonly configurationId: string;
94
+ readonly adapterId: string;
95
+ readonly scopeKey: string;
96
+ readonly credentialRevision: number;
97
+ readonly nonce: string;
98
+ readonly ciphertext: string;
99
+ readonly authTag: string;
100
+ readonly schemaVersion: number;
101
+ }
102
+ declare class CredentialEncryptionError extends Error {
103
+ readonly code = "credential_encryption_failed";
104
+ constructor(message: string, options?: ErrorOptions);
105
+ }
106
+ declare function encryptCredentials(credentials: Readonly<Record<string, unknown>>, context: CredentialEncryptionContext, keyring: EncryptionKeyring): Promise<EncryptedCredentialEnvelope>;
107
+ declare function decryptCredentials<T extends Record<string, unknown>>(envelope: EncryptedCredentialEnvelope, context: CredentialEncryptionContext, keyring: EncryptionKeyring): Promise<T>;
108
+ declare function createStaticKeyring(options: {
109
+ readonly activeKeyId: string;
110
+ readonly keys: Readonly<Record<string, Uint8Array>>;
111
+ }): EncryptionKeyring;
112
+ declare function createEnvironmentKeyring(options: {
113
+ readonly activeKeyId: string;
114
+ readonly keys: Readonly<Record<string, string>>;
115
+ readonly encoding?: "base64" | "hex";
116
+ }): EncryptionKeyring;
117
+
118
+ export { InvalidParameterBindingError as I, CredentialEncryptionError as a, SmsOrchestrator as d, applyParameterBindings as f, createEnvironmentKeyring as g, createStaticKeyring as h, decryptCredentials as i, encryptCredentials as j };
119
+ export type { CredentialEncryptionContext as C, EncryptedCredentialEnvelope as E, OperationSnapshot as O, ParameterBinding as P, ResolvedSendConfiguration as R, SendConfigurationResolver as S, EncryptionKeyring as b, SendOperationStore as c, SmsOrchestratorOptions as e };
@@ -0,0 +1,119 @@
1
+ import { h as SmsSendExecutor, g as SmsSendCommand, j as SmsSendResult } from './manager-B3b12wET.js';
2
+ import { S as SmsProviderAdapter, b as ProviderSubmissionResult } from './provider-QoH6qlAe.js';
3
+
4
+ type ParameterBinding = {
5
+ readonly kind: "named";
6
+ readonly providerParameter: string;
7
+ readonly sourceParameter: string;
8
+ } | {
9
+ readonly kind: "ordered";
10
+ readonly position: number;
11
+ readonly sourceParameter: string;
12
+ } | {
13
+ readonly kind: "fixed-slot";
14
+ readonly slot: string;
15
+ readonly sourceParameter: string;
16
+ };
17
+ declare class InvalidParameterBindingError extends TypeError {
18
+ readonly code = "template_binding_invalid";
19
+ constructor(message: string);
20
+ }
21
+ declare function applyParameterBindings(parameters: Readonly<Record<string, unknown>>, bindings: readonly ParameterBinding[]): Readonly<Record<string, unknown>> | readonly unknown[];
22
+
23
+ interface ResolvedSendConfiguration {
24
+ readonly id: string;
25
+ readonly version: number;
26
+ readonly adapter: SmsProviderAdapter<Record<string, unknown>>;
27
+ readonly credentials: Record<string, unknown>;
28
+ readonly template: unknown;
29
+ readonly bindings: readonly ParameterBinding[];
30
+ }
31
+ interface SendConfigurationResolver {
32
+ resolve(scopeKey: string, actionKey: string): Promise<ResolvedSendConfiguration>;
33
+ }
34
+ interface OperationSnapshot {
35
+ readonly id: string;
36
+ readonly state: "reserved" | "processing" | "submitted" | "delivered" | "failed" | "unknown";
37
+ readonly providerMessageId?: string;
38
+ }
39
+ interface SendOperationStore {
40
+ reserve(input: {
41
+ readonly scopeKey: string;
42
+ readonly actionKey: string;
43
+ readonly actionDefinitionVersion: string;
44
+ readonly configurationVersion: number;
45
+ readonly idempotencyKey: string;
46
+ readonly requestFingerprint: string;
47
+ readonly recipientFingerprint: string;
48
+ readonly expiresAt: Date;
49
+ }): Promise<{
50
+ readonly created: boolean;
51
+ readonly operation: OperationSnapshot;
52
+ }>;
53
+ beginAttempt(input: {
54
+ readonly operationId: string;
55
+ readonly providerConfigurationId: string;
56
+ readonly adapterId: string;
57
+ readonly expiresAt: Date;
58
+ }): Promise<void>;
59
+ completeAttempt(operationId: string, result: ProviderSubmissionResult): Promise<OperationSnapshot>;
60
+ }
61
+ interface SmsOrchestratorOptions {
62
+ readonly scopeKey?: string;
63
+ readonly actionDefinitionVersion: string;
64
+ readonly fingerprintSecret: Uint8Array;
65
+ readonly operationRetentionMs?: number;
66
+ readonly configurations: SendConfigurationResolver;
67
+ readonly operations: SendOperationStore;
68
+ readonly now?: () => Date;
69
+ }
70
+ declare class SmsOrchestrator implements SmsSendExecutor {
71
+ private readonly options;
72
+ private readonly scopeKey;
73
+ private readonly retentionMs;
74
+ private readonly now;
75
+ constructor(options: SmsOrchestratorOptions);
76
+ execute(command: SmsSendCommand): Promise<SmsSendResult>;
77
+ }
78
+
79
+ interface EncryptionKeyring {
80
+ readonly activeKeyId: string;
81
+ getKey(keyId: string): Promise<Uint8Array | undefined> | Uint8Array | undefined;
82
+ }
83
+ interface CredentialEncryptionContext {
84
+ readonly configurationId: string;
85
+ readonly adapterId: string;
86
+ readonly scopeKey: string;
87
+ readonly credentialRevision: number;
88
+ readonly schemaVersion: number;
89
+ }
90
+ interface EncryptedCredentialEnvelope {
91
+ readonly algorithm: string;
92
+ readonly keyId: string;
93
+ readonly configurationId: string;
94
+ readonly adapterId: string;
95
+ readonly scopeKey: string;
96
+ readonly credentialRevision: number;
97
+ readonly nonce: string;
98
+ readonly ciphertext: string;
99
+ readonly authTag: string;
100
+ readonly schemaVersion: number;
101
+ }
102
+ declare class CredentialEncryptionError extends Error {
103
+ readonly code = "credential_encryption_failed";
104
+ constructor(message: string, options?: ErrorOptions);
105
+ }
106
+ declare function encryptCredentials(credentials: Readonly<Record<string, unknown>>, context: CredentialEncryptionContext, keyring: EncryptionKeyring): Promise<EncryptedCredentialEnvelope>;
107
+ declare function decryptCredentials<T extends Record<string, unknown>>(envelope: EncryptedCredentialEnvelope, context: CredentialEncryptionContext, keyring: EncryptionKeyring): Promise<T>;
108
+ declare function createStaticKeyring(options: {
109
+ readonly activeKeyId: string;
110
+ readonly keys: Readonly<Record<string, Uint8Array>>;
111
+ }): EncryptionKeyring;
112
+ declare function createEnvironmentKeyring(options: {
113
+ readonly activeKeyId: string;
114
+ readonly keys: Readonly<Record<string, string>>;
115
+ readonly encoding?: "base64" | "hex";
116
+ }): EncryptionKeyring;
117
+
118
+ export { InvalidParameterBindingError as I, CredentialEncryptionError as a, SmsOrchestrator as d, applyParameterBindings as f, createEnvironmentKeyring as g, createStaticKeyring as h, decryptCredentials as i, encryptCredentials as j };
119
+ export type { CredentialEncryptionContext as C, EncryptedCredentialEnvelope as E, OperationSnapshot as O, ParameterBinding as P, ResolvedSendConfiguration as R, SendConfigurationResolver as S, EncryptionKeyring as b, SendOperationStore as c, SmsOrchestratorOptions as e };