@vaam-apps/vsms-node 0.3.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vymalo
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 ADDED
@@ -0,0 +1,54 @@
1
+ # @vaam-apps/vsms-node
2
+
3
+ Official Node.js SDK for [vsms](https://github.com/vaam-apps/vsms).
4
+
5
+ Owns the `private_key_jwt` credential lifecycle, so a caller writes `client.sendMessage(...)` and never touches a JWT.
6
+
7
+ ## Features
8
+
9
+ - **Automated `private_key_jwt` Auth**: Automatic assertion signing (RFC 7523), token acquisition, in-memory caching with safety margins, and single-flight de-duplication.
10
+ - **Bounded 401 Refresh**: Transparently renews the access token and retries once if a token is invalidated mid-flight.
11
+ - **Idempotency Support**: Built-in support for `Idempotency-Key` headers and response replay tracking (`SendMessageOutcome.idempotencyReplayed`).
12
+ - **Standard TypeScript Types**: Clean domain types with zero framework lock-in.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @vaam-apps/vsms-node
18
+ # or
19
+ pnpm add @vaam-apps/vsms-node
20
+ ```
21
+
22
+ ## Quickstart
23
+
24
+ ```typescript
25
+ import { VsmsClient } from "@vaam-apps/vsms-node";
26
+
27
+ // Initialize client with private key on disk or PEM string
28
+ const client = VsmsClient.privateKeyJwt({
29
+ issuer: "http://127.0.0.1:8080",
30
+ clientId: "your-client-id",
31
+ keyPath: "/path/to/client-key.pem",
32
+ // scope defaults to "sms:send sms:read"
33
+ });
34
+
35
+ // Send an SMS
36
+ const outcome = await client.sendMessage(
37
+ {
38
+ to: "+237677123456",
39
+ senderId: "VYMALO",
40
+ body: "Hello from @vaam-apps/vsms-node!",
41
+ },
42
+ {
43
+ // Optional retry-safe idempotency key
44
+ idempotencyKey: "my-custom-order-id-123",
45
+ }
46
+ );
47
+
48
+ console.log("Sent message:", outcome.result.messageId, "State:", outcome.result.state);
49
+ console.log("Was replayed from cache:", outcome.idempotencyReplayed);
50
+
51
+ // Read message status
52
+ const message = await client.getMessage(outcome.result.messageId);
53
+ console.log("Status:", message.state);
54
+ ```
@@ -0,0 +1,43 @@
1
+ import { type PrivateKeyJwtConfigOptions, type TokenStore } from "./token.js";
2
+ import type { Message, PreviewInput, PreviewResult, SendMessageInput, SendMessageResult } from "./types.js";
3
+ export interface SendMessageOptions {
4
+ idempotencyKey?: string | undefined;
5
+ }
6
+ export interface SendMessageOutcome {
7
+ result: SendMessageResult;
8
+ idempotencyReplayed: boolean;
9
+ }
10
+ export interface VsmsClientOptions {
11
+ baseUrl: string;
12
+ tokenStore: TokenStore;
13
+ }
14
+ export interface PrivateKeyJwtClientOptions extends PrivateKeyJwtConfigOptions {
15
+ keyPath?: string | undefined;
16
+ privateKeyPem?: string | undefined;
17
+ }
18
+ export declare class VsmsClient {
19
+ #private;
20
+ constructor(options: VsmsClientOptions);
21
+ /**
22
+ * Convenience factory: constructs a VsmsClient using private_key_jwt authentication.
23
+ *
24
+ * Note: in this deployment, the OP issuer and API origin are the same host (sms-gateway
25
+ * mounts both /token and the REST router on the same listener), so baseUrl defaults to issuer.
26
+ */
27
+ static privateKeyJwt(options: PrivateKeyJwtClientOptions): VsmsClient;
28
+ get tokenStore(): TokenStore;
29
+ /**
30
+ * `POST /$procs/sendMessage`. Handles token acquisition, caching, bounded 401 retry,
31
+ * and optional `Idempotency-Key` header with replay tracking.
32
+ */
33
+ sendMessage(input: SendMessageInput, options?: SendMessageOptions | undefined): Promise<SendMessageOutcome>;
34
+ /**
35
+ * `POST /$procs/previewMessage`.
36
+ */
37
+ previewMessage(input: PreviewInput): Promise<PreviewResult>;
38
+ /**
39
+ * `GET /messages/{id}`.
40
+ */
41
+ getMessage(id: string): Promise<Message>;
42
+ }
43
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,0BAA0B,EAE/B,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,OAAO,EACP,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAKpB,MAAM,WAAW,kBAAkB;IACjC,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,0BAA2B,SAAQ,0BAA0B;IAC5E,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACpC;AAQD,qBAAa,UAAU;;gBAIT,OAAO,EAAE,iBAAiB;IAKtC;;;;;OAKG;IACH,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,0BAA0B,GAAG,UAAU;IAgBrE,IAAI,UAAU,IAAI,UAAU,CAE3B;IA4CD;;;OAGG;IACG,WAAW,CACf,KAAK,EAAE,gBAAgB,EACvB,OAAO,CAAC,EAAE,kBAAkB,GAAG,SAAS,GACvC,OAAO,CAAC,kBAAkB,CAAC;IA+C9B;;OAEG;IACG,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC;IA8BjE;;OAEG;IACG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CA2B/C"}
package/dist/client.js ADDED
@@ -0,0 +1,167 @@
1
+ import { SdkError } from "./error.js";
2
+ import { PrivateKeyJwtTokenStore } from "./token.js";
3
+ const IDEMPOTENCY_REPLAYED_HEADER = "idempotency-replayed";
4
+ const IDEMPOTENCY_KEY_HEADER = "idempotency-key";
5
+ export class VsmsClient {
6
+ #baseUrl;
7
+ #tokenStore;
8
+ constructor(options){
9
+ this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
10
+ this.#tokenStore = options.tokenStore;
11
+ }
12
+ /**
13
+ * Convenience factory: constructs a VsmsClient using private_key_jwt authentication.
14
+ *
15
+ * Note: in this deployment, the OP issuer and API origin are the same host (sms-gateway
16
+ * mounts both /token and the REST router on the same listener), so baseUrl defaults to issuer.
17
+ */ static privateKeyJwt(options) {
18
+ const tokenStore = new PrivateKeyJwtTokenStore({
19
+ issuer: options.issuer,
20
+ clientId: options.clientId,
21
+ scope: options.scope,
22
+ audience: options.audience
23
+ }, {
24
+ keyPath: options.keyPath,
25
+ pemString: options.privateKeyPem
26
+ });
27
+ return new VsmsClient({
28
+ baseUrl: options.issuer,
29
+ tokenStore
30
+ });
31
+ }
32
+ get tokenStore() {
33
+ return this.#tokenStore;
34
+ }
35
+ #parseJsonOrThrow(text, status, method) {
36
+ try {
37
+ return JSON.parse(text);
38
+ } catch (err) {
39
+ throw new SdkError(`failed to parse ${method} response as JSON: ${text}`, {
40
+ cause: err,
41
+ httpStatus: status
42
+ });
43
+ }
44
+ }
45
+ /**
46
+ * Helper that calls an authenticated endpoint, with automatic bounded 401 retry.
47
+ */ async #fetchWithAuth(path, init) {
48
+ const url = `${this.#baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
49
+ const makeRequest = async ()=>{
50
+ const token = await this.#tokenStore.get();
51
+ const headers = new Headers(init.headers);
52
+ headers.set("authorization", `Bearer ${token}`);
53
+ if (!headers.has("accept")) {
54
+ headers.set("accept", "application/json");
55
+ }
56
+ try {
57
+ return await fetch(url, {
58
+ ...init,
59
+ headers
60
+ });
61
+ } catch (err) {
62
+ throw new SdkError(`request to ${url} failed: network error`, {
63
+ cause: err
64
+ });
65
+ }
66
+ };
67
+ let res = await makeRequest();
68
+ if (res.status === 401) {
69
+ await this.#tokenStore.invalidate();
70
+ res = await makeRequest();
71
+ }
72
+ return res;
73
+ }
74
+ /**
75
+ * `POST /$procs/sendMessage`. Handles token acquisition, caching, bounded 401 retry,
76
+ * and optional `Idempotency-Key` header with replay tracking.
77
+ */ async sendMessage(input, options) {
78
+ const headers = {
79
+ "content-type": "application/json",
80
+ accept: "application/json"
81
+ };
82
+ if (options?.idempotencyKey) {
83
+ headers[IDEMPOTENCY_KEY_HEADER] = options.idempotencyKey;
84
+ }
85
+ const response = await this.#fetchWithAuth("/$procs/sendMessage", {
86
+ method: "POST",
87
+ headers,
88
+ body: JSON.stringify({
89
+ args: input
90
+ })
91
+ });
92
+ const text = await response.text();
93
+ const isReplayed = response.headers.get(IDEMPOTENCY_REPLAYED_HEADER) === "true";
94
+ if (!response.ok) {
95
+ let parsed;
96
+ try {
97
+ parsed = JSON.parse(text);
98
+ } catch {
99
+ // Fallback for non-JSON errors (e.g. text/plain idempotency in flight message)
100
+ }
101
+ throw new SdkError(parsed?.message ?? text ?? `sendMessage failed with status ${response.status}`, {
102
+ httpStatus: response.status,
103
+ gatewayCode: parsed?.code,
104
+ fieldErrors: typeof parsed?.details === "object" && parsed.details !== null ? parsed.details : undefined
105
+ });
106
+ }
107
+ const result = this.#parseJsonOrThrow(text, response.status, "sendMessage");
108
+ return {
109
+ result,
110
+ idempotencyReplayed: isReplayed
111
+ };
112
+ }
113
+ /**
114
+ * `POST /$procs/previewMessage`.
115
+ */ async previewMessage(input) {
116
+ const response = await this.#fetchWithAuth("/$procs/previewMessage", {
117
+ method: "POST",
118
+ headers: {
119
+ "content-type": "application/json",
120
+ accept: "application/json"
121
+ },
122
+ body: JSON.stringify({
123
+ args: input
124
+ })
125
+ });
126
+ const text = await response.text();
127
+ if (!response.ok) {
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(text);
131
+ } catch {
132
+ // fallback
133
+ }
134
+ throw new SdkError(parsed?.message ?? text ?? `previewMessage failed with status ${response.status}`, {
135
+ httpStatus: response.status,
136
+ gatewayCode: parsed?.code
137
+ });
138
+ }
139
+ return this.#parseJsonOrThrow(text, response.status, "previewMessage");
140
+ }
141
+ /**
142
+ * `GET /messages/{id}`.
143
+ */ async getMessage(id) {
144
+ const response = await this.#fetchWithAuth(`/messages/${encodeURIComponent(id)}`, {
145
+ method: "GET",
146
+ headers: {
147
+ accept: "application/json"
148
+ }
149
+ });
150
+ const text = await response.text();
151
+ if (!response.ok) {
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(text);
155
+ } catch {
156
+ // fallback
157
+ }
158
+ throw new SdkError(parsed?.message ?? text ?? `GET /messages/${id} failed with status ${response.status}`, {
159
+ httpStatus: response.status,
160
+ gatewayCode: parsed?.code
161
+ });
162
+ }
163
+ return this.#parseJsonOrThrow(text, response.status, `GET /messages/${id}`);
164
+ }
165
+ }
166
+
167
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { SdkError } from \"./error.js\";\nimport {\n type PrivateKeyJwtConfigOptions,\n PrivateKeyJwtTokenStore,\n type TokenStore,\n} from \"./token.js\";\nimport type {\n Message,\n PreviewInput,\n PreviewResult,\n SendMessageInput,\n SendMessageResult,\n} from \"./types.js\";\n\nconst IDEMPOTENCY_REPLAYED_HEADER = \"idempotency-replayed\";\nconst IDEMPOTENCY_KEY_HEADER = \"idempotency-key\";\n\nexport interface SendMessageOptions {\n idempotencyKey?: string | undefined;\n}\n\nexport interface SendMessageOutcome {\n result: SendMessageResult;\n idempotencyReplayed: boolean;\n}\n\nexport interface VsmsClientOptions {\n baseUrl: string;\n tokenStore: TokenStore;\n}\n\nexport interface PrivateKeyJwtClientOptions extends PrivateKeyJwtConfigOptions {\n keyPath?: string | undefined;\n privateKeyPem?: string | undefined;\n}\n\ninterface CratestackErrorResponseBody {\n code?: string | undefined;\n message?: string | undefined;\n details?: unknown;\n}\n\nexport class VsmsClient {\n readonly #baseUrl: string;\n readonly #tokenStore: TokenStore;\n\n constructor(options: VsmsClientOptions) {\n this.#baseUrl = options.baseUrl.replace(/\\/+$/, \"\");\n this.#tokenStore = options.tokenStore;\n }\n\n /**\n * Convenience factory: constructs a VsmsClient using private_key_jwt authentication.\n *\n * Note: in this deployment, the OP issuer and API origin are the same host (sms-gateway\n * mounts both /token and the REST router on the same listener), so baseUrl defaults to issuer.\n */\n static privateKeyJwt(options: PrivateKeyJwtClientOptions): VsmsClient {\n const tokenStore = new PrivateKeyJwtTokenStore(\n {\n issuer: options.issuer,\n clientId: options.clientId,\n scope: options.scope,\n audience: options.audience,\n },\n {\n keyPath: options.keyPath,\n pemString: options.privateKeyPem,\n },\n );\n return new VsmsClient({ baseUrl: options.issuer, tokenStore });\n }\n\n get tokenStore(): TokenStore {\n return this.#tokenStore;\n }\n\n #parseJsonOrThrow<T>(text: string, status: number, method: string): T {\n try {\n return JSON.parse(text) as T;\n } catch (err) {\n throw new SdkError(`failed to parse ${method} response as JSON: ${text}`, {\n cause: err,\n httpStatus: status,\n });\n }\n }\n\n /**\n * Helper that calls an authenticated endpoint, with automatic bounded 401 retry.\n */\n async #fetchWithAuth(path: string, init: RequestInit): Promise<Response> {\n const url = `${this.#baseUrl}${path.startsWith(\"/\") ? path : `/${path}`}`;\n\n const makeRequest = async (): Promise<Response> => {\n const token = await this.#tokenStore.get();\n const headers = new Headers(init.headers);\n headers.set(\"authorization\", `Bearer ${token}`);\n if (!headers.has(\"accept\")) {\n headers.set(\"accept\", \"application/json\");\n }\n try {\n return await fetch(url, {\n ...init,\n headers,\n });\n } catch (err) {\n throw new SdkError(`request to ${url} failed: network error`, { cause: err });\n }\n };\n\n let res = await makeRequest();\n if (res.status === 401) {\n await this.#tokenStore.invalidate();\n res = await makeRequest();\n }\n return res;\n }\n\n /**\n * `POST /$procs/sendMessage`. Handles token acquisition, caching, bounded 401 retry,\n * and optional `Idempotency-Key` header with replay tracking.\n */\n async sendMessage(\n input: SendMessageInput,\n options?: SendMessageOptions | undefined,\n ): Promise<SendMessageOutcome> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n };\n if (options?.idempotencyKey) {\n headers[IDEMPOTENCY_KEY_HEADER] = options.idempotencyKey;\n }\n\n const response = await this.#fetchWithAuth(\"/$procs/sendMessage\", {\n method: \"POST\",\n headers,\n body: JSON.stringify({ args: input }),\n });\n\n const text = await response.text();\n const isReplayed = response.headers.get(IDEMPOTENCY_REPLAYED_HEADER) === \"true\";\n\n if (!response.ok) {\n let parsed: CratestackErrorResponseBody | undefined;\n try {\n parsed = JSON.parse(text) as CratestackErrorResponseBody;\n } catch {\n // Fallback for non-JSON errors (e.g. text/plain idempotency in flight message)\n }\n\n throw new SdkError(\n parsed?.message ?? text ?? `sendMessage failed with status ${response.status}`,\n {\n httpStatus: response.status,\n gatewayCode: parsed?.code,\n fieldErrors:\n typeof parsed?.details === \"object\" && parsed.details !== null\n ? (parsed.details as Record<string, string[]>)\n : undefined,\n },\n );\n }\n\n const result = this.#parseJsonOrThrow<SendMessageResult>(text, response.status, \"sendMessage\");\n\n return {\n result,\n idempotencyReplayed: isReplayed,\n };\n }\n\n /**\n * `POST /$procs/previewMessage`.\n */\n async previewMessage(input: PreviewInput): Promise<PreviewResult> {\n const response = await this.#fetchWithAuth(\"/$procs/previewMessage\", {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n },\n body: JSON.stringify({ args: input }),\n });\n\n const text = await response.text();\n if (!response.ok) {\n let parsed: CratestackErrorResponseBody | undefined;\n try {\n parsed = JSON.parse(text) as CratestackErrorResponseBody;\n } catch {\n // fallback\n }\n throw new SdkError(\n parsed?.message ?? text ?? `previewMessage failed with status ${response.status}`,\n {\n httpStatus: response.status,\n gatewayCode: parsed?.code,\n },\n );\n }\n\n return this.#parseJsonOrThrow<PreviewResult>(text, response.status, \"previewMessage\");\n }\n\n /**\n * `GET /messages/{id}`.\n */\n async getMessage(id: string): Promise<Message> {\n const response = await this.#fetchWithAuth(`/messages/${encodeURIComponent(id)}`, {\n method: \"GET\",\n headers: {\n accept: \"application/json\",\n },\n });\n\n const text = await response.text();\n if (!response.ok) {\n let parsed: CratestackErrorResponseBody | undefined;\n try {\n parsed = JSON.parse(text) as CratestackErrorResponseBody;\n } catch {\n // fallback\n }\n throw new SdkError(\n parsed?.message ?? text ?? `GET /messages/${id} failed with status ${response.status}`,\n {\n httpStatus: response.status,\n gatewayCode: parsed?.code,\n },\n );\n }\n\n return this.#parseJsonOrThrow<Message>(text, response.status, `GET /messages/${id}`);\n }\n}\n"],"names":["SdkError","PrivateKeyJwtTokenStore","IDEMPOTENCY_REPLAYED_HEADER","IDEMPOTENCY_KEY_HEADER","VsmsClient","options","baseUrl","replace","tokenStore","privateKeyJwt","issuer","clientId","scope","audience","keyPath","pemString","privateKeyPem","text","status","method","JSON","parse","err","cause","httpStatus","path","init","url","startsWith","makeRequest","token","get","headers","Headers","set","has","fetch","res","invalidate","sendMessage","input","accept","idempotencyKey","response","body","stringify","args","isReplayed","ok","parsed","message","gatewayCode","code","fieldErrors","details","undefined","result","idempotencyReplayed","previewMessage","getMessage","id","encodeURIComponent"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The one error type this SDK throws or returns.
3
+ * Wraps REST call errors, OAuth / token failures, and validation errors.
4
+ */
5
+ export interface SdkErrorOptions {
6
+ httpStatus?: number | undefined;
7
+ gatewayCode?: string | undefined;
8
+ fieldErrors?: Record<string, string[]> | undefined;
9
+ cause?: unknown;
10
+ }
11
+ export declare class SdkError extends Error {
12
+ readonly httpStatus: number | undefined;
13
+ readonly gatewayCode: string | undefined;
14
+ readonly fieldErrors: Record<string, string[]> | undefined;
15
+ constructor(message: string, options?: SdkErrorOptions);
16
+ /**
17
+ * `true` for a `409 Conflict` (either a duplicate `clientRef` or an in-flight `Idempotency-Key`).
18
+ */
19
+ isConflict(): boolean;
20
+ /**
21
+ * `true` for the specific `409` `IdempotencyLayer` returns while
22
+ * another request under the *same* `Idempotency-Key` is still being
23
+ * processed (not yet reserved-and-replayable).
24
+ */
25
+ isIdempotencyInFlight(): boolean;
26
+ /**
27
+ * `true` for the `422` `IdempotencyLayer` returns when an
28
+ * `Idempotency-Key` is reused with a request that doesn't match the
29
+ * first one under that key byte-for-byte.
30
+ */
31
+ isIdempotencyKeyConflict(): boolean;
32
+ /**
33
+ * `true` if the final error surfaced from a call was a `401 Unauthorized`
34
+ * after bounded refresh was attempted.
35
+ */
36
+ isUnauthorized(): boolean;
37
+ }
38
+ //# sourceMappingURL=error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC;IACnD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,SAAS,CAAC;gBAE/C,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe;IAQtD;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;;;OAIG;IACH,qBAAqB,IAAI,OAAO;IAIhC;;;;OAIG;IACH,wBAAwB,IAAI,OAAO;IAInC;;;OAGG;IACH,cAAc,IAAI,OAAO;CAG1B"}
package/dist/error.js ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The one error type this SDK throws or returns.
3
+ * Wraps REST call errors, OAuth / token failures, and validation errors.
4
+ */ export class SdkError extends Error {
5
+ httpStatus;
6
+ gatewayCode;
7
+ fieldErrors;
8
+ constructor(message, options){
9
+ super(message, {
10
+ cause: options?.cause
11
+ });
12
+ this.name = "SdkError";
13
+ this.httpStatus = options?.httpStatus;
14
+ this.gatewayCode = options?.gatewayCode;
15
+ this.fieldErrors = options?.fieldErrors;
16
+ }
17
+ /**
18
+ * `true` for a `409 Conflict` (either a duplicate `clientRef` or an in-flight `Idempotency-Key`).
19
+ */ isConflict() {
20
+ return this.httpStatus === 409;
21
+ }
22
+ /**
23
+ * `true` for the specific `409` `IdempotencyLayer` returns while
24
+ * another request under the *same* `Idempotency-Key` is still being
25
+ * processed (not yet reserved-and-replayable).
26
+ */ isIdempotencyInFlight() {
27
+ return this.httpStatus === 409 && this.message.includes("Idempotency-Key");
28
+ }
29
+ /**
30
+ * `true` for the `422` `IdempotencyLayer` returns when an
31
+ * `Idempotency-Key` is reused with a request that doesn't match the
32
+ * first one under that key byte-for-byte.
33
+ */ isIdempotencyKeyConflict() {
34
+ return this.httpStatus === 422 && this.message.includes("idempotency_key_conflict");
35
+ }
36
+ /**
37
+ * `true` if the final error surfaced from a call was a `401 Unauthorized`
38
+ * after bounded refresh was attempted.
39
+ */ isUnauthorized() {
40
+ return this.httpStatus === 401;
41
+ }
42
+ }
43
+
44
+ //# sourceMappingURL=error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/error.ts"],"sourcesContent":["/**\n * The one error type this SDK throws or returns.\n * Wraps REST call errors, OAuth / token failures, and validation errors.\n */\n\nexport interface SdkErrorOptions {\n httpStatus?: number | undefined;\n gatewayCode?: string | undefined;\n fieldErrors?: Record<string, string[]> | undefined;\n cause?: unknown;\n}\n\nexport class SdkError extends Error {\n readonly httpStatus: number | undefined;\n readonly gatewayCode: string | undefined;\n readonly fieldErrors: Record<string, string[]> | undefined;\n\n constructor(message: string, options?: SdkErrorOptions) {\n super(message, { cause: options?.cause });\n this.name = \"SdkError\";\n this.httpStatus = options?.httpStatus;\n this.gatewayCode = options?.gatewayCode;\n this.fieldErrors = options?.fieldErrors;\n }\n\n /**\n * `true` for a `409 Conflict` (either a duplicate `clientRef` or an in-flight `Idempotency-Key`).\n */\n isConflict(): boolean {\n return this.httpStatus === 409;\n }\n\n /**\n * `true` for the specific `409` `IdempotencyLayer` returns while\n * another request under the *same* `Idempotency-Key` is still being\n * processed (not yet reserved-and-replayable).\n */\n isIdempotencyInFlight(): boolean {\n return this.httpStatus === 409 && this.message.includes(\"Idempotency-Key\");\n }\n\n /**\n * `true` for the `422` `IdempotencyLayer` returns when an\n * `Idempotency-Key` is reused with a request that doesn't match the\n * first one under that key byte-for-byte.\n */\n isIdempotencyKeyConflict(): boolean {\n return this.httpStatus === 422 && this.message.includes(\"idempotency_key_conflict\");\n }\n\n /**\n * `true` if the final error surfaced from a call was a `401 Unauthorized`\n * after bounded refresh was attempted.\n */\n isUnauthorized(): boolean {\n return this.httpStatus === 401;\n }\n}\n"],"names":["SdkError","Error","httpStatus","gatewayCode","fieldErrors","message","options","cause","name","isConflict","isIdempotencyInFlight","includes","isIdempotencyKeyConflict","isUnauthorized"],"mappings":"AAAA;;;CAGC,GASD,OAAO,MAAMA,iBAAiBC;IACnBC,WAA+B;IAC/BC,YAAgC;IAChCC,YAAkD;IAE3D,YAAYC,OAAe,EAAEC,OAAyB,CAAE;QACtD,KAAK,CAACD,SAAS;YAAEE,OAAOD,SAASC;QAAM;QACvC,IAAI,CAACC,IAAI,GAAG;QACZ,IAAI,CAACN,UAAU,GAAGI,SAASJ;QAC3B,IAAI,CAACC,WAAW,GAAGG,SAASH;QAC5B,IAAI,CAACC,WAAW,GAAGE,SAASF;IAC9B;IAEA;;GAEC,GACDK,aAAsB;QACpB,OAAO,IAAI,CAACP,UAAU,KAAK;IAC7B;IAEA;;;;GAIC,GACDQ,wBAAiC;QAC/B,OAAO,IAAI,CAACR,UAAU,KAAK,OAAO,IAAI,CAACG,OAAO,CAACM,QAAQ,CAAC;IAC1D;IAEA;;;;GAIC,GACDC,2BAAoC;QAClC,OAAO,IAAI,CAACV,UAAU,KAAK,OAAO,IAAI,CAACG,OAAO,CAACM,QAAQ,CAAC;IAC1D;IAEA;;;GAGC,GACDE,iBAA0B;QACxB,OAAO,IAAI,CAACX,UAAU,KAAK;IAC7B;AACF"}
@@ -0,0 +1,5 @@
1
+ export { type PrivateKeyJwtClientOptions, type SendMessageOptions, type SendMessageOutcome, VsmsClient, type VsmsClientOptions, } from "./client.js";
2
+ export { SdkError, type SdkErrorOptions } from "./error.js";
3
+ export { type CachedToken, type PrivateKeyJwtConfigOptions, PrivateKeyJwtTokenStore, type TokenAudience, type TokenStore, } from "./token.js";
4
+ export * from "./types.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,UAAU,EACV,KAAK,iBAAiB,GACvB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAC5D,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,0BAA0B,EAC/B,uBAAuB,EACvB,KAAK,aAAa,EAClB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC;AACpB,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { VsmsClient } from "./client.js";
2
+ export { SdkError } from "./error.js";
3
+ export { PrivateKeyJwtTokenStore } from "./token.js";
4
+ export * from "./types.js";
5
+
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {\n type PrivateKeyJwtClientOptions,\n type SendMessageOptions,\n type SendMessageOutcome,\n VsmsClient,\n type VsmsClientOptions,\n} from \"./client.js\";\nexport { SdkError, type SdkErrorOptions } from \"./error.js\";\nexport {\n type CachedToken,\n type PrivateKeyJwtConfigOptions,\n PrivateKeyJwtTokenStore,\n type TokenAudience,\n type TokenStore,\n} from \"./token.js\";\nexport * from \"./types.js\";\n"],"names":["VsmsClient","SdkError","PrivateKeyJwtTokenStore"],"mappings":"AAAA,SAIEA,UAAU,QAEL,cAAc;AACrB,SAASC,QAAQ,QAA8B,aAAa;AAC5D,SAGEC,uBAAuB,QAGlB,aAAa;AACpB,cAAc,aAAa"}
@@ -0,0 +1,27 @@
1
+ export type TokenAudience = "token_endpoint" | "issuer";
2
+ export interface PrivateKeyJwtConfigOptions {
3
+ issuer: string;
4
+ clientId: string;
5
+ scope?: string | undefined;
6
+ audience?: TokenAudience | undefined;
7
+ }
8
+ export interface CachedToken {
9
+ accessToken: string;
10
+ expiresAtMs: number;
11
+ }
12
+ export interface TokenStore {
13
+ get(): Promise<string>;
14
+ invalidate(): Promise<void> | void;
15
+ }
16
+ export declare class PrivateKeyJwtTokenStore implements TokenStore {
17
+ #private;
18
+ constructor(options: PrivateKeyJwtConfigOptions, keyMaterial: {
19
+ pemString?: string | undefined;
20
+ keyPath?: string | undefined;
21
+ });
22
+ static fromKeyPath(issuer: string, clientId: string, keyPath: string, scope?: string | undefined, audience?: TokenAudience | undefined): PrivateKeyJwtTokenStore;
23
+ static fromKeyPem(issuer: string, clientId: string, pemString: string, scope?: string | undefined, audience?: TokenAudience | undefined): PrivateKeyJwtTokenStore;
24
+ get(): Promise<string>;
25
+ invalidate(): void;
26
+ }
27
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAExD,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB;AASD,MAAM,WAAW,UAAU;IACzB,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACpC;AAID,qBAAa,uBAAwB,YAAW,UAAU;;gBAWtD,OAAO,EAAE,0BAA0B,EACnC,WAAW,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;IAqC/E,MAAM,CAAC,WAAW,CAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,EAC1B,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,GACnC,uBAAuB;IAI1B,MAAM,CAAC,UAAU,CACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,EAC1B,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,GACnC,uBAAuB;IA0EpB,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC;IAsB5B,UAAU,IAAI,IAAI;CAGnB"}
package/dist/token.js ADDED
@@ -0,0 +1,155 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { importPKCS8, SignJWT } from "jose";
4
+ import { SdkError } from "./error.js";
5
+ const ASSERTION_TTL_SECONDS = 60;
6
+ const CLIENT_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
7
+ const DEFAULT_TOKEN_TTL_SECONDS = 15 * 60;
8
+ const EXPIRY_SAFETY_MARGIN_SECONDS = 60;
9
+ export class PrivateKeyJwtTokenStore {
10
+ #issuer;
11
+ #tokenEndpoint;
12
+ #clientId;
13
+ #scope;
14
+ #audience;
15
+ #signingKeyPromise;
16
+ #cached = null;
17
+ #inFlight = null;
18
+ constructor(options, keyMaterial){
19
+ this.#issuer = options.issuer.replace(/\/+$/, "");
20
+ this.#tokenEndpoint = `${this.#issuer}/token`;
21
+ this.#clientId = options.clientId;
22
+ const rawScope = options.scope ?? "sms:send sms:read";
23
+ const trimmedScope = rawScope.trim();
24
+ if (trimmedScope.length === 0) {
25
+ throw new SdkError("scope cannot be empty (must request explicit permissions, e.g. 'sms:send sms:read')");
26
+ }
27
+ this.#scope = trimmedScope;
28
+ this.#audience = options.audience ?? "token_endpoint";
29
+ this.#signingKeyPromise = (async ()=>{
30
+ let pem = keyMaterial.pemString;
31
+ if (pem === undefined && keyMaterial.keyPath !== undefined) {
32
+ try {
33
+ pem = readFileSync(keyMaterial.keyPath, "utf8");
34
+ } catch (err) {
35
+ throw new SdkError(`failed to read private key at ${keyMaterial.keyPath}`, {
36
+ cause: err
37
+ });
38
+ }
39
+ }
40
+ if (!pem) {
41
+ throw new SdkError("no private key PEM or key path provided");
42
+ }
43
+ try {
44
+ return await importPKCS8(pem, "RS256");
45
+ } catch (err) {
46
+ throw new SdkError("invalid RSA private key PEM (PKCS#8 expected)", {
47
+ cause: err
48
+ });
49
+ }
50
+ })();
51
+ }
52
+ static fromKeyPath(issuer, clientId, keyPath, scope, audience) {
53
+ return new PrivateKeyJwtTokenStore({
54
+ issuer,
55
+ clientId,
56
+ scope,
57
+ audience
58
+ }, {
59
+ keyPath
60
+ });
61
+ }
62
+ static fromKeyPem(issuer, clientId, pemString, scope, audience) {
63
+ return new PrivateKeyJwtTokenStore({
64
+ issuer,
65
+ clientId,
66
+ scope,
67
+ audience
68
+ }, {
69
+ pemString
70
+ });
71
+ }
72
+ async #mintAssertion() {
73
+ const key = await this.#signingKeyPromise;
74
+ const now = Math.floor(Date.now() / 1000);
75
+ const aud = this.#audience === "issuer" ? this.#issuer : this.#tokenEndpoint;
76
+ return await new SignJWT({}).setProtectedHeader({
77
+ alg: "RS256",
78
+ kid: this.#clientId
79
+ }).setIssuer(this.#clientId).setSubject(this.#clientId).setAudience(aud).setJti(randomUUID()).setIssuedAt(now).setExpirationTime(now + ASSERTION_TTL_SECONDS).sign(key);
80
+ }
81
+ async #requestToken() {
82
+ let assertion;
83
+ try {
84
+ assertion = await this.#mintAssertion();
85
+ } catch (err) {
86
+ if (err instanceof SdkError) throw err;
87
+ throw new SdkError("failed to sign client assertion", {
88
+ cause: err
89
+ });
90
+ }
91
+ const body = new URLSearchParams({
92
+ grant_type: "client_credentials",
93
+ client_id: this.#clientId,
94
+ client_assertion_type: CLIENT_ASSERTION_TYPE,
95
+ client_assertion: assertion,
96
+ scope: this.#scope
97
+ });
98
+ let response;
99
+ try {
100
+ response = await fetch(this.#tokenEndpoint, {
101
+ method: "POST",
102
+ headers: {
103
+ "content-type": "application/x-www-form-urlencoded"
104
+ },
105
+ body: body.toString()
106
+ });
107
+ } catch (err) {
108
+ throw new SdkError(`token request to ${this.#tokenEndpoint} failed: network error`, {
109
+ cause: err
110
+ });
111
+ }
112
+ const text = await response.text();
113
+ if (!response.ok) {
114
+ throw new SdkError(`token request to ${this.#tokenEndpoint} failed (${response.status}): ${text}`, {
115
+ httpStatus: response.status
116
+ });
117
+ }
118
+ let parsed;
119
+ try {
120
+ parsed = JSON.parse(text);
121
+ } catch (err) {
122
+ throw new SdkError(`token response from ${this.#tokenEndpoint} is not valid JSON: ${text}`, {
123
+ cause: err
124
+ });
125
+ }
126
+ const ttlSeconds = parsed.expires_in ?? DEFAULT_TOKEN_TTL_SECONDS;
127
+ return {
128
+ accessToken: parsed.access_token,
129
+ expiresAtMs: Date.now() + Math.max(ttlSeconds - EXPIRY_SAFETY_MARGIN_SECONDS, 0) * 1000
130
+ };
131
+ }
132
+ async get() {
133
+ if (this.#cached != null && this.#cached.expiresAtMs > Date.now()) {
134
+ return this.#cached.accessToken;
135
+ }
136
+ if (this.#inFlight) {
137
+ return await this.#inFlight;
138
+ }
139
+ this.#inFlight = (async ()=>{
140
+ try {
141
+ const token = await this.#requestToken();
142
+ this.#cached = token;
143
+ return token.accessToken;
144
+ } finally{
145
+ this.#inFlight = null;
146
+ }
147
+ })();
148
+ return await this.#inFlight;
149
+ }
150
+ invalidate() {
151
+ this.#cached = null;
152
+ }
153
+ }
154
+
155
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/token.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { readFileSync } from \"node:fs\";\nimport { importPKCS8, SignJWT } from \"jose\";\nimport { SdkError } from \"./error.js\";\n\nconst ASSERTION_TTL_SECONDS = 60;\nconst CLIENT_ASSERTION_TYPE = \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\";\nconst DEFAULT_TOKEN_TTL_SECONDS = 15 * 60;\nconst EXPIRY_SAFETY_MARGIN_SECONDS = 60;\n\nexport type TokenAudience = \"token_endpoint\" | \"issuer\";\n\nexport interface PrivateKeyJwtConfigOptions {\n issuer: string;\n clientId: string;\n scope?: string | undefined;\n audience?: TokenAudience | undefined;\n}\n\nexport interface CachedToken {\n accessToken: string;\n expiresAtMs: number;\n}\n\ninterface TokenResponse {\n access_token: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n}\n\nexport interface TokenStore {\n get(): Promise<string>;\n invalidate(): Promise<void> | void;\n}\n\ntype KeyLike = Awaited<ReturnType<typeof importPKCS8>>;\n\nexport class PrivateKeyJwtTokenStore implements TokenStore {\n readonly #issuer: string;\n readonly #tokenEndpoint: string;\n readonly #clientId: string;\n readonly #scope: string;\n readonly #audience: TokenAudience;\n readonly #signingKeyPromise: Promise<KeyLike>;\n #cached: CachedToken | null = null;\n #inFlight: Promise<string> | null = null;\n\n constructor(\n options: PrivateKeyJwtConfigOptions,\n keyMaterial: { pemString?: string | undefined; keyPath?: string | undefined },\n ) {\n this.#issuer = options.issuer.replace(/\\/+$/, \"\");\n this.#tokenEndpoint = `${this.#issuer}/token`;\n this.#clientId = options.clientId;\n const rawScope = options.scope ?? \"sms:send sms:read\";\n const trimmedScope = rawScope.trim();\n if (trimmedScope.length === 0) {\n throw new SdkError(\n \"scope cannot be empty (must request explicit permissions, e.g. 'sms:send sms:read')\",\n );\n }\n this.#scope = trimmedScope;\n this.#audience = options.audience ?? \"token_endpoint\";\n\n this.#signingKeyPromise = (async () => {\n let pem = keyMaterial.pemString;\n if (pem === undefined && keyMaterial.keyPath !== undefined) {\n try {\n pem = readFileSync(keyMaterial.keyPath, \"utf8\");\n } catch (err) {\n throw new SdkError(`failed to read private key at ${keyMaterial.keyPath}`, {\n cause: err,\n });\n }\n }\n if (!pem) {\n throw new SdkError(\"no private key PEM or key path provided\");\n }\n try {\n return await importPKCS8(pem, \"RS256\");\n } catch (err) {\n throw new SdkError(\"invalid RSA private key PEM (PKCS#8 expected)\", { cause: err });\n }\n })();\n }\n\n static fromKeyPath(\n issuer: string,\n clientId: string,\n keyPath: string,\n scope?: string | undefined,\n audience?: TokenAudience | undefined,\n ): PrivateKeyJwtTokenStore {\n return new PrivateKeyJwtTokenStore({ issuer, clientId, scope, audience }, { keyPath });\n }\n\n static fromKeyPem(\n issuer: string,\n clientId: string,\n pemString: string,\n scope?: string | undefined,\n audience?: TokenAudience | undefined,\n ): PrivateKeyJwtTokenStore {\n return new PrivateKeyJwtTokenStore({ issuer, clientId, scope, audience }, { pemString });\n }\n\n async #mintAssertion(): Promise<string> {\n const key = await this.#signingKeyPromise;\n const now = Math.floor(Date.now() / 1000);\n const aud = this.#audience === \"issuer\" ? this.#issuer : this.#tokenEndpoint;\n\n return await new SignJWT({})\n .setProtectedHeader({ alg: \"RS256\", kid: this.#clientId })\n .setIssuer(this.#clientId)\n .setSubject(this.#clientId)\n .setAudience(aud)\n .setJti(randomUUID())\n .setIssuedAt(now)\n .setExpirationTime(now + ASSERTION_TTL_SECONDS)\n .sign(key);\n }\n\n async #requestToken(): Promise<CachedToken> {\n let assertion: string;\n try {\n assertion = await this.#mintAssertion();\n } catch (err) {\n if (err instanceof SdkError) throw err;\n throw new SdkError(\"failed to sign client assertion\", { cause: err });\n }\n\n const body = new URLSearchParams({\n grant_type: \"client_credentials\",\n client_id: this.#clientId,\n client_assertion_type: CLIENT_ASSERTION_TYPE,\n client_assertion: assertion,\n scope: this.#scope,\n });\n\n let response: Response;\n try {\n response = await fetch(this.#tokenEndpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: body.toString(),\n });\n } catch (err) {\n throw new SdkError(`token request to ${this.#tokenEndpoint} failed: network error`, {\n cause: err,\n });\n }\n\n const text = await response.text();\n if (!response.ok) {\n throw new SdkError(\n `token request to ${this.#tokenEndpoint} failed (${response.status}): ${text}`,\n { httpStatus: response.status },\n );\n }\n\n let parsed: TokenResponse;\n try {\n parsed = JSON.parse(text) as TokenResponse;\n } catch (err) {\n throw new SdkError(`token response from ${this.#tokenEndpoint} is not valid JSON: ${text}`, {\n cause: err,\n });\n }\n\n const ttlSeconds = parsed.expires_in ?? DEFAULT_TOKEN_TTL_SECONDS;\n return {\n accessToken: parsed.access_token,\n expiresAtMs: Date.now() + Math.max(ttlSeconds - EXPIRY_SAFETY_MARGIN_SECONDS, 0) * 1000,\n };\n }\n\n async get(): Promise<string> {\n if (this.#cached != null && this.#cached.expiresAtMs > Date.now()) {\n return this.#cached.accessToken;\n }\n\n if (this.#inFlight) {\n return await this.#inFlight;\n }\n\n this.#inFlight = (async () => {\n try {\n const token = await this.#requestToken();\n this.#cached = token;\n return token.accessToken;\n } finally {\n this.#inFlight = null;\n }\n })();\n\n return await this.#inFlight;\n }\n\n invalidate(): void {\n this.#cached = null;\n }\n}\n"],"names":["randomUUID","readFileSync","importPKCS8","SignJWT","SdkError","ASSERTION_TTL_SECONDS","CLIENT_ASSERTION_TYPE","DEFAULT_TOKEN_TTL_SECONDS","EXPIRY_SAFETY_MARGIN_SECONDS","PrivateKeyJwtTokenStore","options","keyMaterial","issuer","replace","clientId","rawScope","scope","trimmedScope","trim","length","audience","pem","pemString","undefined","keyPath","err","cause","fromKeyPath","fromKeyPem","key","now","Math","floor","Date","aud","setProtectedHeader","alg","kid","setIssuer","setSubject","setAudience","setJti","setIssuedAt","setExpirationTime","sign","assertion","body","URLSearchParams","grant_type","client_id","client_assertion_type","client_assertion","response","fetch","method","headers","toString","text","ok","status","httpStatus","parsed","JSON","parse","ttlSeconds","expires_in","accessToken","access_token","expiresAtMs","max","get","token","invalidate"],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Types describing vsms domain models and procedures.
3
+ *
4
+ * DESIGN DECISION: These types are deliberately hand-written rather than
5
+ * generated via `cratestack generate-typescript`.
6
+ *
7
+ * The full schema is large (23 models, 18 enums, 18 procedures), mostly
8
+ * concerning admin console operations and internal routing state. This SDK
9
+ * is focused purely on the integrator experience: sending messages, checking
10
+ * delivery, and previewing routing. Exposing the full CrateStack-generated
11
+ * types would clutter the SDK with internal models that integrators cannot
12
+ * access anyway. By hand-writing these types, we curate the public API
13
+ * surface to exactly what the SDK exposes.
14
+ */
15
+ export type Encoding = "gsm7" | "ucs2";
16
+ export type OperatorCode = "mtn" | "orange" | "camtel" | "nexttel" | "unknown";
17
+ export type MessageClass = "otp" | "transactional" | "notification" | "marketing";
18
+ export type MessageState = "accepted" | "queued" | "routed" | "submitted" | "delivered" | "uncertain" | "undelivered" | "failed" | "expired" | "rejected" | "cancelled";
19
+ export interface PreviewInput {
20
+ body: string;
21
+ to?: string;
22
+ }
23
+ export interface PreviewResult {
24
+ encoding: Encoding;
25
+ segments: number;
26
+ length: number;
27
+ perSegment: number;
28
+ offending: string[];
29
+ suggestion?: string;
30
+ operator: OperatorCode;
31
+ normalizedTo?: string;
32
+ }
33
+ export interface SendMessageInput {
34
+ to: string;
35
+ body: string;
36
+ senderId?: string;
37
+ class?: MessageClass;
38
+ clientRef?: string;
39
+ /** ISO-8601 string */
40
+ scheduledAt?: string;
41
+ validityMinutes?: number;
42
+ }
43
+ export interface SendMessageResult {
44
+ messageId: string;
45
+ state: MessageState;
46
+ encoding: Encoding;
47
+ segments: number;
48
+ operator: OperatorCode;
49
+ estimatedCostXaf: string;
50
+ }
51
+ export interface Message {
52
+ id: string;
53
+ state: MessageState;
54
+ to: string;
55
+ body: string;
56
+ senderId?: string;
57
+ class: MessageClass;
58
+ clientRef?: string;
59
+ encoding: Encoding;
60
+ segments: number;
61
+ operator: OperatorCode;
62
+ estimatedCostXaf: string;
63
+ providerMessageRef?: string;
64
+ createdAt: string;
65
+ updatedAt: string;
66
+ [key: string]: unknown;
67
+ }
68
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AACvC,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;AAC/E,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,eAAe,GAAG,cAAc,GAAG,WAAW,CAAC;AAElF,MAAM,MAAM,YAAY,GACpB,UAAU,GACV,QAAQ,GACR,QAAQ,GACR,WAAW,GACX,WAAW,GACX,WAAW,GACX,aAAa,GACb,QAAQ,GACR,SAAS,GACT,UAAU,GACV,WAAW,CAAC;AAEhB,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,YAAY,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,YAAY,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
package/dist/types.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Types describing vsms domain models and procedures.
3
+ *
4
+ * DESIGN DECISION: These types are deliberately hand-written rather than
5
+ * generated via `cratestack generate-typescript`.
6
+ *
7
+ * The full schema is large (23 models, 18 enums, 18 procedures), mostly
8
+ * concerning admin console operations and internal routing state. This SDK
9
+ * is focused purely on the integrator experience: sending messages, checking
10
+ * delivery, and previewing routing. Exposing the full CrateStack-generated
11
+ * types would clutter the SDK with internal models that integrators cannot
12
+ * access anyway. By hand-writing these types, we curate the public API
13
+ * surface to exactly what the SDK exposes.
14
+ */ export { };
15
+
16
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Types describing vsms domain models and procedures.\n *\n * DESIGN DECISION: These types are deliberately hand-written rather than\n * generated via `cratestack generate-typescript`.\n *\n * The full schema is large (23 models, 18 enums, 18 procedures), mostly\n * concerning admin console operations and internal routing state. This SDK\n * is focused purely on the integrator experience: sending messages, checking\n * delivery, and previewing routing. Exposing the full CrateStack-generated\n * types would clutter the SDK with internal models that integrators cannot\n * access anyway. By hand-writing these types, we curate the public API\n * surface to exactly what the SDK exposes.\n */\nexport type Encoding = \"gsm7\" | \"ucs2\";\nexport type OperatorCode = \"mtn\" | \"orange\" | \"camtel\" | \"nexttel\" | \"unknown\";\nexport type MessageClass = \"otp\" | \"transactional\" | \"notification\" | \"marketing\";\n\nexport type MessageState =\n | \"accepted\"\n | \"queued\"\n | \"routed\"\n | \"submitted\"\n | \"delivered\"\n | \"uncertain\"\n | \"undelivered\"\n | \"failed\"\n | \"expired\"\n | \"rejected\"\n | \"cancelled\";\n\nexport interface PreviewInput {\n body: string;\n to?: string;\n}\n\nexport interface PreviewResult {\n encoding: Encoding;\n segments: number;\n length: number;\n perSegment: number;\n offending: string[];\n suggestion?: string;\n operator: OperatorCode;\n normalizedTo?: string;\n}\n\nexport interface SendMessageInput {\n to: string;\n body: string;\n senderId?: string;\n class?: MessageClass;\n clientRef?: string;\n /** ISO-8601 string */\n scheduledAt?: string;\n validityMinutes?: number;\n}\n\nexport interface SendMessageResult {\n messageId: string;\n state: MessageState;\n encoding: Encoding;\n segments: number;\n operator: OperatorCode;\n estimatedCostXaf: string;\n}\n\nexport interface Message {\n id: string;\n state: MessageState;\n to: string;\n body: string;\n senderId?: string;\n class: MessageClass;\n clientRef?: string;\n encoding: Encoding;\n segments: number;\n operator: OperatorCode;\n estimatedCostXaf: string;\n providerMessageRef?: string;\n createdAt: string;\n updatedAt: string;\n [key: string]: unknown;\n}\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GAsDD,WAgBC"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@vaam-apps/vsms-node",
3
+ "version": "0.3.3",
4
+ "description": "Official Node.js SDK for vsms: owns the private_key_jwt credential lifecycle so integrators stop hand-rolling token acquisition.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/vaam-apps/vsms.git",
9
+ "directory": "sdks/node/vsms-sdk-node"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "dependencies": {
29
+ "jose": "^6.2.12"
30
+ },
31
+ "devDependencies": {
32
+ "@swc/cli": "^0.8.1",
33
+ "@swc/core": "^1.16.2",
34
+ "@types/node": "^26",
35
+ "typescript": "5.9.3"
36
+ },
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "scripts": {
41
+ "build": "swc src -d dist --strip-leading-paths && tsc -p tsconfig.json --emitDeclarationOnly",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "node --test"
44
+ }
45
+ }