@lovrozagar/honey 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/docs/api-ref/go.md +399 -0
- package/docs/api-ref/python.md +399 -0
- package/docs/api-ref/rust.md +399 -0
- package/docs/api-ref/typescript.md +399 -0
- package/docs/invalidate-spec.md +303 -0
- package/docs/sdk.md +722 -0
- package/package.json +120 -0
- package/src/accepts.ts +78 -0
- package/src/body-limit.ts +100 -0
- package/src/build/index.ts +174 -0
- package/src/cli-go/auth.go +15 -0
- package/src/cli-go/config.go +165 -0
- package/src/cli-go/errors.go +57 -0
- package/src/cli-go/multipart.go +55 -0
- package/src/cli-go/output.go +109 -0
- package/src/cli-go/root.go +30 -0
- package/src/cli-go/stream.go +88 -0
- package/src/cli-go/version.go +5 -0
- package/src/cli.ts +204 -0
- package/src/client/error.ts +27 -0
- package/src/client/http.ts +502 -0
- package/src/client/index.ts +127 -0
- package/src/client/sdk.ts +265 -0
- package/src/client/sse.ts +102 -0
- package/src/client/types.ts +211 -0
- package/src/client/ws.ts +115 -0
- package/src/client-go/errors.go +207 -0
- package/src/client-go/invalidation.go +373 -0
- package/src/client-go/realtime.go +317 -0
- package/src/client-go/result.go +20 -0
- package/src/client-go/runtime.go +413 -0
- package/src/client-go/sse.go +149 -0
- package/src/client-go/transport.go +452 -0
- package/src/client-go/ws.go +104 -0
- package/src/client-mcp/README.md +10 -0
- package/src/client-mcp/server.ts +94 -0
- package/src/client-mcp/types.ts +19 -0
- package/src/client-python/_errors.py +115 -0
- package/src/client-python/_invalidation.py +578 -0
- package/src/client-python/_realtime.py +156 -0
- package/src/client-python/_runtime.py +469 -0
- package/src/client-python/_sse.py +99 -0
- package/src/client-python/_transport.py +335 -0
- package/src/client-python/_ws.py +131 -0
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +200 -0
- package/src/client-rust/invalidation.rs +441 -0
- package/src/client-rust/invalidation_sync.rs +306 -0
- package/src/client-rust/realtime.rs +695 -0
- package/src/client-rust/result.rs +22 -0
- package/src/client-rust/runtime.rs +547 -0
- package/src/client-rust/runtime_sync.rs +452 -0
- package/src/client-rust/sse.rs +167 -0
- package/src/client-rust/ws.rs +143 -0
- package/src/cloudflare/realtime.ts +236 -0
- package/src/codegen-go-cli.ts +1180 -0
- package/src/codegen-go.ts +1010 -0
- package/src/codegen-hash.ts +8 -0
- package/src/codegen-ir.ts +517 -0
- package/src/codegen-loaders.ts +53 -0
- package/src/codegen-mcp.ts +364 -0
- package/src/codegen-python.ts +1028 -0
- package/src/codegen-rust.ts +1652 -0
- package/src/codegen-sanitize.ts +67 -0
- package/src/codegen-schema-naming.ts +146 -0
- package/src/codegen.ts +5015 -0
- package/src/context.ts +210 -0
- package/src/cookie-sign.ts +58 -0
- package/src/cookie.ts +89 -0
- package/src/cors.ts +96 -0
- package/src/crypto.ts +19 -0
- package/src/csrf.ts +54 -0
- package/src/curl-logger.ts +235 -0
- package/src/detect-runtime.ts +19 -0
- package/src/error.ts +52 -0
- package/src/errors.ts +87 -0
- package/src/etag.ts +74 -0
- package/src/feature-load.ts +23 -0
- package/src/go-type-emitter.ts +404 -0
- package/src/honey-response.ts +354 -0
- package/src/i18n-slot.ts +20 -0
- package/src/i18n.ts +234 -0
- package/src/index.ts +3843 -0
- package/src/init.ts +149 -0
- package/src/input.ts +8 -0
- package/src/ip-restrict.ts +192 -0
- package/src/logger.ts +149 -0
- package/src/middleware.ts +199 -0
- package/src/node-request.ts +240 -0
- package/src/node.ts +212 -0
- package/src/openapi/register.ts +19 -0
- package/src/openapi/scalar.ts +33 -0
- package/src/openapi/spec-factory.ts +46 -0
- package/src/openapi/spec.ts +31 -0
- package/src/openapi/swagger.ts +34 -0
- package/src/plugin.ts +771 -0
- package/src/powered-by.ts +17 -0
- package/src/pretty-json.ts +39 -0
- package/src/proxy.ts +155 -0
- package/src/python-type-emitter.ts +144 -0
- package/src/realtime/buffer.ts +101 -0
- package/src/realtime/bus.ts +93 -0
- package/src/realtime/longpoll.ts +148 -0
- package/src/realtime/protocol.ts +128 -0
- package/src/realtime/route.ts +104 -0
- package/src/realtime/runtime.ts +430 -0
- package/src/request-id.ts +23 -0
- package/src/request-to-curl.ts +34 -0
- package/src/response.ts +414 -0
- package/src/rust-type-emitter.ts +412 -0
- package/src/secure-headers.ts +69 -0
- package/src/serve-register.ts +8 -0
- package/src/serve-slot.ts +20 -0
- package/src/serve.ts +141 -0
- package/src/server-timing.ts +66 -0
- package/src/static.ts +60 -0
- package/src/telemetry/otel.ts +139 -0
- package/src/testing.ts +114 -0
- package/src/timeout.ts +36 -0
- package/src/tree.ts +464 -0
- package/src/ts-type-emitter.ts +106 -0
- package/src/type-emitter.ts +524 -0
- package/src/type-extractor.ts +749 -0
- package/src/types.ts +638 -0
- package/src/validation.ts +300 -0
- package/src/ws/bun.ts +89 -0
- package/src/ws/cloudflare.ts +133 -0
- package/src/ws/deno.ts +109 -0
- package/src/ws/node.ts +161 -0
- package/src/yaml.ts +80 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
// Code generated by honey. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
package sdk
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"bytes"
|
|
7
|
+
"context"
|
|
8
|
+
"crypto/rand"
|
|
9
|
+
"encoding/json"
|
|
10
|
+
"errors"
|
|
11
|
+
"fmt"
|
|
12
|
+
"io"
|
|
13
|
+
"net/http"
|
|
14
|
+
"net/url"
|
|
15
|
+
"time"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
// RequestContext carries mutable request metadata through OnRequest hook
|
|
19
|
+
// invocations. Hooks may mutate Method, URL, Headers, or Body; mutations are
|
|
20
|
+
// applied to the outgoing request after the full hook chain completes.
|
|
21
|
+
// Selector / IsStale / InvalidatedBy mirror the TS hook ctx and are populated
|
|
22
|
+
// only when the SDK has an active StaleTracker.
|
|
23
|
+
type RequestContext struct {
|
|
24
|
+
Method string
|
|
25
|
+
URL string
|
|
26
|
+
Path string
|
|
27
|
+
Headers http.Header
|
|
28
|
+
Body []byte
|
|
29
|
+
IsRetry bool
|
|
30
|
+
Selector string
|
|
31
|
+
IsStale bool
|
|
32
|
+
InvalidatedBy []string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ResponseContext carries response metadata through OnResponse hook invocations.
|
|
36
|
+
// Hooks may inspect or mutate Body bytes prior to JSON unmarshal in the caller.
|
|
37
|
+
// Status is observational and is not mutated by hooks.
|
|
38
|
+
// Selector / IsStale / InvalidatedBy mirror the TS hook ctx and are populated
|
|
39
|
+
// only when the SDK has an active StaleTracker.
|
|
40
|
+
type ResponseContext struct {
|
|
41
|
+
Status int
|
|
42
|
+
Headers http.Header
|
|
43
|
+
Body []byte
|
|
44
|
+
IsRetry bool
|
|
45
|
+
Selector string
|
|
46
|
+
IsStale bool
|
|
47
|
+
InvalidatedBy []string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Config holds all configuration for the SDK client.
|
|
51
|
+
type Config struct {
|
|
52
|
+
BaseURL string
|
|
53
|
+
BearerToken string
|
|
54
|
+
Headers map[string]string
|
|
55
|
+
Timeout time.Duration
|
|
56
|
+
ThrowOnError bool
|
|
57
|
+
StaleMaxEntries int
|
|
58
|
+
Invalidation *InvalidationConfig
|
|
59
|
+
OnAuthExpired func(context.Context) (string, error)
|
|
60
|
+
HTTPClient *http.Client
|
|
61
|
+
AuthHeaderName string
|
|
62
|
+
AuthHeaderPrefix string
|
|
63
|
+
OnRequest []func(*RequestContext) error
|
|
64
|
+
OnResponse []func(*ResponseContext) error
|
|
65
|
+
OnLog func(LogEntry)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// LogEntry is the payload passed to Config.OnLog. Consumers plug in their own
|
|
69
|
+
// logger; the SDK never formats, levels, or persists. Status is a pointer so
|
|
70
|
+
// nil encodes "omitted" (request_start) without colliding with a real 0 status.
|
|
71
|
+
// Err is the underlying error on the "error" event; nil otherwise. JSON tags
|
|
72
|
+
// match the TS/Python LogEntry shape for cross-language consumer parity.
|
|
73
|
+
type LogEntry struct {
|
|
74
|
+
Level string `json:"level"`
|
|
75
|
+
Event string `json:"event"`
|
|
76
|
+
Operation string `json:"operation"`
|
|
77
|
+
DurationMs int64 `json:"duration_ms"`
|
|
78
|
+
Status *int `json:"status,omitempty"`
|
|
79
|
+
Err error `json:"-"`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// emitLog invokes the OnLog callback if configured, swallowing any panic so a
|
|
83
|
+
// faulty logger cannot break the request path. Mirrors Python _emit_log /
|
|
84
|
+
// TS optional-chained call.
|
|
85
|
+
func emitLog(cfg Config, entry LogEntry) {
|
|
86
|
+
if cfg.OnLog == nil {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
defer func() {
|
|
90
|
+
_ = recover()
|
|
91
|
+
}()
|
|
92
|
+
cfg.OnLog(entry)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func errStatus(err error) *int {
|
|
96
|
+
var apiErr APIError
|
|
97
|
+
if errors.As(err, &apiErr) {
|
|
98
|
+
s := apiErr.Status()
|
|
99
|
+
return &s
|
|
100
|
+
}
|
|
101
|
+
return nil
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// InvalidationConfig controls cache invalidation behaviour. StaleTime is in
|
|
105
|
+
// milliseconds; ≤0 disables the tracker. StaleMaxEntries caps the LRU map.
|
|
106
|
+
// MaxSourcesPerTarget caps the `by` list per stale entry.
|
|
107
|
+
type InvalidationConfig struct {
|
|
108
|
+
StaleTime int64
|
|
109
|
+
StaleMaxEntries int
|
|
110
|
+
MaxSourcesPerTarget int
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type requestResult struct {
|
|
114
|
+
body []byte
|
|
115
|
+
status int
|
|
116
|
+
resp *http.Response
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// doRequest executes an HTTP request, injects auth, merges headers, fires
|
|
120
|
+
// OnRequest/OnResponse hooks in declaration order, handles OnAuthExpired retry
|
|
121
|
+
// on 401, and wraps context cancellation errors. Emits OnLog lifecycle entries
|
|
122
|
+
// (request_start once at entry, response_received once for the final response,
|
|
123
|
+
// error on terminal failure) — matching TS/Python behavior where retry rounds
|
|
124
|
+
// do not double-emit.
|
|
125
|
+
func doRequest(
|
|
126
|
+
ctx context.Context,
|
|
127
|
+
cfg Config,
|
|
128
|
+
method string,
|
|
129
|
+
rawURL string,
|
|
130
|
+
queryParams url.Values,
|
|
131
|
+
bodyData interface{},
|
|
132
|
+
bodyReader io.Reader,
|
|
133
|
+
contentType string,
|
|
134
|
+
callHeaders map[string]string,
|
|
135
|
+
operation string,
|
|
136
|
+
requestMeta *RequestMeta,
|
|
137
|
+
) (result *requestResult, err error) {
|
|
138
|
+
start := time.Now()
|
|
139
|
+
defer func() {
|
|
140
|
+
if err != nil {
|
|
141
|
+
emitLog(cfg, LogEntry{
|
|
142
|
+
Level: "error",
|
|
143
|
+
Event: "error",
|
|
144
|
+
Operation: operation,
|
|
145
|
+
DurationMs: time.Since(start).Milliseconds(),
|
|
146
|
+
Status: errStatus(err),
|
|
147
|
+
Err: err,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}()
|
|
151
|
+
|
|
152
|
+
if ctx.Err() != nil {
|
|
153
|
+
return nil, fmt.Errorf("honey: request canceled: %w", ctx.Err())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
emitLog(cfg, LogEntry{
|
|
157
|
+
Level: "debug",
|
|
158
|
+
Event: "request_start",
|
|
159
|
+
Operation: operation,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
result, err = executeRequest(ctx, cfg, method, rawURL, queryParams, bodyData, bodyReader, contentType, callHeaders, cfg.BearerToken, false, requestMeta)
|
|
163
|
+
if err != nil {
|
|
164
|
+
return nil, err
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if result.status == 401 && cfg.OnAuthExpired != nil {
|
|
168
|
+
newToken, authErr := cfg.OnAuthExpired(ctx)
|
|
169
|
+
if authErr != nil {
|
|
170
|
+
err = fmt.Errorf("honey: OnAuthExpired failed: %w", authErr)
|
|
171
|
+
return nil, err
|
|
172
|
+
}
|
|
173
|
+
/* streaming bodies are single-shot: once consumed by the first request,
|
|
174
|
+
* retry has no way to replay. Documented as v1 non-goal — consumers
|
|
175
|
+
* needing auto-retry on streamed uploads wrap in an outer retry loop. */
|
|
176
|
+
result, err = executeRequest(ctx, cfg, method, rawURL, queryParams, bodyData, bodyReader, contentType, callHeaders, newToken, true, requestMeta)
|
|
177
|
+
if err != nil {
|
|
178
|
+
return nil, err
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
status := result.status
|
|
183
|
+
level := "info"
|
|
184
|
+
if status >= 400 {
|
|
185
|
+
level = "warn"
|
|
186
|
+
}
|
|
187
|
+
emitLog(cfg, LogEntry{
|
|
188
|
+
Level: level,
|
|
189
|
+
Event: "response_received",
|
|
190
|
+
Operation: operation,
|
|
191
|
+
DurationMs: time.Since(start).Milliseconds(),
|
|
192
|
+
Status: &status,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
if err2 := raiseForStatus(result.resp, result.body); err2 != nil {
|
|
196
|
+
err = err2
|
|
197
|
+
return nil, err
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result, nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func executeRequest(
|
|
204
|
+
ctx context.Context,
|
|
205
|
+
cfg Config,
|
|
206
|
+
method string,
|
|
207
|
+
rawURL string,
|
|
208
|
+
queryParams url.Values,
|
|
209
|
+
bodyData interface{},
|
|
210
|
+
bodyReader io.Reader,
|
|
211
|
+
contentType string,
|
|
212
|
+
callHeaders map[string]string,
|
|
213
|
+
bearerToken string,
|
|
214
|
+
isRetry bool,
|
|
215
|
+
requestMeta *RequestMeta,
|
|
216
|
+
) (*requestResult, error) {
|
|
217
|
+
fullURL, err := buildURL(cfg.BaseURL, rawURL, queryParams)
|
|
218
|
+
if err != nil {
|
|
219
|
+
return nil, fmt.Errorf("honey: build URL: %w", err)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* bodyReader wins over bodyData — streaming uploads bypass JSON marshal.
|
|
223
|
+
* RequestContext.Body stays nil for streams so hook consumers don't try to
|
|
224
|
+
* read or re-hash a single-shot reader. */
|
|
225
|
+
var bodyBytes []byte
|
|
226
|
+
if bodyReader == nil && bodyData != nil {
|
|
227
|
+
encoded, encErr := json.Marshal(bodyData)
|
|
228
|
+
if encErr != nil {
|
|
229
|
+
return nil, fmt.Errorf("honey: marshal body: %w", encErr)
|
|
230
|
+
}
|
|
231
|
+
bodyBytes = encoded
|
|
232
|
+
}
|
|
233
|
+
hasBody := bodyReader != nil || bodyData != nil
|
|
234
|
+
|
|
235
|
+
headers := mergeHeaders(cfg, callHeaders, bearerToken, hasBody, contentType)
|
|
236
|
+
|
|
237
|
+
reqCtx := &RequestContext{
|
|
238
|
+
Method: method,
|
|
239
|
+
URL: fullURL,
|
|
240
|
+
Path: rawURL,
|
|
241
|
+
Headers: headers,
|
|
242
|
+
Body: bodyBytes,
|
|
243
|
+
IsRetry: isRetry,
|
|
244
|
+
}
|
|
245
|
+
if requestMeta != nil {
|
|
246
|
+
reqCtx.Selector = requestMeta.Selector
|
|
247
|
+
reqCtx.IsStale = requestMeta.IsStale
|
|
248
|
+
reqCtx.InvalidatedBy = requestMeta.InvalidatedBy
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for _, hook := range cfg.OnRequest {
|
|
252
|
+
if hookErr := hook(reqCtx); hookErr != nil {
|
|
253
|
+
return nil, fmt.Errorf("honey: OnRequest hook: %w", hookErr)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
var reader io.Reader
|
|
258
|
+
if bodyReader != nil {
|
|
259
|
+
reader = bodyReader
|
|
260
|
+
} else if reqCtx.Body != nil {
|
|
261
|
+
reader = bytes.NewReader(reqCtx.Body)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
req, err := http.NewRequestWithContext(ctx, reqCtx.Method, reqCtx.URL, reader)
|
|
265
|
+
if err != nil {
|
|
266
|
+
return nil, fmt.Errorf("honey: build request: %w", err)
|
|
267
|
+
}
|
|
268
|
+
req.Header = reqCtx.Headers
|
|
269
|
+
|
|
270
|
+
resp, err := cfg.HTTPClient.Do(req)
|
|
271
|
+
if err != nil {
|
|
272
|
+
if ctx.Err() != nil {
|
|
273
|
+
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
|
274
|
+
return nil, fmt.Errorf("honey: request timed out: %w", ctx.Err())
|
|
275
|
+
}
|
|
276
|
+
if errors.Is(ctx.Err(), context.Canceled) {
|
|
277
|
+
return nil, fmt.Errorf("honey: request canceled: %w", ctx.Err())
|
|
278
|
+
}
|
|
279
|
+
return nil, fmt.Errorf("honey: context error: %w", ctx.Err())
|
|
280
|
+
}
|
|
281
|
+
return nil, fmt.Errorf("honey: http do: %w", err)
|
|
282
|
+
}
|
|
283
|
+
defer resp.Body.Close()
|
|
284
|
+
|
|
285
|
+
body, readErr := io.ReadAll(resp.Body)
|
|
286
|
+
if readErr != nil {
|
|
287
|
+
return nil, fmt.Errorf("honey: read body: %w", readErr)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
respCtx := &ResponseContext{
|
|
291
|
+
Status: resp.StatusCode,
|
|
292
|
+
Headers: resp.Header,
|
|
293
|
+
Body: body,
|
|
294
|
+
IsRetry: isRetry,
|
|
295
|
+
}
|
|
296
|
+
if requestMeta != nil {
|
|
297
|
+
respCtx.Selector = requestMeta.Selector
|
|
298
|
+
respCtx.IsStale = requestMeta.IsStale
|
|
299
|
+
respCtx.InvalidatedBy = requestMeta.InvalidatedBy
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for _, hook := range cfg.OnResponse {
|
|
303
|
+
if hookErr := hook(respCtx); hookErr != nil {
|
|
304
|
+
return nil, fmt.Errorf("honey: OnResponse hook: %w", hookErr)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return &requestResult{body: respCtx.Body, status: resp.StatusCode, resp: resp}, nil
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func buildURL(baseURL, path string, queryParams url.Values) (string, error) {
|
|
312
|
+
base, err := url.Parse(baseURL)
|
|
313
|
+
if err != nil {
|
|
314
|
+
return "", err
|
|
315
|
+
}
|
|
316
|
+
ref, err := url.Parse(path)
|
|
317
|
+
if err != nil {
|
|
318
|
+
return "", err
|
|
319
|
+
}
|
|
320
|
+
resolved := base.ResolveReference(ref)
|
|
321
|
+
if len(queryParams) > 0 {
|
|
322
|
+
existing := resolved.Query()
|
|
323
|
+
for k, vals := range queryParams {
|
|
324
|
+
for _, v := range vals {
|
|
325
|
+
existing.Add(k, v)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
resolved.RawQuery = existing.Encode()
|
|
329
|
+
}
|
|
330
|
+
return resolved.String(), nil
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// mergeHeaders builds the final http.Header for a request by merging in order:
|
|
334
|
+
// config Headers → call Headers (per-call wins per key) → Content-Type when
|
|
335
|
+
// hasBody → Authorization when bearerToken non-empty. Returned header is owned
|
|
336
|
+
// by the caller and may be mutated freely (e.g. by OnRequest hooks).
|
|
337
|
+
// contentType is the explicit Content-Type for the request body; when empty
|
|
338
|
+
// and hasBody is true, defaults to application/json. Callers pass
|
|
339
|
+
// application/octet-stream for streaming-upload ops.
|
|
340
|
+
func mergeHeaders(
|
|
341
|
+
cfg Config,
|
|
342
|
+
callHeaders map[string]string,
|
|
343
|
+
bearerToken string,
|
|
344
|
+
hasBody bool,
|
|
345
|
+
contentType string,
|
|
346
|
+
) http.Header {
|
|
347
|
+
h := http.Header{}
|
|
348
|
+
if hasBody {
|
|
349
|
+
ct := contentType
|
|
350
|
+
if ct == "" {
|
|
351
|
+
ct = "application/json"
|
|
352
|
+
}
|
|
353
|
+
h.Set("Content-Type", ct)
|
|
354
|
+
}
|
|
355
|
+
for k, v := range cfg.Headers {
|
|
356
|
+
h.Set(k, v)
|
|
357
|
+
}
|
|
358
|
+
for k, v := range callHeaders {
|
|
359
|
+
h.Set(k, v)
|
|
360
|
+
}
|
|
361
|
+
if bearerToken != "" {
|
|
362
|
+
name := cfg.AuthHeaderName
|
|
363
|
+
if name == "" {
|
|
364
|
+
name = "Authorization"
|
|
365
|
+
}
|
|
366
|
+
prefix := cfg.AuthHeaderPrefix
|
|
367
|
+
if prefix == "" {
|
|
368
|
+
prefix = "Bearer "
|
|
369
|
+
}
|
|
370
|
+
h.Set(name, prefix+bearerToken)
|
|
371
|
+
}
|
|
372
|
+
return h
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func unmarshalBody[T any](body []byte) (*T, error) {
|
|
376
|
+
if len(body) == 0 {
|
|
377
|
+
return nil, nil
|
|
378
|
+
}
|
|
379
|
+
var out T
|
|
380
|
+
if err := json.Unmarshal(body, &out); err != nil {
|
|
381
|
+
return nil, fmt.Errorf("honey: unmarshal response: %w", err)
|
|
382
|
+
}
|
|
383
|
+
return &out, nil
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// newUUIDv4 returns a freshly generated RFC 4122 §4.4 random UUID string.
|
|
387
|
+
// Uses crypto/rand for entropy; sets version (4) and variant (10xx) bits per
|
|
388
|
+
// the spec, then formats canonical 8-4-4-4-12 lower-case hex. Zero external
|
|
389
|
+
// deps so generated SDKs add no third-party rows to consumer go.sum files.
|
|
390
|
+
func newUUIDv4() string {
|
|
391
|
+
var b [16]byte
|
|
392
|
+
if _, err := rand.Read(b[:]); err != nil {
|
|
393
|
+
/* crypto/rand.Read is documented to never return non-nil err on supported
|
|
394
|
+
* platforms; if it does the OS entropy source is broken — silent fallback
|
|
395
|
+
* would corrupt dedupe semantics, so panic mirrors google/uuid. */
|
|
396
|
+
panic("honey: crypto/rand failed: " + err.Error())
|
|
397
|
+
}
|
|
398
|
+
b[6] = (b[6] & 0x0f) | 0x40 /* version 4 */
|
|
399
|
+
b[8] = (b[8] & 0x3f) | 0x80 /* variant 10xx */
|
|
400
|
+
const hex = "0123456789abcdef"
|
|
401
|
+
out := make([]byte, 36)
|
|
402
|
+
j := 0
|
|
403
|
+
for i, by := range b {
|
|
404
|
+
if i == 4 || i == 6 || i == 8 || i == 10 {
|
|
405
|
+
out[j] = '-'
|
|
406
|
+
j++
|
|
407
|
+
}
|
|
408
|
+
out[j] = hex[by>>4]
|
|
409
|
+
out[j+1] = hex[by&0x0f]
|
|
410
|
+
j += 2
|
|
411
|
+
}
|
|
412
|
+
return string(out)
|
|
413
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Code generated by honey. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
package sdk
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"bufio"
|
|
7
|
+
"context"
|
|
8
|
+
"io"
|
|
9
|
+
"iter"
|
|
10
|
+
"net/http"
|
|
11
|
+
"strconv"
|
|
12
|
+
"strings"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// SSEEvent represents a single server-sent event.
|
|
16
|
+
type SSEEvent struct {
|
|
17
|
+
Data string
|
|
18
|
+
Event string
|
|
19
|
+
ID string
|
|
20
|
+
Retry int
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sseMaxBuffer = 1024 * 1024
|
|
24
|
+
|
|
25
|
+
// parseSSEStream returns an iter.Seq2 that yields SSEEvent values read from resp.Body.
|
|
26
|
+
// On non-2xx response it yields a typed error immediately then terminates.
|
|
27
|
+
// The response body is closed when the iterator returns.
|
|
28
|
+
//
|
|
29
|
+
// Ported from public/honey/core/src/client/sse.ts:62-102 — bufio.Scanner with
|
|
30
|
+
// double-newline split mirrors the TextDecoder+split(/\r\n\r\n|…/) approach.
|
|
31
|
+
func parseSSEStream(ctx context.Context, resp *http.Response) iter.Seq2[SSEEvent, error] {
|
|
32
|
+
return func(yield func(SSEEvent, error) bool) {
|
|
33
|
+
defer resp.Body.Close()
|
|
34
|
+
|
|
35
|
+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
36
|
+
body, _ := io.ReadAll(resp.Body)
|
|
37
|
+
apiErr := raiseForStatus(resp, body)
|
|
38
|
+
yield(SSEEvent{}, apiErr)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
scanner := bufio.NewScanner(resp.Body)
|
|
43
|
+
scanner.Buffer(make([]byte, 0, 4096), sseMaxBuffer)
|
|
44
|
+
scanner.Split(splitDoubleNewline)
|
|
45
|
+
|
|
46
|
+
for scanner.Scan() {
|
|
47
|
+
if ctx.Err() != nil {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
block := scanner.Text()
|
|
51
|
+
if strings.TrimSpace(block) == "" {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
ev, ok := parseSSEBlock(block)
|
|
55
|
+
if !ok {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
if !yield(ev, nil) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if err := scanner.Err(); err != nil {
|
|
64
|
+
yield(SSEEvent{}, err)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// splitDoubleNewline is a bufio.SplitFunc that splits on \r\n\r\n, \n\n, or \r\r.
|
|
70
|
+
func splitDoubleNewline(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
71
|
+
delimiters := []string{"\r\n\r\n", "\n\n", "\r\r"}
|
|
72
|
+
for _, d := range delimiters {
|
|
73
|
+
db := []byte(d)
|
|
74
|
+
for i := 0; i <= len(data)-len(db); i++ {
|
|
75
|
+
match := true
|
|
76
|
+
for j := range db {
|
|
77
|
+
if data[i+j] != db[j] {
|
|
78
|
+
match = false
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if match {
|
|
83
|
+
return i + len(db), data[:i], nil
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if atEOF && len(data) > 0 {
|
|
88
|
+
return len(data), data, nil
|
|
89
|
+
}
|
|
90
|
+
return 0, nil, nil
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func parseSSEBlock(block string) (SSEEvent, bool) {
|
|
94
|
+
lines := strings.Split(block, "\n")
|
|
95
|
+
isComment := true
|
|
96
|
+
var data string
|
|
97
|
+
hasData := false
|
|
98
|
+
var event string
|
|
99
|
+
var id string
|
|
100
|
+
retry := 0
|
|
101
|
+
|
|
102
|
+
for _, rawLine := range lines {
|
|
103
|
+
line := strings.TrimRight(rawLine, "\r")
|
|
104
|
+
if strings.HasPrefix(line, ":") {
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
isComment = false
|
|
108
|
+
|
|
109
|
+
colonIdx := strings.IndexByte(line, ':')
|
|
110
|
+
var field, val string
|
|
111
|
+
if colonIdx == -1 {
|
|
112
|
+
field = line
|
|
113
|
+
val = ""
|
|
114
|
+
} else {
|
|
115
|
+
field = line[:colonIdx]
|
|
116
|
+
val = line[colonIdx+1:]
|
|
117
|
+
if strings.HasPrefix(val, " ") {
|
|
118
|
+
val = val[1:]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
switch field {
|
|
123
|
+
case "data":
|
|
124
|
+
if !hasData {
|
|
125
|
+
data = val
|
|
126
|
+
} else {
|
|
127
|
+
data = data + "\n" + val
|
|
128
|
+
}
|
|
129
|
+
hasData = true
|
|
130
|
+
case "event":
|
|
131
|
+
event = val
|
|
132
|
+
case "id":
|
|
133
|
+
/* per spec: id containing null byte must be ignored */
|
|
134
|
+
if !strings.ContainsRune(val, 0) {
|
|
135
|
+
id = val
|
|
136
|
+
}
|
|
137
|
+
case "retry":
|
|
138
|
+
if n, err := strconv.Atoi(val); err == nil && n >= 0 {
|
|
139
|
+
retry = n
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if isComment || !hasData {
|
|
145
|
+
return SSEEvent{}, false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return SSEEvent{Data: data, Event: event, ID: id, Retry: retry}, true
|
|
149
|
+
}
|