@amirstack/chapar 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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shopify Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ 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, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # @amirstack/chapar
@@ -0,0 +1,13 @@
1
+ import { type ChaparConfiguration } from './config';
2
+ type SendParameters = {
3
+ content: {
4
+ text: string;
5
+ };
6
+ };
7
+ export declare class Chapar {
8
+ #private;
9
+ constructor(config?: ChaparConfiguration);
10
+ send(params: SendParameters): Promise<void>;
11
+ }
12
+ export {};
13
+ //# sourceMappingURL=chapar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chapar.d.ts","sourceRoot":"","sources":["../src/lib/chapar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAIjE,KAAK,cAAc,GAAG;IACpB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;CACF,CAAA;AAED,qBAAa,MAAM;;IAGjB,YAAY,MAAM,CAAC,EAAE,mBAAmB,EAEvC;IAoBK,IAAI,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhD;CACF"}
@@ -0,0 +1,18 @@
1
+ export type ChaparConfiguration = {
2
+ apiUrl?: string;
3
+ auth?: {
4
+ clientId?: string;
5
+ clientSecret?: string;
6
+ };
7
+ };
8
+ type DeepRequired<T> = T extends (...args: any[]) => any ? T : T extends object ? {
9
+ [K in keyof T]-?: DeepRequired<T[K]>;
10
+ } : T;
11
+ type ResolvedConfiguration = DeepRequired<ChaparConfiguration>;
12
+ export declare class ChaparConfig {
13
+ #private;
14
+ constructor(config?: ChaparConfiguration);
15
+ resolve(): ResolvedConfiguration;
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/lib/config.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE;QACL,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;CACF,CAAA;AAED,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GACpD,CAAC,GACD,CAAC,SAAS,MAAM,GACd;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GACxC,CAAC,CAAA;AAEP,KAAK,qBAAqB,GAAG,YAAY,CAAC,mBAAmB,CAAC,CAAA;AAE9D,qBAAa,YAAY;;IAGvB,YAAY,MAAM,CAAC,EAAE,mBAAmB,EAEvC;IA0BD,OAAO,IAAI,qBAAqB,CAoB/B;CACF"}
@@ -0,0 +1,22 @@
1
+ export declare class ChaparError extends Error {
2
+ }
3
+ export declare class ChaparAuthError extends ChaparError {
4
+ }
5
+ export declare class ChaparUnparsableError extends ChaparError {
6
+ }
7
+ export declare class ChaparServerError extends ChaparError {
8
+ }
9
+ export declare class ChaparSendError extends ChaparError {
10
+ readonly code: string;
11
+ readonly details: unknown;
12
+ constructor(error: {
13
+ code: string;
14
+ message: string;
15
+ details?: unknown;
16
+ });
17
+ }
18
+ export declare class ChaparUnhandledError extends ChaparError {
19
+ }
20
+ export declare class ChaparConfigResolvingFailure extends ChaparError {
21
+ }
22
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/lib/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,WAAY,SAAQ,KAAK;CAAG;AACzC,qBAAa,eAAgB,SAAQ,WAAW;CAAG;AACnD,qBAAa,qBAAsB,SAAQ,WAAW;CAAG;AACzD,qBAAa,iBAAkB,SAAQ,WAAW;CAAG;AACrD,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IAEzB,YAAY,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,EAItE;CACF;AACD,qBAAa,oBAAqB,SAAQ,WAAW;CAAG;AACxD,qBAAa,4BAA6B,SAAQ,WAAW;CAAG"}
@@ -0,0 +1,4 @@
1
+ export { Chapar } from './chapar';
2
+ export { ChaparConfig, type ChaparConfiguration } from './config';
3
+ export { ChaparAuthError, ChaparConfigResolvingFailure, ChaparError, ChaparSendError, ChaparServerError, ChaparUnhandledError, ChaparUnparsableError, } from './errors';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,UAAU,CAAA;AACjE,OAAO,EACL,eAAe,EACf,4BAA4B,EAC5B,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,UAAU,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare function parseResponse(response: Response): Promise<unknown>;
2
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../src/lib/response.ts"],"names":[],"mappings":"AA0DA,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAaxE"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@amirstack/chapar",
3
+ "version": "0.1.0",
4
+ "description": "Client for sending messages through the Chapar API.",
5
+ "author": "Amir Ziaei <a@amirziaei.me> (https://amirziaei.me)",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/amir-ziaei/chapar",
10
+ "directory": "packages/client"
11
+ },
12
+ "homepage": "https://github.com/amir-ziaei/chapar/tree/main/packages/client#readme",
13
+ "files": [
14
+ "LICENSE",
15
+ "README.md",
16
+ "dist",
17
+ "src",
18
+ "!src/**/*.test.ts"
19
+ ],
20
+ "type": "module",
21
+ "exports": {
22
+ ".": "./src/lib/index.ts",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "publishConfig": {
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ }
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "test": "vitest",
37
+ "lint": "oxlint",
38
+ "lint:fix": "oxlint --fix",
39
+ "fmt": "oxfmt --check",
40
+ "fmt:fix": "oxfmt",
41
+ "types": "tsc --noEmit",
42
+ "validate": "bun --parallel lint fmt types build",
43
+ "prepublishOnly": "bun run build"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bun": "latest",
47
+ "msw": "^2.14.6",
48
+ "vitest": "^4.1.10"
49
+ }
50
+ }
@@ -0,0 +1,40 @@
1
+ import { ChaparConfig, type ChaparConfiguration } from './config'
2
+ import { ChaparUnhandledError } from './errors'
3
+ import { parseResponse } from './response'
4
+
5
+ type SendParameters = {
6
+ content: {
7
+ text: string
8
+ }
9
+ }
10
+
11
+ export class Chapar {
12
+ readonly #config: ChaparConfig
13
+
14
+ constructor(config?: ChaparConfiguration) {
15
+ this.#config = new ChaparConfig(config)
16
+ }
17
+
18
+ async #request(params: SendParameters): Promise<Response> {
19
+ const config = this.#config.resolve()
20
+ try {
21
+ return await fetch(config.apiUrl, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'CF-Access-Client-Id': config.auth.clientId,
25
+ 'CF-Access-Client-Secret': config.auth.clientSecret,
26
+ },
27
+ body: JSON.stringify(params),
28
+ })
29
+ } catch (cause) {
30
+ throw new ChaparUnhandledError('The request could not be completed', {
31
+ cause,
32
+ })
33
+ }
34
+ }
35
+
36
+ async send(params: SendParameters): Promise<void> {
37
+ const response = await this.#request(params)
38
+ await parseResponse(response)
39
+ }
40
+ }
@@ -0,0 +1,71 @@
1
+ import { ChaparConfigResolvingFailure } from './errors'
2
+
3
+ export type ChaparConfiguration = {
4
+ apiUrl?: string
5
+ auth?: {
6
+ clientId?: string
7
+ clientSecret?: string
8
+ }
9
+ }
10
+
11
+ type DeepRequired<T> = T extends (...args: any[]) => any
12
+ ? T
13
+ : T extends object
14
+ ? { [K in keyof T]-?: DeepRequired<T[K]> }
15
+ : T
16
+
17
+ type ResolvedConfiguration = DeepRequired<ChaparConfiguration>
18
+
19
+ export class ChaparConfig {
20
+ readonly #config?: ChaparConfiguration
21
+
22
+ constructor(config?: ChaparConfiguration) {
23
+ this.#config = config
24
+ }
25
+
26
+ #readEnv(key: string): string | undefined {
27
+ if (typeof process === 'undefined' || typeof process.env === 'undefined') {
28
+ return undefined
29
+ }
30
+ return process.env[key]
31
+ }
32
+
33
+ #resolveValue(
34
+ value: string | undefined,
35
+ envKey: string,
36
+ name: string,
37
+ ): string {
38
+ if (value) {
39
+ return value
40
+ }
41
+ const envValue = this.#readEnv(envKey)
42
+ if (envValue) {
43
+ return envValue
44
+ }
45
+ throw new ChaparConfigResolvingFailure(
46
+ `Missing required configuration: ${name}`,
47
+ )
48
+ }
49
+
50
+ resolve(): ResolvedConfiguration {
51
+ return {
52
+ apiUrl: this.#resolveValue(
53
+ this.#config?.apiUrl,
54
+ 'CHAPAR_API_URL',
55
+ 'apiUrl',
56
+ ),
57
+ auth: {
58
+ clientId: this.#resolveValue(
59
+ this.#config?.auth?.clientId,
60
+ 'CHAPAR_AUTH_CLIENT_ID',
61
+ 'auth.clientId',
62
+ ),
63
+ clientSecret: this.#resolveValue(
64
+ this.#config?.auth?.clientSecret,
65
+ 'CHAPAR_AUTH_CLIENT_SECRET',
66
+ 'auth.clientSecret',
67
+ ),
68
+ },
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,16 @@
1
+ export class ChaparError extends Error {}
2
+ export class ChaparAuthError extends ChaparError {}
3
+ export class ChaparUnparsableError extends ChaparError {}
4
+ export class ChaparServerError extends ChaparError {}
5
+ export class ChaparSendError extends ChaparError {
6
+ readonly code: string
7
+ readonly details: unknown
8
+
9
+ constructor(error: { code: string; message: string; details?: unknown }) {
10
+ super(error.message)
11
+ this.code = error.code
12
+ this.details = error.details
13
+ }
14
+ }
15
+ export class ChaparUnhandledError extends ChaparError {}
16
+ export class ChaparConfigResolvingFailure extends ChaparError {}
@@ -0,0 +1,11 @@
1
+ export { Chapar } from './chapar'
2
+ export { ChaparConfig, type ChaparConfiguration } from './config'
3
+ export {
4
+ ChaparAuthError,
5
+ ChaparConfigResolvingFailure,
6
+ ChaparError,
7
+ ChaparSendError,
8
+ ChaparServerError,
9
+ ChaparUnhandledError,
10
+ ChaparUnparsableError,
11
+ } from './errors'
@@ -0,0 +1,72 @@
1
+ import {
2
+ ChaparAuthError,
3
+ ChaparSendError,
4
+ ChaparServerError,
5
+ ChaparUnparsableError,
6
+ } from './errors'
7
+
8
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
9
+ typeof value === 'object' && value !== null
10
+
11
+ type ParsedEnvelope =
12
+ | { ok: true; data: unknown }
13
+ | {
14
+ ok: false
15
+ error: { code: string; message: string; details: unknown }
16
+ }
17
+
18
+ function parseEnvelope(data: unknown): ParsedEnvelope {
19
+ if (!isRecord(data) || typeof data.ok !== 'boolean') {
20
+ throw new ChaparUnparsableError()
21
+ }
22
+
23
+ if (data.ok) {
24
+ return { ok: true, data: data.data }
25
+ }
26
+
27
+ const { error } = data
28
+ if (
29
+ !isRecord(error) ||
30
+ typeof error.code !== 'string' ||
31
+ typeof error.message !== 'string'
32
+ ) {
33
+ throw new ChaparUnparsableError()
34
+ }
35
+
36
+ return {
37
+ ok: false,
38
+ error: {
39
+ code: error.code,
40
+ message: error.message,
41
+ details: error.details,
42
+ },
43
+ }
44
+ }
45
+
46
+ async function readJson(response: Response): Promise<unknown> {
47
+ if (response.headers.get('Content-Type') !== 'application/json') {
48
+ throw new ChaparUnparsableError()
49
+ }
50
+ try {
51
+ return await response.json()
52
+ } catch (cause) {
53
+ throw new ChaparUnparsableError('The response body was not valid JSON', {
54
+ cause,
55
+ })
56
+ }
57
+ }
58
+
59
+ export async function parseResponse(response: Response): Promise<unknown> {
60
+ if (response.status === 401 || response.status === 403) {
61
+ throw new ChaparAuthError()
62
+ }
63
+ if (response.status >= 500) {
64
+ throw new ChaparServerError()
65
+ }
66
+
67
+ const parsed = parseEnvelope(await readJson(response))
68
+ if (!parsed.ok) {
69
+ throw new ChaparSendError(parsed.error)
70
+ }
71
+ return parsed.data
72
+ }