@mushi-mushi/core 0.5.1 → 0.8.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/CONTRIBUTING.md +4 -0
- package/README.md +27 -2
- package/SECURITY.md +74 -0
- package/dist/index.cjs +86 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -3
- package/dist/index.d.ts +151 -3
- package/dist/index.js +85 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CONTRIBUTING.md
CHANGED
|
@@ -33,6 +33,10 @@ pnpm lint # ESLint
|
|
|
33
33
|
pnpm format # Prettier
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Ad-hoc screenshots captured during UI reviews can live temporarily at the repo
|
|
37
|
+
root, but root-level `*.png` files are intentionally ignored. Canonical
|
|
38
|
+
screenshots that should be versioned belong under `docs/screenshots/`.
|
|
39
|
+
|
|
36
40
|
### Working on a single package
|
|
37
41
|
|
|
38
42
|
```bash
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Core types, API client, and utilities for the Mushi Mushi SDK.
|
|
|
7
7
|
## What's Inside
|
|
8
8
|
|
|
9
9
|
- **Types**: `MushiConfig`, `MushiReport`, `MushiEnvironment`, and all shared interfaces
|
|
10
|
-
- **API Client**: Fetch-based HTTP client with retry and exponential backoff
|
|
10
|
+
- **API Client**: Fetch-based HTTP client with retry and exponential backoff. Tags every internal request with `X-Mushi-Internal: <kind>` so framework SDKs can filter their own traffic out of network capture and `apiCascade`. Ships HMAC-signed reporter helpers (`getLatestSdkVersion`, `listReporterReports`, `listReporterComments`, `replyToReporterReport`) for the two-way reply pipeline
|
|
11
11
|
- **Pre-Filter**: On-device Stage 0 spam/gibberish filter (runs client-side, zero server cost)
|
|
12
12
|
- **Offline Queue**: IndexedDB-backed queue with auto-sync on reconnect
|
|
13
13
|
- **Environment Capture**: Browser/device snapshot (viewport, user agent, connection info)
|
|
@@ -15,10 +15,35 @@ Core types, API client, and utilities for the Mushi Mushi SDK.
|
|
|
15
15
|
- **Session ID**: Tab-scoped session correlation
|
|
16
16
|
- **Rate Limiter**: Token bucket self-throttle to prevent API flooding
|
|
17
17
|
|
|
18
|
+
## Public types added in 0.7 → 0.11
|
|
19
|
+
|
|
20
|
+
| Type | Purpose |
|
|
21
|
+
|----------------------------|-----------------------------------------------------------------------------------------------|
|
|
22
|
+
| `MushiPreset` | `'production-calm' \| 'beta-loud' \| 'internal-debug' \| 'manual-only'` posture bundles. |
|
|
23
|
+
| `MushiWidgetAnchor` | Raw-CSS positioning (`top` / `right` / `bottom` / `left`) for the widget launcher. |
|
|
24
|
+
| `MushiPrivacyConfig` | `maskSelectors`, `blockSelectors`, `allowUserRemoveScreenshot` for screenshot redaction. |
|
|
25
|
+
| `MushiUrlMatcher` | `string \| RegExp` element used by `capture.ignoreUrls` and `apiCascade.ignoreUrls`. |
|
|
26
|
+
| `MushiApiCascadeConfig` | Object form of `proactive.apiCascade` so URL filters can be declared per-host-app. |
|
|
27
|
+
| `MushiDiagnosticsResult` | Return shape of `Mushi.diagnose()` (CSP, runtime-config, capture, widget health). |
|
|
28
|
+
| `MushiSdkVersionInfo` | Response shape for `getLatestSdkVersion(packageName)`; powers the outdated-banner UI. |
|
|
29
|
+
| `MushiTimelineEntry` | `{ ts, kind: 'route' \| 'click' \| 'request' \| 'log' \| 'screen', payload }` repro entries. |
|
|
30
|
+
| `MushiReporterReport` | Reporter-facing report row (HMAC-authed) with `unread_count` for the widget badge. |
|
|
31
|
+
| `MushiReporterComment` | Reporter-facing comment row (HMAC-authed) tagged `author_kind: 'admin' \| 'reporter'`. |
|
|
32
|
+
|
|
33
|
+
Constants: `MUSHI_INTERNAL_HEADER` (`'X-Mushi-Internal'`),
|
|
34
|
+
`MUSHI_INTERNAL_INIT_MARKER`, and the `MushiInternalRequestKind` literal union
|
|
35
|
+
are re-exported so framework adapters can build their own self-noise filters.
|
|
36
|
+
|
|
18
37
|
## Usage
|
|
19
38
|
|
|
20
39
|
```typescript
|
|
21
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
createApiClient,
|
|
42
|
+
createPreFilter,
|
|
43
|
+
captureEnvironment,
|
|
44
|
+
createRateLimiter,
|
|
45
|
+
MUSHI_INTERNAL_HEADER,
|
|
46
|
+
} from '@mushi-mushi/core';
|
|
22
47
|
```
|
|
23
48
|
|
|
24
49
|
This package is used internally by `@mushi-mushi/web` and `@mushi-mushi/react`. Most consumers should use those packages instead.
|
package/SECURITY.md
CHANGED
|
@@ -48,3 +48,77 @@ We will acknowledge receipt within 48 hours and aim to release a patch within 7
|
|
|
48
48
|
- **Rotate API keys** regularly via the admin console
|
|
49
49
|
- **Enable SSO** for team projects (Enterprise tier)
|
|
50
50
|
- **Review audit logs** periodically for suspicious activity
|
|
51
|
+
|
|
52
|
+
## Supply-chain hardening (how this package is protected)
|
|
53
|
+
|
|
54
|
+
Mushi Mushi is built and published with the controls below. Consumers can
|
|
55
|
+
verify each control independently — the goal is to make tampering both
|
|
56
|
+
difficult and detectable.
|
|
57
|
+
|
|
58
|
+
### Publish-time controls
|
|
59
|
+
|
|
60
|
+
| Control | What it does | How to verify |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| **npm Trusted Publisher (OIDC)** | Every release is published from `.github/workflows/release.yml` on `master` using a short-lived OIDC token. Long-lived `NPM_TOKEN` is not used for publishing. | `npm view @mushi-mushi/<pkg> --json` shows `"trustedPublisher"` populated for recent versions. |
|
|
63
|
+
| **npm provenance attestations** | Every published tarball ships a [Sigstore provenance attestation](https://docs.npmjs.com/generating-provenance-statements) cryptographically linking the tarball to the exact GitHub Actions run that built it. | `npm audit signatures` (run inside any project that depends on `@mushi-mushi/*`) reports `verified registry signatures` and `verified attestations`. The npm web UI shows a "Built and signed on GitHub Actions" badge on each version. |
|
|
64
|
+
| **Pre-publish workspace-protocol guard** | Aborts the publish if `workspace:*` ranges leaked into the tarball (the bug class behind the v0.1.0 incident). | `scripts/check-workspace-protocol.mjs` runs before `changeset publish` in `pnpm release`. |
|
|
65
|
+
| **Post-publish tarball verification** | Re-downloads each just-published tarball and asserts it doesn't contain `workspace:*`. | See the "Verify published tarballs do not contain workspace:*" step in `release.yml`. |
|
|
66
|
+
| **Post-publish `npm audit signatures`** | Re-installs each published version and validates registry signatures + provenance against npm's transparency log. | See the "Audit signatures of installed dependencies" step in `release.yml`. |
|
|
67
|
+
|
|
68
|
+
### Build-time controls
|
|
69
|
+
|
|
70
|
+
| Control | What it does |
|
|
71
|
+
|---|---|
|
|
72
|
+
| **All third-party GitHub Actions pinned to commit SHAs** | Every `uses:` in every workflow under `.github/workflows/` is pinned to a 40-character commit SHA with a version comment. Floating tags (`@v4`, `@main`) are mutable and were the entry point for the [tj-actions/changed-files compromise (CVE-2025-30066)](https://github.com/step-security/harden-runner#detected-attacks). |
|
|
73
|
+
| **Harden-Runner egress audit on every job** | [step-security/harden-runner](https://github.com/step-security/harden-runner) records every outbound network call, file write, and process spawn on every CI runner. Detects exfiltration attempts in real time — caught the tj-actions, NX, Shai-Hulud, and Axios attacks for other projects. |
|
|
74
|
+
| **OpenSSF Scorecard** | Weekly + on-push score of the repo's security posture (Pinned-Dependencies, Token-Permissions, Branch-Protection, Code-Review, Dangerous-Workflow, Maintained, SAST, Security-Policy, Signed-Releases, Vulnerabilities). Public results at [scorecard.dev](https://scorecard.dev/viewer/?uri=github.com/kensaurus/mushi-mushi). |
|
|
75
|
+
| **Server-side secret scan (Gitleaks)** | Every PR and every push to `master` runs Gitleaks across the diff / full tree. Belt-and-suspenders to the local pre-commit hook (`scripts/check-no-secrets.mjs`) which can be bypassed with `--no-verify`. |
|
|
76
|
+
| **Local pre-commit secret scanner** | `scripts/check-no-secrets.mjs` runs as a git hook installed by `pnpm install`, blocking commits that look like AWS / Stripe / GitHub / Anthropic / OpenAI / Slack / Supabase keys. |
|
|
77
|
+
| **CodeQL `security-extended`** | Semantic analysis of every TypeScript / JavaScript change finds injection sinks, taint flows, prototype pollution, etc. Runs on every PR, push, and weekly cron. |
|
|
78
|
+
| **Dependency review on PRs** | `actions/dependency-review-action` blocks the PR if it adds or upgrades a dep with a high-severity advisory. |
|
|
79
|
+
| **`pnpm audit --prod --audit-level=high`** | Weekly cron + every push to `master` fails on any high/critical advisory in production deps. |
|
|
80
|
+
|
|
81
|
+
### Install-time controls (protect the project's own dependency graph)
|
|
82
|
+
|
|
83
|
+
| Control | What it does |
|
|
84
|
+
|---|---|
|
|
85
|
+
| **`min-release-age` (npm) / `minimumReleaseAge` (pnpm)** | Refuses to resolve any dep version published less than 7 days ago. The Axios 1.14.1 / 0.30.4 compromise (Mar 2026) was detected and removed within ~5 hours; Shai-Hulud (Sep 2025) within <12 hours — a 7-day cooldown blocks every publicly-disclosed 2025–2026 npm supply-chain attack outright. |
|
|
86
|
+
| **`strictDepBuilds: true`** | Fails the install if any transitive dep tries to run a `postinstall` hook the workspace hasn't pre-approved (`onlyBuiltDependencies` allow-list). |
|
|
87
|
+
| **`blockExoticSubdeps: true`** | Refuses to resolve transitive deps from git URLs, tarball URLs, or filesystem paths — anything that didn't go through the npm registry's signing pipeline. |
|
|
88
|
+
| **Dependabot with cooldown** | Routine dep upgrades wait 7 days; security advisories bypass the cooldown automatically. |
|
|
89
|
+
| **`pnpm audit signatures`-style verification** | The release pipeline re-runs `npm audit signatures` against each published version after the publish, with `--audit-level=high`. |
|
|
90
|
+
|
|
91
|
+
### Verifying a Mushi Mushi tarball before installing
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# 1. Check provenance attestation matches the public GitHub Actions run
|
|
95
|
+
npm view @mushi-mushi/core --json | jq '.signatures, .dist'
|
|
96
|
+
|
|
97
|
+
# 2. Inside your own project after install
|
|
98
|
+
npm audit signatures
|
|
99
|
+
|
|
100
|
+
# Expected: every @mushi-mushi/* package reports
|
|
101
|
+
# "verified registry signature"
|
|
102
|
+
# "verified attestation"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If `npm audit signatures` reports any `@mushi-mushi/*` package as unsigned
|
|
106
|
+
or with an invalid attestation, **stop the install and email
|
|
107
|
+
kensaurus@gmail.com immediately** — that's the symptom of either a
|
|
108
|
+
registry compromise or a tampered tarball, and we want to know within
|
|
109
|
+
hours, not days.
|
|
110
|
+
|
|
111
|
+
### What this hardening does NOT cover
|
|
112
|
+
|
|
113
|
+
- **Self-hosted deployments.** Once the package is on your machine, the
|
|
114
|
+
security of your `node_modules`, build pipeline, and runtime is your
|
|
115
|
+
responsibility. The hardening above protects the path from source to
|
|
116
|
+
registry; it cannot protect a tarball after it has been downloaded.
|
|
117
|
+
- **Compromise of `kensaurus@gmail.com`.** A trusted-publisher rule still
|
|
118
|
+
lets the legitimate maintainer publish from any branch they push. If
|
|
119
|
+
you find yourself with admin access to this repo, treat
|
|
120
|
+
`.github/workflows/release.yml` as a tier-0 secret.
|
|
121
|
+
- **First-party bugs.** Provenance proves *who* built the tarball and
|
|
122
|
+
*when*; it does not prove the code is bug-free. CodeQL + tests cover
|
|
123
|
+
that surface, but no automation catches everything — please continue
|
|
124
|
+
to report issues to the address above.
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/api-client.ts
|
|
4
4
|
var DEFAULT_API_ENDPOINT = "https://dxptnwrhwsqckaftyymj.supabase.co/functions/v1/api";
|
|
5
|
+
var MUSHI_INTERNAL_HEADER = "X-Mushi-Internal";
|
|
6
|
+
var MUSHI_INTERNAL_INIT_MARKER = "__mushiInternal";
|
|
5
7
|
var DEFAULT_TIMEOUT = 1e4;
|
|
6
8
|
var DEFAULT_MAX_RETRIES = 2;
|
|
7
9
|
function createApiClient(options) {
|
|
@@ -13,7 +15,7 @@ function createApiClient(options) {
|
|
|
13
15
|
maxRetries = DEFAULT_MAX_RETRIES
|
|
14
16
|
} = options;
|
|
15
17
|
let baseUrl = apiEndpoint.replace(/\/$/, "");
|
|
16
|
-
async function request(method, path, body, retries = maxRetries) {
|
|
18
|
+
async function request(method, path, body, retries = maxRetries, internalKind) {
|
|
17
19
|
const url = `${baseUrl}${path}`;
|
|
18
20
|
const controller = new AbortController();
|
|
19
21
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
@@ -23,10 +25,12 @@ function createApiClient(options) {
|
|
|
23
25
|
headers: {
|
|
24
26
|
"Content-Type": "application/json",
|
|
25
27
|
"X-Mushi-Api-Key": apiKey,
|
|
26
|
-
"X-Mushi-Project": projectId
|
|
28
|
+
"X-Mushi-Project": projectId,
|
|
29
|
+
...internalKind ? { [MUSHI_INTERNAL_HEADER]: internalKind } : {}
|
|
27
30
|
},
|
|
28
31
|
body: body ? JSON.stringify(body) : void 0,
|
|
29
|
-
signal: controller.signal
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
...internalKind ? { [MUSHI_INTERNAL_INIT_MARKER]: internalKind } : {}
|
|
30
34
|
});
|
|
31
35
|
clearTimeout(timer);
|
|
32
36
|
if (response.status === 307 || response.status === 308) {
|
|
@@ -35,7 +39,7 @@ function createApiClient(options) {
|
|
|
35
39
|
const targetBase = target.replace(/\/v1\/.*$/, "").replace(/\/$/, "");
|
|
36
40
|
if (targetBase !== baseUrl) {
|
|
37
41
|
baseUrl = targetBase;
|
|
38
|
-
return request(method, path, body, retries - 1);
|
|
42
|
+
return request(method, path, body, retries - 1, internalKind);
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
@@ -43,7 +47,7 @@ function createApiClient(options) {
|
|
|
43
47
|
const errorBody = await response.json().catch(() => ({}));
|
|
44
48
|
if (response.status >= 500 && retries > 0) {
|
|
45
49
|
await sleep(getBackoffDelay(maxRetries - retries));
|
|
46
|
-
return request(method, path, body, retries - 1);
|
|
50
|
+
return request(method, path, body, retries - 1, internalKind);
|
|
47
51
|
}
|
|
48
52
|
return {
|
|
49
53
|
ok: false,
|
|
@@ -60,7 +64,7 @@ function createApiClient(options) {
|
|
|
60
64
|
clearTimeout(timer);
|
|
61
65
|
if (retries > 0 && isRetryable(error)) {
|
|
62
66
|
await sleep(getBackoffDelay(maxRetries - retries));
|
|
63
|
-
return request(method, path, body, retries - 1);
|
|
67
|
+
return request(method, path, body, retries - 1, internalKind);
|
|
64
68
|
}
|
|
65
69
|
return {
|
|
66
70
|
ok: false,
|
|
@@ -71,18 +75,87 @@ function createApiClient(options) {
|
|
|
71
75
|
};
|
|
72
76
|
}
|
|
73
77
|
}
|
|
78
|
+
async function requestForReporter(method, path, reporterToken, body) {
|
|
79
|
+
const tokenHash = await sha256Hex(reporterToken);
|
|
80
|
+
const ts = String(Date.now());
|
|
81
|
+
const hmac = await hmacSha256Hex(apiKey, `${projectId}.${ts}.${tokenHash}`);
|
|
82
|
+
const url = `${baseUrl}${path}`;
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
method,
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"X-Mushi-Api-Key": apiKey,
|
|
88
|
+
"X-Mushi-Project": projectId,
|
|
89
|
+
[MUSHI_INTERNAL_HEADER]: "reporter-poll",
|
|
90
|
+
"X-Reporter-Token-Hash": tokenHash,
|
|
91
|
+
"X-Reporter-Ts": ts,
|
|
92
|
+
"X-Reporter-Hmac": hmac
|
|
93
|
+
},
|
|
94
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
95
|
+
[MUSHI_INTERNAL_INIT_MARKER]: "reporter-poll"
|
|
96
|
+
});
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: {
|
|
102
|
+
code: `HTTP_${response.status}`,
|
|
103
|
+
message: errorBody.error?.message ?? errorBody.message ?? `HTTP ${response.status} error`
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const payload = await response.json();
|
|
108
|
+
return { ok: true, data: payload.data ?? payload };
|
|
109
|
+
}
|
|
74
110
|
return {
|
|
75
111
|
async submitReport(report) {
|
|
76
|
-
return request("POST", "/v1/reports", report);
|
|
112
|
+
return request("POST", "/v1/reports", report, maxRetries, "report-submit");
|
|
77
113
|
},
|
|
78
114
|
async getReportStatus(reportId) {
|
|
79
|
-
return request("GET", `/v1/reports/${reportId}/status
|
|
115
|
+
return request("GET", `/v1/reports/${reportId}/status`, void 0, maxRetries, "report-status");
|
|
80
116
|
},
|
|
81
117
|
async getSdkConfig() {
|
|
82
|
-
return request("GET", "/v1/sdk/config");
|
|
118
|
+
return request("GET", "/v1/sdk/config", void 0, maxRetries, "sdk-config");
|
|
119
|
+
},
|
|
120
|
+
async getLatestSdkVersion(packageName) {
|
|
121
|
+
const query = new URLSearchParams({ package: packageName }).toString();
|
|
122
|
+
return request("GET", `/v1/sdk/latest-version?${query}`, void 0, maxRetries, "sdk-config");
|
|
123
|
+
},
|
|
124
|
+
async listReporterReports(reporterToken) {
|
|
125
|
+
return requestForReporter("GET", "/v1/reporter/reports", reporterToken);
|
|
126
|
+
},
|
|
127
|
+
async listReporterComments(reportId, reporterToken) {
|
|
128
|
+
return requestForReporter(
|
|
129
|
+
"GET",
|
|
130
|
+
`/v1/reporter/reports/${reportId}/comments`,
|
|
131
|
+
reporterToken
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
async replyToReporterReport(reportId, reporterToken, body) {
|
|
135
|
+
return requestForReporter(
|
|
136
|
+
"POST",
|
|
137
|
+
`/v1/reporter/reports/${reportId}/reply`,
|
|
138
|
+
reporterToken,
|
|
139
|
+
{ body }
|
|
140
|
+
);
|
|
83
141
|
}
|
|
84
142
|
};
|
|
85
143
|
}
|
|
144
|
+
async function sha256Hex(value) {
|
|
145
|
+
const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
146
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
147
|
+
}
|
|
148
|
+
async function hmacSha256Hex(secret, value) {
|
|
149
|
+
const key = await crypto.subtle.importKey(
|
|
150
|
+
"raw",
|
|
151
|
+
new TextEncoder().encode(secret),
|
|
152
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
153
|
+
false,
|
|
154
|
+
["sign"]
|
|
155
|
+
);
|
|
156
|
+
const buffer = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value));
|
|
157
|
+
return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
158
|
+
}
|
|
86
159
|
function sleep(ms) {
|
|
87
160
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
88
161
|
}
|
|
@@ -776,7 +849,7 @@ function collectInputs() {
|
|
|
776
849
|
hardwareConcurrency: nav?.hardwareConcurrency
|
|
777
850
|
};
|
|
778
851
|
}
|
|
779
|
-
async function
|
|
852
|
+
async function sha256Hex2(input) {
|
|
780
853
|
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
781
854
|
const buf = new TextEncoder().encode(input);
|
|
782
855
|
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
@@ -796,7 +869,7 @@ async function getDeviceFingerprintHash() {
|
|
|
796
869
|
}
|
|
797
870
|
const inputs = collectInputs();
|
|
798
871
|
const serialised = JSON.stringify(inputs);
|
|
799
|
-
const hash = await
|
|
872
|
+
const hash = await sha256Hex2(serialised);
|
|
800
873
|
if (typeof localStorage !== "undefined") {
|
|
801
874
|
try {
|
|
802
875
|
localStorage.setItem(CACHE_KEY, hash);
|
|
@@ -933,6 +1006,8 @@ function scrubPii(text, config) {
|
|
|
933
1006
|
}
|
|
934
1007
|
|
|
935
1008
|
exports.DEFAULT_API_ENDPOINT = DEFAULT_API_ENDPOINT;
|
|
1009
|
+
exports.MUSHI_INTERNAL_HEADER = MUSHI_INTERNAL_HEADER;
|
|
1010
|
+
exports.MUSHI_INTERNAL_INIT_MARKER = MUSHI_INTERNAL_INIT_MARKER;
|
|
936
1011
|
exports.REGION_ENDPOINTS = REGION_ENDPOINTS;
|
|
937
1012
|
exports.captureEnvironment = captureEnvironment;
|
|
938
1013
|
exports.createApiClient = createApiClient;
|