@stackonward/cli 1.7.0 → 1.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.
Files changed (69) hide show
  1. package/dist/index.js +8 -8
  2. package/dist/index.js.map +1 -1
  3. package/dist/resources/components/nuxt/site-source/app/composables/useWebPush.ts +4 -0
  4. package/dist/resources/components/nuxt/site-source/app/plugins/web-push.ts +52 -0
  5. package/dist/resources/components/nuxt/site-source/app/web-push/worker.ts +8 -0
  6. package/dist/resources/components/registry.json +11 -1
  7. package/dist/resources/registry.json +15 -3
  8. package/dist/resources/templates/base/{.claude → .codex}/docs/architecture-template.md +1 -1
  9. package/dist/resources/templates/base/{.claude → .codex}/docs/architecture.md +1 -1
  10. package/dist/resources/templates/base/{.claude → .codex}/docs/index.md +3 -3
  11. package/dist/resources/templates/base/{.claude → .codex}/docs/verification.md +1 -1
  12. package/dist/resources/templates/base/AGENTS.md +24 -24
  13. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/api-design.md +1 -1
  14. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/architecture-template.md +1 -1
  15. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/check-list.md +3 -3
  16. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/database.md +1 -1
  17. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/error-handling.md +1 -1
  18. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/index.md +1 -1
  19. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/logging.md +1 -1
  20. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/mediagen-provider-onboarding-sop.md +2 -2
  21. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/mediagen-provider-quick-reference.md +1 -1
  22. package/dist/resources/templates/go-backend/{.claude → .codex}/docs/module-standards.md +1 -1
  23. package/dist/resources/templates/go-backend/.dockerignore +1 -2
  24. package/dist/resources/templates/go-backend/AGENTS.md +19 -19
  25. package/dist/resources/templates/go-backend/README.md +5 -1
  26. package/dist/resources/templates/go-backend/deployments/onex/product-onboarding.example.json +201 -0
  27. package/dist/resources/templates/go-backend/docs/onex-notifications.md +85 -0
  28. package/dist/resources/templates/go-backend/docs/openapi-provenance.json +1 -1
  29. package/dist/resources/templates/go-backend/docs/runtime-provenance.json +23 -2
  30. package/dist/resources/templates/go-backend/docs/source-provenance.json +124 -6
  31. package/dist/resources/templates/go-backend/docs/standards-provenance.json +30 -30
  32. package/dist/resources/templates/go-backend/internal/common/middleware/cors.go +1 -0
  33. package/dist/resources/templates/go-backend/internal/common/middleware/middleware_test.go +1 -0
  34. package/dist/resources/templates/go-backend/internal/core/httpclient/safe_post.go +28 -0
  35. package/dist/resources/templates/go-backend/internal/core/httpclient/safe_post_test.go +56 -0
  36. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/client.go +453 -0
  37. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/client_test.go +772 -0
  38. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/codec.go +33 -0
  39. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/error.go +146 -0
  40. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/error_notification_test.go +29 -0
  41. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/notification/client.go +132 -0
  42. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/notification/client_test.go +222 -0
  43. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/notification/contracts.go +219 -0
  44. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/resilience.go +23 -0
  45. package/dist/resources/templates/go-backend/internal/integrations/onex/v1/transport_policy.go +96 -0
  46. package/dist/resources/templates/go-backend/internal/products/README.md +1 -1
  47. package/dist/resources/templates/go-backend/internal/sharedevents/README.md +1 -1
  48. package/dist/resources/templates/go-backend/scripts/check-openapi-clean-deletion.sh +3 -3
  49. package/dist/resources/templates/mcp-server/.prettierignore +1 -0
  50. package/dist/resources/templates/nuxt/{.claude → .codex}/docs/stack.md +10 -0
  51. package/dist/resources/templates/nuxt/.dockerignore +1 -1
  52. package/dist/resources/templates/nuxt/.env.example +6 -0
  53. package/dist/resources/templates/nuxt/.prettierignore +2 -1
  54. package/dist/resources/templates/nuxt/DESIGN.md +1 -1
  55. package/dist/resources/templates/nuxt/README.md +8 -0
  56. package/dist/resources/templates/nuxt/nuxt.config.ts +13 -1
  57. package/dist/resources/templates/nuxt/package.json +4 -2
  58. package/package.json +1 -1
  59. package/dist/resources/templates/base/CLAUDE.md +0 -437
  60. package/dist/resources/templates/go-backend/CLAUDE.md +0 -1139
  61. /package/dist/resources/templates/base/{.claude → .codex}/docs/security-lifecycle.md +0 -0
  62. /package/dist/resources/templates/go-backend/{.claude → .codex}/docs/e2e-standards.md +0 -0
  63. /package/dist/resources/templates/go-backend/{.claude → .codex}/docs/event-runtime-boundary.md +0 -0
  64. /package/dist/resources/templates/go-backend/{.claude → .codex}/docs/identifier-contract.md +0 -0
  65. /package/dist/resources/templates/go-backend/{.claude → .codex}/docs/openapi-contract.md +0 -0
  66. /package/dist/resources/templates/go-backend/{.claude → .codex}/docs/payment-gateway-onboarding.md +0 -0
  67. /package/dist/resources/templates/go-backend/{.claude → .codex}/docs/platform-boundary.md +0 -0
  68. /package/dist/resources/templates/go-backend/{.claude → .codex}/docs/quality-gates.md +0 -0
  69. /package/dist/resources/templates/mcp-server/{.claude → .codex}/docs/stack.md +0 -0
@@ -0,0 +1,453 @@
1
+ package v1
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "mime"
9
+ "net/http"
10
+ "net/url"
11
+ "regexp"
12
+ "strconv"
13
+ "strings"
14
+ "time"
15
+
16
+ "{{module}}/internal/common/internalservice"
17
+ "{{module}}/internal/common/types"
18
+ "{{module}}/internal/core/httpclient"
19
+ )
20
+
21
+ const (
22
+ maximumConfirmedObjectEncodedBytes = ((1 << 20) + 2) / 3 * 4
23
+ maximumResponseEnvelopeBytes = 4 << 10
24
+ defaultMaxResponseBytes = maximumConfirmedObjectEncodedBytes + maximumResponseEnvelopeBytes
25
+ maximumRequestIDBytes = 128
26
+ )
27
+
28
+ var productCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_-]{1,63}$`)
29
+
30
+ // Config 定义 OneX internal v1 HTTP 边界的不可变运行参数。
31
+ type Config struct {
32
+ BaseURL string
33
+ ProductCode string
34
+ Timeout time.Duration
35
+ MaxResponseBytes int
36
+ SafeReadRetry SafeReadRetryConfig
37
+ OperationRetries map[string]int
38
+ RetryWaiter RetryWaiter
39
+ Dependency DependencyExecutor
40
+ }
41
+
42
+ // Call 描述一次带独立 deadline 的 OneX wire 请求。
43
+ type Call struct {
44
+ Method string
45
+ Path string
46
+ Query url.Values
47
+ UserID int64
48
+ RequestID string
49
+ IdempotencyKey string
50
+ Body any
51
+ ExpectedStatuses []int
52
+ RequiredResponseHeaders map[string]string
53
+ Timeout time.Duration
54
+ Dependency string
55
+ Operation string
56
+ }
57
+
58
+ // Client 统一执行 OneX internal v1 认证、可信头和严格响应解码。
59
+ type Client struct {
60
+ httpClient *httpclient.Client
61
+ signer internalservice.Signer
62
+ baseURL string
63
+ productCode string
64
+ maxResponseBytes int
65
+ defaultTimeout time.Duration
66
+ safeReadRetry SafeReadRetryConfig
67
+ operationRetries map[string]int
68
+ retryWaiter RetryWaiter
69
+ dependency DependencyExecutor
70
+ }
71
+
72
+ // NewClient 构造不跟随重定向、仅按显式策略重试安全读取的 OneX v1 专用客户端。
73
+ func NewClient(client *httpclient.Client, signer internalservice.Signer, config Config) (*Client, error) {
74
+ if client == nil || signer == nil {
75
+ return nil, fmt.Errorf("onex v1 http client and signer are required")
76
+ }
77
+ baseURL, err := validateBaseURL(config.BaseURL)
78
+ if err != nil {
79
+ return nil, err
80
+ }
81
+ productCode := strings.TrimSpace(config.ProductCode)
82
+ if productCode == "" || productCode != config.ProductCode || !productCodePattern.MatchString(productCode) {
83
+ return nil, fmt.Errorf("onex v1 product code is invalid")
84
+ }
85
+ if config.Timeout < 0 {
86
+ return nil, fmt.Errorf("onex v1 timeout is invalid")
87
+ }
88
+ operationRetries := make(map[string]int, len(config.OperationRetries))
89
+ maximumRetries := config.SafeReadRetry.MaxRetries
90
+ for key, retries := range config.OperationRetries {
91
+ parts := strings.Split(key, ".")
92
+ if len(parts) != 2 {
93
+ return nil, fmt.Errorf("onex v1 operation retry key is invalid")
94
+ }
95
+ canonical, keyErr := dependencyOperationKey(parts[0], parts[1])
96
+ if keyErr != nil || canonical != key || retries < 0 || retries > maximumSafeReadRetries {
97
+ return nil, fmt.Errorf("onex v1 operation retry policy is invalid")
98
+ }
99
+ operationRetries[key] = retries
100
+ if retries > maximumRetries {
101
+ maximumRetries = retries
102
+ }
103
+ }
104
+ retryPolicy := config.SafeReadRetry
105
+ retryPolicy.MaxRetries = maximumRetries
106
+ if err := validateSafeReadRetryConfig(retryPolicy); err != nil {
107
+ return nil, err
108
+ }
109
+ if maximumRetries > 0 && config.Timeout <= 0 {
110
+ return nil, fmt.Errorf("onex v1 safe read retry requires a bounded timeout")
111
+ }
112
+ maxResponseBytes := config.MaxResponseBytes
113
+ if maxResponseBytes == 0 {
114
+ maxResponseBytes = defaultMaxResponseBytes
115
+ }
116
+ if maxResponseBytes < 1024 {
117
+ return nil, fmt.Errorf("onex v1 response limit is invalid")
118
+ }
119
+ derived := client.WithOptions(httpclient.Options{
120
+ Timeout: config.Timeout,
121
+ RedirectPolicy: httpclient.PreserveRedirectResponsePolicy(),
122
+ EnableLogging: false,
123
+ URLLogSanitizer: func(string) string { return "onex://internal/v1" },
124
+ })
125
+ derived.Resty().SetResponseBodyLimit(maxResponseBytes)
126
+ retryWaiter := config.RetryWaiter
127
+ if retryWaiter == nil {
128
+ retryWaiter = timerRetryWaiter{}
129
+ }
130
+ return &Client{
131
+ httpClient: derived,
132
+ signer: signer,
133
+ baseURL: baseURL,
134
+ productCode: productCode,
135
+ maxResponseBytes: maxResponseBytes,
136
+ defaultTimeout: config.Timeout,
137
+ safeReadRetry: config.SafeReadRetry,
138
+ operationRetries: operationRetries,
139
+ retryWaiter: retryWaiter,
140
+ dependency: config.Dependency,
141
+ }, nil
142
+ }
143
+
144
+ // Do 执行一次请求,并将成功响应严格解码到 out。
145
+ func (c *Client) Do(ctx context.Context, call Call, out any) error {
146
+ if c == nil {
147
+ return NewValidationError("onex_client_unavailable")
148
+ }
149
+ call.Query = cloneQueryValues(call.Query)
150
+ if err := validateCall(call, out); err != nil {
151
+ return err
152
+ }
153
+ requestCtx, cancel := operationContext(ctx, call.Timeout, c.defaultTimeout)
154
+ defer cancel()
155
+ if call.RequestID != "" && !validRequestID(call.RequestID) {
156
+ return NewValidationError("onex_request_id_invalid")
157
+ }
158
+ headers, err := c.signer.AuthHeaders(requestCtx)
159
+ if err != nil {
160
+ return mapAuthenticationError(requestCtx, err)
161
+ }
162
+ requestHeaders, err := canonicalServiceHeaders(headers)
163
+ if err != nil {
164
+ return err
165
+ }
166
+ requestHeaders["Content-Type"] = "application/json"
167
+ requestHeaders["Accept"] = "application/json"
168
+ requestHeaders[types.HeaderOneXProductCode] = c.productCode
169
+ if call.UserID > 0 {
170
+ requestHeaders[types.HeaderInternalUserID] = strconv.FormatInt(call.UserID, 10)
171
+ }
172
+ if call.RequestID != "" {
173
+ requestHeaders[types.HeaderRequestID] = call.RequestID
174
+ }
175
+ if call.IdempotencyKey != "" {
176
+ requestHeaders[types.HeaderIdempotencyKey] = call.IdempotencyKey
177
+ }
178
+ var body []byte
179
+ if call.Body != nil {
180
+ body, err = json.Marshal(call.Body)
181
+ if err != nil {
182
+ return &Error{Kind: ErrorKindValidation, Code: "onex_request_encoding_failed", cause: err}
183
+ }
184
+ }
185
+ requestURL, err := url.Parse(c.baseURL + call.Path)
186
+ if err != nil {
187
+ return &Error{Kind: ErrorKindContract, Code: "onex_request_url_invalid", cause: err}
188
+ }
189
+ requestURL.RawQuery = call.Query.Encode()
190
+ operation := func(executionCtx context.Context) error {
191
+ response, executionErr := c.execute(executionCtx, call, requestURL.String(), body, requestHeaders)
192
+ if executionErr != nil {
193
+ return mapTransportError(executionCtx, call.Method, executionErr)
194
+ }
195
+ if len(response.Body) > c.maxResponseBytes {
196
+ return &Error{Kind: ErrorKindContract, Code: "onex_response_too_large", HTTPStatus: response.StatusCode}
197
+ }
198
+ if !containsStatus(call.ExpectedStatuses, response.StatusCode) {
199
+ return decodeHTTPError(response)
200
+ }
201
+ if headerErr := requireResponseHeaders(response.Headers, call.RequiredResponseHeaders); headerErr != nil {
202
+ return &Error{Kind: ErrorKindContract, Code: "onex_response_headers_invalid", HTTPStatus: response.StatusCode, cause: headerErr}
203
+ }
204
+ if response.StatusCode == http.StatusNoContent {
205
+ if len(response.Body) != 0 {
206
+ return &Error{Kind: ErrorKindContract, Code: "onex_response_contract_invalid", HTTPStatus: response.StatusCode}
207
+ }
208
+ return nil
209
+ }
210
+ if contentTypeErr := requireJSONContentType(response.Headers); contentTypeErr != nil {
211
+ return &Error{Kind: ErrorKindContract, Code: "onex_response_content_type_invalid", HTTPStatus: response.StatusCode, cause: contentTypeErr}
212
+ }
213
+ if decodeErr := DecodeStrict(response.Body, out); decodeErr != nil {
214
+ return &Error{Kind: ErrorKindContract, Code: "onex_response_contract_invalid", HTTPStatus: response.StatusCode, cause: decodeErr}
215
+ }
216
+ return nil
217
+ }
218
+ if c.dependency == nil {
219
+ return operation(requestCtx)
220
+ }
221
+ key, err := dependencyOperationKey(call.Dependency, call.Operation)
222
+ if err != nil {
223
+ return NewContractError("onex_dependency_operation_invalid", err)
224
+ }
225
+ attempted, err := c.dependency.Execute(requestCtx, key, operation)
226
+ if err == nil {
227
+ return nil
228
+ }
229
+ if attempted {
230
+ return err
231
+ }
232
+ return &Error{Kind: ErrorKindUnavailable, Code: "onex_dependency_unavailable", Retryable: true, cause: err}
233
+ }
234
+
235
+ func (c *Client) execute(ctx context.Context, call Call, requestURL string, body []byte, headers map[string]string) (*httpclient.Response, error) {
236
+ retryLimit := c.safeReadRetry.MaxRetries
237
+ if key, err := dependencyOperationKey(call.Dependency, call.Operation); err == nil {
238
+ if configured, exists := c.operationRetries[key]; exists {
239
+ retryLimit = configured
240
+ }
241
+ }
242
+ for retryNumber := 0; ; retryNumber++ {
243
+ response, err := c.httpClient.DoOnceWithHeaders(ctx, call.Method, requestURL, body, nil, headers)
244
+ if call.Method != http.MethodGet || retryNumber >= retryLimit || !retryableSafeReadResult(ctx, response, err) {
245
+ return response, err
246
+ }
247
+ retryAfter := ""
248
+ if response != nil {
249
+ retryAfter = response.Headers.Get(types.HeaderRetryAfter)
250
+ }
251
+ delay := safeReadRetryDelay(c.safeReadRetry, retryNumber+1, retryAfter, time.Now().UTC())
252
+ if deadline, ok := ctx.Deadline(); ok && delay >= time.Until(deadline) {
253
+ return response, err
254
+ }
255
+ if waitErr := c.retryWaiter.Wait(ctx, delay); waitErr != nil {
256
+ return nil, waitErr
257
+ }
258
+ }
259
+ }
260
+
261
+ func retryableSafeReadResult(ctx context.Context, response *httpclient.Response, err error) bool {
262
+ if ctx.Err() != nil {
263
+ return false
264
+ }
265
+ if err != nil {
266
+ return true
267
+ }
268
+ return response != nil && shouldRetrySafeReadStatus(response.StatusCode)
269
+ }
270
+
271
+ func operationContext(ctx context.Context, operationTimeout, defaultTimeout time.Duration) (context.Context, context.CancelFunc) {
272
+ timeout := operationTimeout
273
+ if timeout == 0 {
274
+ timeout = defaultTimeout
275
+ }
276
+ if timeout <= 0 {
277
+ return context.WithCancel(ctx)
278
+ }
279
+ return context.WithTimeout(ctx, timeout)
280
+ }
281
+
282
+ func canonicalServiceHeaders(headers map[string]string) (map[string]string, error) {
283
+ if len(headers) != 1 {
284
+ return nil, NewContractError("onex_authentication_contract_invalid", nil)
285
+ }
286
+ authorization := ""
287
+ for name, value := range headers {
288
+ if !strings.EqualFold(name, "Authorization") {
289
+ return nil, NewContractError("onex_authentication_contract_invalid", nil)
290
+ }
291
+ authorization = value
292
+ }
293
+ const prefix = "Bearer "
294
+ if !strings.HasPrefix(authorization, prefix) || !validHeaderCredential(strings.TrimPrefix(authorization, prefix)) {
295
+ return nil, NewContractError("onex_authentication_contract_invalid", nil)
296
+ }
297
+ return map[string]string{"Authorization": authorization}, nil
298
+ }
299
+
300
+ func validHeaderCredential(value string) bool {
301
+ if value == "" || len(value) > 8192 {
302
+ return false
303
+ }
304
+ for index := range len(value) {
305
+ if value[index] < 0x21 || value[index] > 0x7e {
306
+ return false
307
+ }
308
+ }
309
+ return true
310
+ }
311
+
312
+ func validRequestID(value string) bool {
313
+ if value == "" || len(value) > maximumRequestIDBytes || value != strings.TrimSpace(value) {
314
+ return false
315
+ }
316
+ for index := range len(value) {
317
+ if value[index] < 0x21 || value[index] > 0x7e {
318
+ return false
319
+ }
320
+ }
321
+ return true
322
+ }
323
+
324
+ func mapAuthenticationError(ctx context.Context, err error) error {
325
+ if errors.Is(ctx.Err(), context.Canceled) || errors.Is(err, context.Canceled) {
326
+ return &Error{Kind: ErrorKindCanceled, Code: "onex_request_canceled", cause: err}
327
+ }
328
+ return &Error{Kind: ErrorKindUnavailable, Code: "onex_authentication_unavailable", Retryable: true, cause: err}
329
+ }
330
+
331
+ func mapTransportError(ctx context.Context, method string, err error) error {
332
+ if errors.Is(ctx.Err(), context.Canceled) || errors.Is(err, context.Canceled) {
333
+ return &Error{Kind: ErrorKindCanceled, Code: "onex_request_canceled", cause: err}
334
+ }
335
+ if method != http.MethodGet {
336
+ return &Error{Kind: ErrorKindResultUnknown, Code: "onex_result_unknown", Retryable: true, cause: err}
337
+ }
338
+ return &Error{Kind: ErrorKindUnavailable, Code: "onex_request_unavailable", Retryable: true, cause: err}
339
+ }
340
+
341
+ func validateBaseURL(raw string) (string, error) {
342
+ value := strings.TrimRight(strings.TrimSpace(raw), "/")
343
+ parsed, err := url.Parse(value)
344
+ if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
345
+ return "", fmt.Errorf("onex v1 base URL is invalid")
346
+ }
347
+ if parsed.EscapedPath() != "/internal/v1" {
348
+ return "", fmt.Errorf("onex v1 base URL must end at /internal/v1")
349
+ }
350
+ return value, nil
351
+ }
352
+
353
+ func validateCall(call Call, out any) error {
354
+ if call.Method != http.MethodGet && call.Method != http.MethodPost && call.Method != http.MethodPut {
355
+ return NewValidationError("onex_method_invalid")
356
+ }
357
+ if !strings.HasPrefix(call.Path, "/") || strings.ContainsAny(call.Path, "?#") || strings.Contains(call.Path, "//") {
358
+ return NewValidationError("onex_path_invalid")
359
+ }
360
+ if err := validateQueryValues(call.Query); err != nil {
361
+ return NewValidationError("onex_query_invalid")
362
+ }
363
+ if call.UserID < 0 || strings.TrimSpace(call.RequestID) != call.RequestID || strings.TrimSpace(call.IdempotencyKey) != call.IdempotencyKey {
364
+ return NewValidationError("onex_trusted_context_invalid")
365
+ }
366
+ noContent := len(call.ExpectedStatuses) == 1 && call.ExpectedStatuses[0] == http.StatusNoContent
367
+ if len(call.ExpectedStatuses) == 0 || noContent && out != nil || !noContent && (out == nil || containsStatus(call.ExpectedStatuses, http.StatusNoContent)) {
368
+ return NewValidationError("onex_response_contract_required")
369
+ }
370
+ if call.Timeout < 0 || (call.Method == http.MethodGet && (call.Body != nil || call.IdempotencyKey != "")) {
371
+ return NewValidationError("onex_operation_policy_invalid")
372
+ }
373
+ if (call.Dependency == "") != (call.Operation == "") {
374
+ return NewValidationError("onex_dependency_operation_invalid")
375
+ }
376
+ if call.Dependency != "" {
377
+ if _, err := dependencyOperationKey(call.Dependency, call.Operation); err != nil {
378
+ return NewValidationError("onex_dependency_operation_invalid")
379
+ }
380
+ }
381
+ return nil
382
+ }
383
+
384
+ func cloneQueryValues(values url.Values) url.Values {
385
+ if len(values) == 0 {
386
+ return nil
387
+ }
388
+ cloned := make(url.Values, len(values))
389
+ for key, items := range values {
390
+ cloned[key] = append([]string(nil), items...)
391
+ }
392
+ return cloned
393
+ }
394
+
395
+ func validateQueryValues(values url.Values) error {
396
+ if len(values) > 16 {
397
+ return fmt.Errorf("query contains too many keys")
398
+ }
399
+ totalValues := 0
400
+ for key, items := range values {
401
+ if key == "" || len(key) > 128 || key != strings.TrimSpace(key) || !validQueryText(key) {
402
+ return fmt.Errorf("query key is invalid")
403
+ }
404
+ totalValues += len(items)
405
+ if len(items) == 0 || totalValues > 64 {
406
+ return fmt.Errorf("query values are invalid")
407
+ }
408
+ for _, value := range items {
409
+ if len(value) > 1024 || !validQueryText(value) {
410
+ return fmt.Errorf("query value is invalid")
411
+ }
412
+ }
413
+ }
414
+ if len(values.Encode()) > 8192 {
415
+ return fmt.Errorf("encoded query is too large")
416
+ }
417
+ return nil
418
+ }
419
+
420
+ func validQueryText(value string) bool {
421
+ for _, character := range value {
422
+ if character < 0x20 || character == 0x7f {
423
+ return false
424
+ }
425
+ }
426
+ return true
427
+ }
428
+
429
+ func containsStatus(statuses []int, status int) bool {
430
+ for _, candidate := range statuses {
431
+ if candidate == status {
432
+ return true
433
+ }
434
+ }
435
+ return false
436
+ }
437
+
438
+ func requireJSONContentType(headers http.Header) error {
439
+ mediaType, _, err := mime.ParseMediaType(headers.Get("Content-Type"))
440
+ if err != nil || mediaType != "application/json" {
441
+ return fmt.Errorf("response media type is not application/json")
442
+ }
443
+ return nil
444
+ }
445
+
446
+ func requireResponseHeaders(headers http.Header, required map[string]string) error {
447
+ for name, expected := range required {
448
+ if strings.TrimSpace(name) == "" || expected == "" || headers.Get(name) != expected {
449
+ return fmt.Errorf("required response header is missing or invalid")
450
+ }
451
+ }
452
+ return nil
453
+ }