@ai-content-space/loopx 0.1.2 → 0.1.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/README.md +343 -56
- package/README.zh-CN.md +392 -0
- package/package.json +4 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/scripts/plugin-install.test.mjs +1 -0
- package/plugins/loopx/skills/archive/SKILL.md +39 -0
- package/plugins/loopx/skills/build/SKILL.md +111 -9
- package/plugins/loopx/skills/clarify/SKILL.md +121 -1
- package/plugins/loopx/skills/debug/SKILL.md +296 -0
- package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
- package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
- package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
- package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
- package/plugins/loopx/skills/go-style/SKILL.md +71 -0
- package/plugins/loopx/skills/kratos/SKILL.md +74 -0
- package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
- package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
- package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
- package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
- package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
- package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
- package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
- package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
- package/plugins/loopx/skills/plan/SKILL.md +22 -2
- package/plugins/loopx/skills/review/SKILL.md +98 -1
- package/plugins/loopx/skills/tdd/SKILL.md +371 -0
- package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
- package/plugins/loopx/skills/verify/SKILL.md +139 -0
- package/scripts/codex-stop-hook.mjs +71 -0
- package/scripts/codex-workflow-hook.mjs +153 -0
- package/skills/archive/SKILL.md +39 -0
- package/skills/build/SKILL.md +111 -9
- package/skills/clarify/SKILL.md +121 -1
- package/skills/debug/SKILL.md +296 -0
- package/skills/debug/condition-based-waiting.md +115 -0
- package/skills/debug/defense-in-depth.md +122 -0
- package/skills/debug/find-polluter.sh +63 -0
- package/skills/debug/root-cause-tracing.md +169 -0
- package/skills/go-style/SKILL.md +71 -0
- package/skills/kratos/SKILL.md +74 -0
- package/skills/kratos/references/advanced-features.md +314 -0
- package/skills/kratos/references/architecture.md +488 -0
- package/skills/kratos/references/configuration.md +399 -0
- package/skills/kratos/references/http-customization.md +512 -0
- package/skills/kratos/references/middleware-logging.md +400 -0
- package/skills/kratos/references/proto-api-design.md +432 -0
- package/skills/kratos/references/security-auth.md +411 -0
- package/skills/kratos/references/troubleshooting.md +385 -0
- package/skills/plan/SKILL.md +18 -2
- package/skills/review/SKILL.md +98 -1
- package/skills/tdd/SKILL.md +371 -0
- package/skills/tdd/testing-anti-patterns.md +299 -0
- package/skills/verify/SKILL.md +139 -0
- package/src/build-runtime.mjs +303 -26
- package/src/build-stop-gate.mjs +94 -0
- package/src/cli.mjs +47 -5
- package/src/codex-exec-runtime.mjs +105 -5
- package/src/context-manifest.mjs +172 -0
- package/src/install-discovery.mjs +352 -5
- package/src/next-skill.mjs +57 -5
- package/src/plan-runtime.mjs +79 -122
- package/src/review-runtime.mjs +378 -0
- package/src/runtime-maintenance.mjs +428 -14
- package/src/template-governance.mjs +223 -0
- package/src/workflow.mjs +1941 -117
- package/src/workspace-context.mjs +166 -0
- package/src/workspace-memory.mjs +69 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Security & Auth
|
|
2
|
+
|
|
3
|
+
Guide for JWT, Casbin, idempotency, and data masking in Kratos.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
|
|
7
|
+
- Extracting JWT claims from context
|
|
8
|
+
- Setting up Casbin authorization
|
|
9
|
+
- Implementing idempotency middleware
|
|
10
|
+
- Data masking for sensitive fields
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## JWT Context Extraction
|
|
15
|
+
|
|
16
|
+
### Get Payload from Context
|
|
17
|
+
|
|
18
|
+
```go
|
|
19
|
+
import (
|
|
20
|
+
"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
|
|
21
|
+
jwtV4 "github.com/golang-jwt/jwt/v4"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
func getPayloadFromCtx(ctx context.Context, partName string) (string, error) {
|
|
25
|
+
if claims, ok := jwt.FromContext(ctx); ok {
|
|
26
|
+
if m, ok := claims.(jwtV4.MapClaims); ok {
|
|
27
|
+
if v, ok := m[partName].(string); ok {
|
|
28
|
+
return v, nil
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return "", errors.New("invalid Jwt")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Usage
|
|
36
|
+
func (s *UserService) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.GetUserResponse, error) {
|
|
37
|
+
userID, err := getPayloadFromCtx(ctx, "user_id")
|
|
38
|
+
if err != nil {
|
|
39
|
+
return nil, err
|
|
40
|
+
}
|
|
41
|
+
// Use userID in business logic
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### JWT Middleware Setup
|
|
46
|
+
|
|
47
|
+
```go
|
|
48
|
+
import (
|
|
49
|
+
"github.com/go-kratos/kratos/v2/middleware/auth/jwt"
|
|
50
|
+
jwtV4 "github.com/golang-jwt/jwt/v4"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
var signingKey = []byte("your-secret-key")
|
|
54
|
+
|
|
55
|
+
// jwt.Server requires a Keyfunc for token verification
|
|
56
|
+
srv := http.NewServer(
|
|
57
|
+
http.Address(":8000"),
|
|
58
|
+
http.Middleware(
|
|
59
|
+
jwt.Server(
|
|
60
|
+
func(token *jwtV4.Token) (interface{}, error) {
|
|
61
|
+
// Verify signing method
|
|
62
|
+
if _, ok := token.Method.(*jwtV4.SigningMethodHMAC); !ok {
|
|
63
|
+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
64
|
+
}
|
|
65
|
+
return signingKey, nil
|
|
66
|
+
},
|
|
67
|
+
jwt.WithSigningMethod(jwtV4.SigningMethodHS256),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Why Keyfunc:** The `jwt.Server` middleware requires a function to retrieve the verification key. This allows for key rotation and multi-key scenarios. For simple cases, return the static key directly.
|
|
74
|
+
|
|
75
|
+
### JWT Best Practices
|
|
76
|
+
|
|
77
|
+
From industry standards:
|
|
78
|
+
|
|
79
|
+
1. **Always use HTTPS** - Prevents token interception
|
|
80
|
+
2. **Limit token refresh** - e.g., 50 times per day max
|
|
81
|
+
3. **Short token lifetime** - e.g., 15 minutes for access token
|
|
82
|
+
4. **Use httponly cookies** - Prevent JavaScript access
|
|
83
|
+
5. **SameSite=Strict** - Prevent CSRF
|
|
84
|
+
|
|
85
|
+
```go
|
|
86
|
+
// Cookie settings
|
|
87
|
+
http.SetCookie(ctx, &http.Cookie{
|
|
88
|
+
Name: "token",
|
|
89
|
+
Value: token,
|
|
90
|
+
HttpOnly: true,
|
|
91
|
+
SameSite: http.SameSiteStrictMode,
|
|
92
|
+
Secure: true, // HTTPS only
|
|
93
|
+
MaxAge: 900, // 15 minutes
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Casbin Integration
|
|
100
|
+
|
|
101
|
+
### Model Configuration
|
|
102
|
+
|
|
103
|
+
Use `keyMatch3` for Kratos URL patterns:
|
|
104
|
+
|
|
105
|
+
```c
|
|
106
|
+
# model.conf
|
|
107
|
+
[request_definition]
|
|
108
|
+
r = sub, dom, obj, act
|
|
109
|
+
|
|
110
|
+
[policy_definition]
|
|
111
|
+
p = sub, dom, obj, act
|
|
112
|
+
|
|
113
|
+
[role_definition]
|
|
114
|
+
g = _, _, _
|
|
115
|
+
|
|
116
|
+
[policy_effect]
|
|
117
|
+
e = some(where (p.eft == allow))
|
|
118
|
+
|
|
119
|
+
[matchers]
|
|
120
|
+
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && (regexMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && r.act == p.act
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Why keyMatch3:** Kratos generates URLs like `/api/v1/user/{user_id}`. keyMatch3 matches `{variable}` patterns.
|
|
124
|
+
|
|
125
|
+
### Middleware Setup
|
|
126
|
+
|
|
127
|
+
```go
|
|
128
|
+
import (
|
|
129
|
+
"github.com/casbin/casbin/v2"
|
|
130
|
+
casbinM "github.com/go-kratos/kratos/v2/middleware/auth/casbin"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
func NewCasbinEnforcer() *casbin.Enforcer {
|
|
134
|
+
m := model.NewModelFromFile("model.conf")
|
|
135
|
+
a := fileadapter.NewAdapter("policy.csv")
|
|
136
|
+
e, _ := casbin.NewEnforcer(m, a)
|
|
137
|
+
return e
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
srv := http.NewServer(
|
|
141
|
+
http.Address(":8000"),
|
|
142
|
+
http.Middleware(
|
|
143
|
+
casbinM.Server(
|
|
144
|
+
casbinM.WithEnforcer(NewCasbinEnforcer()),
|
|
145
|
+
casbinM.WithModel(model),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Get URL in Middleware
|
|
152
|
+
|
|
153
|
+
```go
|
|
154
|
+
func MyMiddleware() middleware.Middleware {
|
|
155
|
+
return func(handler middleware.Handler) middleware.Handler {
|
|
156
|
+
return func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
157
|
+
if tr, ok := transport.FromServerContext(ctx); ok {
|
|
158
|
+
if hr, ok := tr.(*http.Transport); ok {
|
|
159
|
+
method := hr.Request().Method
|
|
160
|
+
path := hr.Request().RequestURI
|
|
161
|
+
|
|
162
|
+
// Use for Casbin, logging, etc.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return handler(ctx, req)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Policy Watcher (Hot Refresh)
|
|
172
|
+
|
|
173
|
+
```go
|
|
174
|
+
import "github.com/casbin/casbin/v2/persist"
|
|
175
|
+
|
|
176
|
+
type Watcher struct {
|
|
177
|
+
callback func(string)
|
|
178
|
+
notify chan struct{}
|
|
179
|
+
done chan struct{} // For graceful shutdown
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func (w *Watcher) SetUpdateCallback(fn func(string)) error {
|
|
183
|
+
w.callback = fn
|
|
184
|
+
go func() {
|
|
185
|
+
for {
|
|
186
|
+
select {
|
|
187
|
+
case <-w.notify:
|
|
188
|
+
fn("policy updated")
|
|
189
|
+
case <-w.done:
|
|
190
|
+
return // Graceful shutdown
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}()
|
|
194
|
+
return nil
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func (w *Watcher) Close() {
|
|
198
|
+
close(w.done)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func (w *Watcher) Update() error {
|
|
202
|
+
w.notify <- struct{}{}
|
|
203
|
+
return nil
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Use in biz layer to trigger refresh
|
|
207
|
+
func (uc *RoleUseCase) UpdatePolicy(roles []*Role) error {
|
|
208
|
+
defer uc.watcher.Update() // Notify Casbin to refresh
|
|
209
|
+
// Update policy in database...
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Idempotency Middleware
|
|
216
|
+
|
|
217
|
+
Prevent duplicate operations on non-idempotent endpoints:
|
|
218
|
+
|
|
219
|
+
### Token-Based Idempotency
|
|
220
|
+
|
|
221
|
+
```go
|
|
222
|
+
func IdempotentMiddleware() middleware.Middleware {
|
|
223
|
+
return func(handler middleware.Handler) middleware.Handler {
|
|
224
|
+
return func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
225
|
+
tr, ok := transport.FromServerContext(ctx)
|
|
226
|
+
if !ok {
|
|
227
|
+
return nil, errors.New("no transport")
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check whitelist (some operations don't need idempotency)
|
|
231
|
+
if isWhitelisted(tr.Operation()) {
|
|
232
|
+
return handler(ctx, req)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get idempotency token
|
|
236
|
+
token := tr.RequestHeader().Get("x-idempotent")
|
|
237
|
+
if token == "" {
|
|
238
|
+
return nil, errors.New(400, "MISSING_TOKEN", "x-idempotent header required")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if token was used
|
|
242
|
+
if wasUsed(token) {
|
|
243
|
+
return nil, errors.New(409, "TOKEN_USED", "duplicate request")
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Mark token as used
|
|
247
|
+
markUsed(token)
|
|
248
|
+
|
|
249
|
+
return handler(ctx, req)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Client Usage
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# First request
|
|
259
|
+
curl -X POST -H "x-idempotent: abc123" https://api.example.com/v1/orders
|
|
260
|
+
|
|
261
|
+
# Retry with same token (fails with 409)
|
|
262
|
+
curl -X POST -H "x-idempotent: abc123" https://api.example.com/v1/orders
|
|
263
|
+
|
|
264
|
+
# New request with new token
|
|
265
|
+
curl -X POST -H "x-idempotent: xyz789" https://api.example.com/v1/orders
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Data Masking
|
|
271
|
+
|
|
272
|
+
Hide sensitive data in responses and logs:
|
|
273
|
+
|
|
274
|
+
### Struct Tag Approach
|
|
275
|
+
|
|
276
|
+
```go
|
|
277
|
+
type User struct {
|
|
278
|
+
Name string `json:"name"`
|
|
279
|
+
Mobile string `json:"mobile" mask:"mobile"` // 138****5678
|
|
280
|
+
Email string `json:"email" mask:"email"` // abc***@example.com
|
|
281
|
+
IDCard string `json:"id_card" mask:"idcard"` // 310***1234
|
|
282
|
+
BankCard string `json:"bank_card" mask:"bankcard"` // 6222 **** 0123
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Masking Functions
|
|
287
|
+
|
|
288
|
+
```go
|
|
289
|
+
var defaultRules = map[string]MaskRule{
|
|
290
|
+
"mobile": {
|
|
291
|
+
Pattern: regexp.MustCompile(`^(\d{3})\d{4}(\d{4})$`),
|
|
292
|
+
Replacement: "$1****$2",
|
|
293
|
+
},
|
|
294
|
+
"email": {
|
|
295
|
+
Pattern: regexp.MustCompile(`^(.{3}).*(@.*)$`),
|
|
296
|
+
Replacement: "$1***$2",
|
|
297
|
+
},
|
|
298
|
+
"idcard": {
|
|
299
|
+
Pattern: regexp.MustCompile(`^(.{6}).*(.{4})$`),
|
|
300
|
+
Replacement: "$1********$2",
|
|
301
|
+
},
|
|
302
|
+
"bankcard": {
|
|
303
|
+
Pattern: regexp.MustCompile(`^(\d{4})\d+(\d{4})$`),
|
|
304
|
+
Replacement: "$1 **** **** $2",
|
|
305
|
+
},
|
|
306
|
+
"name": {
|
|
307
|
+
Handler: func(s string) string {
|
|
308
|
+
if len(s) <= 1 {
|
|
309
|
+
return s
|
|
310
|
+
}
|
|
311
|
+
return s[:1] + strings.Repeat("*", len(s)-1)
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Masker Implementation
|
|
318
|
+
|
|
319
|
+
```go
|
|
320
|
+
type Masker interface {
|
|
321
|
+
Mask(interface{}) interface{}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
func (m *DefaultMasker) Mask(data interface{}) interface{} {
|
|
325
|
+
value := reflect.ValueOf(data)
|
|
326
|
+
|
|
327
|
+
if value.Kind() == reflect.Struct {
|
|
328
|
+
result := reflect.New(value.Type()).Elem()
|
|
329
|
+
for i := 0; i < value.NumField(); i++ {
|
|
330
|
+
field := value.Field(i)
|
|
331
|
+
maskTag := value.Type().Field(i).Tag.Get("mask")
|
|
332
|
+
|
|
333
|
+
if maskTag != "" && field.Kind() == reflect.String {
|
|
334
|
+
if rule, ok := m.rules[maskTag]; ok {
|
|
335
|
+
masked := m.applyRule(rule, field.String())
|
|
336
|
+
result.Field(i).SetString(masked)
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
result.Field(i).Set(field)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return result.Interface()
|
|
343
|
+
}
|
|
344
|
+
return data
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Log Filtering
|
|
351
|
+
|
|
352
|
+
Filter sensitive data from logs:
|
|
353
|
+
|
|
354
|
+
```go
|
|
355
|
+
import "github.com/go-kratos/kratos/v2/log"
|
|
356
|
+
|
|
357
|
+
h := log.NewHelper(
|
|
358
|
+
log.NewFilter(logger,
|
|
359
|
+
// Filter by level
|
|
360
|
+
log.FilterLevel(log.LevelError),
|
|
361
|
+
|
|
362
|
+
// Filter by key
|
|
363
|
+
log.FilterKey("password"),
|
|
364
|
+
|
|
365
|
+
// Filter by value
|
|
366
|
+
log.FilterValue("secret"),
|
|
367
|
+
|
|
368
|
+
// Custom filter
|
|
369
|
+
log.FilterFunc(func(level log.Level, keyvals ...interface{}) bool {
|
|
370
|
+
for i := 0; i < len(keyvals); i += 2 {
|
|
371
|
+
if keyvals[i] == "token" {
|
|
372
|
+
keyvals[i+1] = "***MASKED***"
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return false // Return false to continue logging
|
|
376
|
+
}),
|
|
377
|
+
),
|
|
378
|
+
)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Header/Context Extraction
|
|
384
|
+
|
|
385
|
+
Get request information in middleware or service:
|
|
386
|
+
|
|
387
|
+
```go
|
|
388
|
+
import (
|
|
389
|
+
"github.com/go-kratos/kratos/v2/transport"
|
|
390
|
+
"github.com/go-kratos/kratos/v2/transport/http"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
func extractInfo(ctx context.Context) {
|
|
394
|
+
// Get transport
|
|
395
|
+
if tr, ok := transport.FromServerContext(ctx); ok {
|
|
396
|
+
operation := tr.Operation()
|
|
397
|
+
|
|
398
|
+
// Get HTTP-specific info
|
|
399
|
+
if hr, ok := tr.(*http.Transport); ok {
|
|
400
|
+
method := hr.Request().Method
|
|
401
|
+
path := hr.Request().URL.Path
|
|
402
|
+
userAgent := hr.RequestHeader().Get("User-Agent")
|
|
403
|
+
authorization := hr.RequestHeader().Get("Authorization")
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Set response header
|
|
408
|
+
if httpCtx, ok := ctx.(http.Context); ok {
|
|
409
|
+
httpCtx.Response().Header().Set("X-Custom", "value")
|
|
410
|
+
}
|
|
411
|
+
}
|