@rashidee/co2 1.2.5
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/dist/index.js +25449 -0
- package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
- package/drizzle/0001_wise_wolf_cub.sql +13 -0
- package/drizzle/0002_secret_spyke.sql +16 -0
- package/drizzle/0003_workspace_config_singleton.sql +16 -0
- package/drizzle/0004_terminal_claim_v120.sql +11 -0
- package/drizzle/0005_brainstorm_v124.sql +10 -0
- package/drizzle/0006_project_v140.sql +6 -0
- package/drizzle/0007_prd_wizard_v124.sql +14 -0
- package/drizzle/0008_workflow_artifact_v126.sql +24 -0
- package/drizzle/0009_project_import_v150.sql +7 -0
- package/drizzle/0010_quality_v151.sql +3 -0
- package/drizzle/meta/0000_snapshot.json +1335 -0
- package/drizzle/meta/0001_snapshot.json +1416 -0
- package/drizzle/meta/0002_snapshot.json +1510 -0
- package/drizzle/meta/_journal.json +83 -0
- package/package.json +41 -0
- package/plugin/.claude-plugin/marketplace.json +36 -0
- package/plugin/.claude-plugin/plugin.json +22 -0
- package/plugin/README.md +625 -0
- package/plugin/SKILLS.md +350 -0
- package/plugin/co2.svg +5 -0
- package/plugin/commands/brainstorm-loop.md +129 -0
- package/plugin/skills/conductor-defect/SKILL.md +898 -0
- package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
- package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
- package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
- package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
- package/plugin/skills/depgen-k8s/SKILL.md +468 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
- package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
- package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
- package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
- package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
- package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
- package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
- package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
- package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
- package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
- package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
- package/plugin/skills/modelgen-relational/SKILL.md +707 -0
- package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
- package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
- package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
- package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
- package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
- package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
- package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
- package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
- package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
- package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
- package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
- package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
- package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
- package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
- package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
- package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
- package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
- package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
- package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
- package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
- package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
- package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
- package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
- package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
- package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
- package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
- package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
- package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
- package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
- package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
- package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
- package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
- package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
- package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
- package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
- package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
- package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
- package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
- package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
- package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
- package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
- package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
- package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
- package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
- package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
- package/plugin/skills/testgen-functional/SKILL.md +1381 -0
- package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
- package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
- package/plugin/skills/util-projectinit/SKILL.md +307 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
- package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
- package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
- package/plugin/skills/util-projectsync/SKILL.md +501 -0
- package/plugin/skills/util-updprd/SKILL.md +324 -0
- package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
- package/plugin/skills/util-ustagger/SKILL.md +154 -0
- package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
- package/static/assets/arc-DRKw7sjs.js +1 -0
- package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
- package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
- package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
- package/static/assets/channel-C2nVbuwv.js +1 -0
- package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
- package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
- package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
- package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
- package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
- package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
- package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
- package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
- package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
- package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
- package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
- package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
- package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
- package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
- package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
- package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
- package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
- package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
- package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
- package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
- package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
- package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
- package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
- package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
- package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
- package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
- package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
- package/static/assets/graph-C9eacEi8.js +1 -0
- package/static/assets/index-LZSQkLE2.css +41 -0
- package/static/assets/index-y7XDSgMG.js +495 -0
- package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
- package/static/assets/init-Gi6I4Gst.js +1 -0
- package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
- package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
- package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
- package/static/assets/katex-C5jXJg4s.js +257 -0
- package/static/assets/layout-DEXfKzaS.js +1 -0
- package/static/assets/linear-CLO--ryk.js +1 -0
- package/static/assets/map-Czzmt4hB.js +1 -0
- package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
- package/static/assets/ordinal-Cboi1Yqb.js +1 -0
- package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
- package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
- package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
- package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
- package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
- package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
- package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
- package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
- package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
- package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
- package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
- package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
- package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
- package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
- package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
- package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
- package/static/index.html +24 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
# HTTP Client Patterns — OkHttp Wiring
|
|
2
|
+
|
|
3
|
+
This reference describes the canonical OkHttp wiring the generated spec must produce.
|
|
4
|
+
The SDK uses **one shared `OkHttpClient`** for the entire library lifetime; every
|
|
5
|
+
service routes through that client; every cross-cutting concern is implemented as an
|
|
6
|
+
`Interceptor`.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why a Single Shared `OkHttpClient`
|
|
11
|
+
|
|
12
|
+
OkHttp's `OkHttpClient` is **expensive to create** (it owns a connection pool, a
|
|
13
|
+
dispatcher, and a thread pool) and **cheap to share** (it is fully thread-safe). The
|
|
14
|
+
SDK MUST:
|
|
15
|
+
|
|
16
|
+
- Build exactly one `OkHttpClient` per `{{ClientName}}` instance
|
|
17
|
+
- Hand the same instance to every module's service
|
|
18
|
+
- Expose a `close()` method on the facade that shuts down the dispatcher and evicts the
|
|
19
|
+
pool
|
|
20
|
+
|
|
21
|
+
> **Anti-pattern:** Creating a new `OkHttpClient.Builder().build()` per service or per
|
|
22
|
+
> request. This leaks file descriptors and hammers the remote API with new connections.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Interceptor Chain
|
|
27
|
+
|
|
28
|
+
Every request flows through this ordered chain:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
[application code]
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
[CorrelationIdInterceptor] ← adds X-Correlation-Id if absent
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
[AuthInterceptor] ← injects Authorization or X-API-Key (conditional)
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
[RetryInterceptor] ← strategic retry on 502/503/504 + IOException
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
[LoggingInterceptor] ← redacted req/res logging via SLF4J (conditional)
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
[OkHttp dispatcher → network]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The order matters:
|
|
50
|
+
|
|
51
|
+
1. **Correlation ID first** — so the same ID propagates through retries and is logged.
|
|
52
|
+
2. **Auth before retry** — if a refresh happens, retry sees the new token.
|
|
53
|
+
3. **Retry before logging** — so logging sees the final attempt's wire data, not every
|
|
54
|
+
attempt.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Interceptor Implementations
|
|
59
|
+
|
|
60
|
+
### CorrelationIdInterceptor
|
|
61
|
+
|
|
62
|
+
```java
|
|
63
|
+
package {{BASE_PACKAGE}}.http;
|
|
64
|
+
|
|
65
|
+
import okhttp3.Interceptor;
|
|
66
|
+
import okhttp3.Request;
|
|
67
|
+
import okhttp3.Response;
|
|
68
|
+
|
|
69
|
+
import java.io.IOException;
|
|
70
|
+
import java.util.UUID;
|
|
71
|
+
|
|
72
|
+
final class CorrelationIdInterceptor implements Interceptor {
|
|
73
|
+
|
|
74
|
+
static final String HEADER = "X-Correlation-Id";
|
|
75
|
+
|
|
76
|
+
@Override
|
|
77
|
+
public Response intercept(Chain chain) throws IOException {
|
|
78
|
+
Request original = chain.request();
|
|
79
|
+
if (original.header(HEADER) != null) {
|
|
80
|
+
return chain.proceed(original);
|
|
81
|
+
}
|
|
82
|
+
Request withId = original.newBuilder()
|
|
83
|
+
.header(HEADER, UUID.randomUUID().toString())
|
|
84
|
+
.build();
|
|
85
|
+
return chain.proceed(withId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### AuthInterceptor (Bearer)
|
|
91
|
+
|
|
92
|
+
```java
|
|
93
|
+
package {{BASE_PACKAGE}}.auth;
|
|
94
|
+
|
|
95
|
+
import okhttp3.Interceptor;
|
|
96
|
+
import okhttp3.Request;
|
|
97
|
+
import okhttp3.Response;
|
|
98
|
+
|
|
99
|
+
import java.io.IOException;
|
|
100
|
+
|
|
101
|
+
public final class AuthInterceptor implements Interceptor {
|
|
102
|
+
|
|
103
|
+
private final BearerCredentialProvider provider;
|
|
104
|
+
|
|
105
|
+
public AuthInterceptor(BearerCredentialProvider provider) {
|
|
106
|
+
this.provider = provider;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Override
|
|
110
|
+
public Response intercept(Chain chain) throws IOException {
|
|
111
|
+
Request request = applyToken(chain.request());
|
|
112
|
+
Response response = chain.proceed(request);
|
|
113
|
+
if (response.code() != 401) {
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
// 401 → refresh once, replay once.
|
|
117
|
+
response.close();
|
|
118
|
+
provider.refresh();
|
|
119
|
+
Request retry = applyToken(chain.request());
|
|
120
|
+
return chain.proceed(retry);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private Request applyToken(Request request) {
|
|
124
|
+
String token = provider.token();
|
|
125
|
+
if (token == null || token.isEmpty()) return request;
|
|
126
|
+
return request.newBuilder()
|
|
127
|
+
.header("Authorization", "Bearer " + token)
|
|
128
|
+
.build();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### AuthInterceptor (API Key)
|
|
134
|
+
|
|
135
|
+
```java
|
|
136
|
+
public final class ApiKeyInterceptor implements Interceptor {
|
|
137
|
+
|
|
138
|
+
private final String headerName;
|
|
139
|
+
private final String value;
|
|
140
|
+
|
|
141
|
+
public ApiKeyInterceptor(String headerName, String value) {
|
|
142
|
+
this.headerName = headerName;
|
|
143
|
+
this.value = value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Override
|
|
147
|
+
public Response intercept(Chain chain) throws IOException {
|
|
148
|
+
Request req = chain.request().newBuilder()
|
|
149
|
+
.header(headerName, value)
|
|
150
|
+
.build();
|
|
151
|
+
return chain.proceed(req);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### AuthInterceptor (HTTP Basic)
|
|
157
|
+
|
|
158
|
+
```java
|
|
159
|
+
import okhttp3.Credentials;
|
|
160
|
+
|
|
161
|
+
public final class BasicAuthInterceptor implements Interceptor {
|
|
162
|
+
|
|
163
|
+
private final String credential;
|
|
164
|
+
|
|
165
|
+
public BasicAuthInterceptor(String username, String password) {
|
|
166
|
+
this.credential = Credentials.basic(username, password);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Override
|
|
170
|
+
public Response intercept(Chain chain) throws IOException {
|
|
171
|
+
Request req = chain.request().newBuilder()
|
|
172
|
+
.header("Authorization", credential)
|
|
173
|
+
.build();
|
|
174
|
+
return chain.proceed(req);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### mTLS — `SSLSocketFactory` Configuration
|
|
180
|
+
|
|
181
|
+
```java
|
|
182
|
+
package {{BASE_PACKAGE}}.auth;
|
|
183
|
+
|
|
184
|
+
import okhttp3.OkHttpClient;
|
|
185
|
+
|
|
186
|
+
import javax.net.ssl.KeyManagerFactory;
|
|
187
|
+
import javax.net.ssl.SSLContext;
|
|
188
|
+
import javax.net.ssl.TrustManagerFactory;
|
|
189
|
+
import javax.net.ssl.X509TrustManager;
|
|
190
|
+
import java.io.InputStream;
|
|
191
|
+
import java.nio.file.Files;
|
|
192
|
+
import java.nio.file.Path;
|
|
193
|
+
import java.security.KeyStore;
|
|
194
|
+
|
|
195
|
+
public final class MTlsConfigurer {
|
|
196
|
+
|
|
197
|
+
private MTlsConfigurer() {}
|
|
198
|
+
|
|
199
|
+
public static OkHttpClient.Builder configure(
|
|
200
|
+
OkHttpClient.Builder builder,
|
|
201
|
+
Path keystorePath, char[] keystorePassword,
|
|
202
|
+
Path truststorePath, char[] truststorePassword) {
|
|
203
|
+
|
|
204
|
+
try (InputStream ksIn = Files.newInputStream(keystorePath);
|
|
205
|
+
InputStream tsIn = Files.newInputStream(truststorePath)) {
|
|
206
|
+
|
|
207
|
+
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
|
208
|
+
keyStore.load(ksIn, keystorePassword);
|
|
209
|
+
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
|
|
210
|
+
KeyManagerFactory.getDefaultAlgorithm());
|
|
211
|
+
kmf.init(keyStore, keystorePassword);
|
|
212
|
+
|
|
213
|
+
KeyStore trustStore = KeyStore.getInstance("PKCS12");
|
|
214
|
+
trustStore.load(tsIn, truststorePassword);
|
|
215
|
+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
|
216
|
+
TrustManagerFactory.getDefaultAlgorithm());
|
|
217
|
+
tmf.init(trustStore);
|
|
218
|
+
|
|
219
|
+
X509TrustManager tm = (X509TrustManager) tmf.getTrustManagers()[0];
|
|
220
|
+
|
|
221
|
+
SSLContext ctx = SSLContext.getInstance("TLS");
|
|
222
|
+
ctx.init(kmf.getKeyManagers(), new javax.net.ssl.TrustManager[]{tm}, null);
|
|
223
|
+
|
|
224
|
+
return builder.sslSocketFactory(ctx.getSocketFactory(), tm);
|
|
225
|
+
} catch (Exception e) {
|
|
226
|
+
throw new IllegalStateException("mTLS configuration failed", e);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### RetryInterceptor
|
|
233
|
+
|
|
234
|
+
```java
|
|
235
|
+
package {{BASE_PACKAGE}}.http;
|
|
236
|
+
|
|
237
|
+
import okhttp3.Interceptor;
|
|
238
|
+
import okhttp3.Request;
|
|
239
|
+
import okhttp3.Response;
|
|
240
|
+
|
|
241
|
+
import java.io.IOException;
|
|
242
|
+
|
|
243
|
+
final class RetryInterceptor implements Interceptor {
|
|
244
|
+
|
|
245
|
+
private final RetryStrategy strategy;
|
|
246
|
+
|
|
247
|
+
RetryInterceptor(RetryStrategy strategy) {
|
|
248
|
+
this.strategy = strategy;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@Override
|
|
252
|
+
public Response intercept(Chain chain) throws IOException {
|
|
253
|
+
Request request = chain.request();
|
|
254
|
+
int attempt = 0;
|
|
255
|
+
IOException lastError = null;
|
|
256
|
+
|
|
257
|
+
while (true) {
|
|
258
|
+
Response response = null;
|
|
259
|
+
IOException error = null;
|
|
260
|
+
try {
|
|
261
|
+
response = chain.proceed(request);
|
|
262
|
+
} catch (IOException e) {
|
|
263
|
+
error = e;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
RetryStrategy.Decision decision = strategy.evaluate(attempt, response, error);
|
|
267
|
+
if (!decision.retry) {
|
|
268
|
+
if (error != null) throw error;
|
|
269
|
+
return response;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (response != null) response.close();
|
|
273
|
+
lastError = error;
|
|
274
|
+
attempt++;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
Thread.sleep(decision.delayMillis);
|
|
278
|
+
} catch (InterruptedException ie) {
|
|
279
|
+
Thread.currentThread().interrupt();
|
|
280
|
+
if (lastError != null) throw new IOException("Interrupted while retrying", lastError);
|
|
281
|
+
throw new IOException("Interrupted while retrying");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### LoggingInterceptor (SLF4J, with redaction)
|
|
289
|
+
|
|
290
|
+
```java
|
|
291
|
+
package {{BASE_PACKAGE}}.http;
|
|
292
|
+
|
|
293
|
+
import okhttp3.Interceptor;
|
|
294
|
+
import okhttp3.Request;
|
|
295
|
+
import okhttp3.Response;
|
|
296
|
+
import org.slf4j.Logger;
|
|
297
|
+
import org.slf4j.LoggerFactory;
|
|
298
|
+
|
|
299
|
+
import java.io.IOException;
|
|
300
|
+
|
|
301
|
+
final class LoggingInterceptor implements Interceptor {
|
|
302
|
+
|
|
303
|
+
private static final Logger log = LoggerFactory.getLogger("{{BASE_PACKAGE}}.http");
|
|
304
|
+
|
|
305
|
+
@Override
|
|
306
|
+
public Response intercept(Chain chain) throws IOException {
|
|
307
|
+
Request req = chain.request();
|
|
308
|
+
if (log.isDebugEnabled()) {
|
|
309
|
+
log.debug("--> {} {} (auth={})",
|
|
310
|
+
req.method(), req.url(),
|
|
311
|
+
req.header("Authorization") == null ? "none" : "Bearer ***");
|
|
312
|
+
}
|
|
313
|
+
long t0 = System.nanoTime();
|
|
314
|
+
Response resp = chain.proceed(req);
|
|
315
|
+
if (log.isDebugEnabled()) {
|
|
316
|
+
long ms = (System.nanoTime() - t0) / 1_000_000L;
|
|
317
|
+
log.debug("<-- {} {} ({} ms)", resp.code(), req.url(), ms);
|
|
318
|
+
}
|
|
319
|
+
return resp;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
> The `LoggingInterceptor` is registered ONLY when SLF4J is on the consumer's
|
|
325
|
+
> classpath (see `Slf4jPresence` in `references/spec-template.md`). The interceptor
|
|
326
|
+
> never logs the request or response BODY — bodies often contain credentials or PII
|
|
327
|
+
> and OkHttp's `consume`-once contract makes safe body logging non-trivial.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Per-Service Call Pattern
|
|
332
|
+
|
|
333
|
+
Every service method follows this template:
|
|
334
|
+
|
|
335
|
+
```java
|
|
336
|
+
public OrderDto getById(String id) {
|
|
337
|
+
HttpUrl url = HttpUrl.parse(config.baseUrl())
|
|
338
|
+
.newBuilder()
|
|
339
|
+
.addPathSegments("orders/" + id)
|
|
340
|
+
.build();
|
|
341
|
+
|
|
342
|
+
Request req = new Request.Builder()
|
|
343
|
+
.url(url)
|
|
344
|
+
.header("Accept", "application/json")
|
|
345
|
+
.get()
|
|
346
|
+
.build();
|
|
347
|
+
|
|
348
|
+
try (Response resp = http.newCall(req).execute()) {
|
|
349
|
+
if (resp.code() == 404) {
|
|
350
|
+
throw new OrdersException.NotFound(404, body(resp), "GET", url.toString());
|
|
351
|
+
}
|
|
352
|
+
if (!resp.isSuccessful()) {
|
|
353
|
+
throw new OrdersException(resp.code(), body(resp), "GET", url.toString());
|
|
354
|
+
}
|
|
355
|
+
@SuppressWarnings("unchecked")
|
|
356
|
+
Map<String, Object> map = (Map<String, Object>) Json.parse(body(resp));
|
|
357
|
+
return OrderDto.fromJsonMap(map);
|
|
358
|
+
} catch (IOException e) {
|
|
359
|
+
throw new SdkClientException("GET " + url + " failed", e);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private static String body(Response resp) throws IOException {
|
|
364
|
+
return resp.body() != null ? resp.body().string() : "";
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Notes:
|
|
369
|
+
|
|
370
|
+
- `try (Response resp = ...)` is mandatory — OkHttp `Response` holds connection
|
|
371
|
+
resources that MUST be closed.
|
|
372
|
+
- `resp.body().string()` consumes the body and closes the stream — call it once.
|
|
373
|
+
- Path segments use `addPathSegments("orders/" + id)` so `id` is URL-encoded by
|
|
374
|
+
OkHttp. NEVER concatenate raw IDs into the URL string.
|
|
375
|
+
- Query parameters use `addQueryParameter("status", status.wireValue())`.
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Async Surface (CompletableFuture)
|
|
380
|
+
|
|
381
|
+
If Async API = yes, every sync method has a `*Async` variant:
|
|
382
|
+
|
|
383
|
+
```java
|
|
384
|
+
public CompletableFuture<OrderDto> getByIdAsync(String id) {
|
|
385
|
+
Request req = /* same builder as the sync version */;
|
|
386
|
+
CompletableFuture<OrderDto> future = new CompletableFuture<>();
|
|
387
|
+
http.newCall(req).enqueue(new Callback() {
|
|
388
|
+
@Override
|
|
389
|
+
public void onFailure(Call call, IOException e) {
|
|
390
|
+
future.completeExceptionally(new SdkClientException("GET " + req.url() + " failed", e));
|
|
391
|
+
}
|
|
392
|
+
@Override
|
|
393
|
+
public void onResponse(Call call, Response resp) {
|
|
394
|
+
try (Response r = resp) {
|
|
395
|
+
if (!r.isSuccessful()) {
|
|
396
|
+
future.completeExceptionally(new OrdersException(r.code(), body(r), "GET", req.url().toString()));
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
@SuppressWarnings("unchecked")
|
|
400
|
+
Map<String, Object> map = (Map<String, Object>) Json.parse(body(r));
|
|
401
|
+
future.complete(OrderDto.fromJsonMap(map));
|
|
402
|
+
} catch (Exception e) {
|
|
403
|
+
future.completeExceptionally(e);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
return future;
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
The sync wrapper is a one-liner over the async one when Async = all:
|
|
412
|
+
|
|
413
|
+
```java
|
|
414
|
+
public OrderDto getById(String id) {
|
|
415
|
+
try {
|
|
416
|
+
return getByIdAsync(id).join();
|
|
417
|
+
} catch (CompletionException ce) {
|
|
418
|
+
Throwable cause = ce.getCause();
|
|
419
|
+
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
|
|
420
|
+
throw new SdkClientException("getById failed", cause);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
OkHttp's dispatcher already manages a thread pool — the SDK does NOT spin up its own.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## WebSocket [CONDITIONAL]
|
|
430
|
+
|
|
431
|
+
OkHttp on JDK 8 supports WebSocket via `OkHttpClient#newWebSocket`:
|
|
432
|
+
|
|
433
|
+
```java
|
|
434
|
+
public Subscription subscribe(String topic, EventListener listener) {
|
|
435
|
+
Request req = new Request.Builder()
|
|
436
|
+
.url(config.baseUrl() + "/ws")
|
|
437
|
+
.header("Authorization", "Bearer " + token())
|
|
438
|
+
.build();
|
|
439
|
+
WebSocket ws = http.newWebSocket(req, new WebSocketListener() {
|
|
440
|
+
@Override public void onMessage(WebSocket s, String text) { listener.onEvent(text); }
|
|
441
|
+
@Override public void onFailure(WebSocket s, Throwable t, Response r) { listener.onError(t); }
|
|
442
|
+
@Override public void onClosed(WebSocket s, int code, String reason) { listener.onClosed(code, reason); }
|
|
443
|
+
});
|
|
444
|
+
return () -> ws.close(1000, "client closed");
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
The public `Subscription` interface is a single-method functional interface:
|
|
449
|
+
|
|
450
|
+
```java
|
|
451
|
+
public interface Subscription extends AutoCloseable {
|
|
452
|
+
@Override void close();
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
The JDK 11 overlay MAY substitute `java.net.http.HttpClient.newWebSocketBuilder()`
|
|
457
|
+
for performance — but the public surface is identical, so consumers see no difference.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Server-Sent Events (SSE) [CONDITIONAL]
|
|
462
|
+
|
|
463
|
+
Requires `okhttp-sse` (also a Square artifact, same group). If SSE is selected, add:
|
|
464
|
+
|
|
465
|
+
```xml
|
|
466
|
+
<dependency>
|
|
467
|
+
<groupId>com.squareup.okhttp3</groupId>
|
|
468
|
+
<artifactId>okhttp-sse</artifactId>
|
|
469
|
+
<version>${okhttp.version}</version>
|
|
470
|
+
</dependency>
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
…and relocate `okhttp3.sse` along with `okhttp3` in the Shade plugin (it's the same
|
|
474
|
+
package prefix, so no extra rule is needed).
|
|
475
|
+
|
|
476
|
+
```java
|
|
477
|
+
EventSources.createFactory(http).newEventSource(req, listener);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Configuration Knobs Surface
|
|
483
|
+
|
|
484
|
+
The `Config` class exposes (immutable, all `Duration`-typed):
|
|
485
|
+
|
|
486
|
+
- `baseUrl` — required
|
|
487
|
+
- `connectTimeout`, `readTimeout`, `writeTimeout` — default 30s each
|
|
488
|
+
- `callTimeout` — default 0 (off; use the per-stage timeouts)
|
|
489
|
+
- `maxIdleConnections` — default 10
|
|
490
|
+
- `keepAliveDuration` — default 5 min
|
|
491
|
+
- `retryStrategy` — defaults to `ExponentialBackoffRetryStrategy.defaults()`
|
|
492
|
+
- `credentialProvider` — null if Auth = none
|
|
493
|
+
|
|
494
|
+
Every knob is a Builder method on `{{ClientName}}.Builder`. Validation
|
|
495
|
+
(e.g., `baseUrl != null`) happens in `build()`.
|