@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.
Files changed (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
@@ -0,0 +1,399 @@
1
+ # TypeScript API Reference
2
+
3
+ Generated from OpenAPI via the shared codegen IR. Do not edit by hand —
4
+ run `bun run public/honey/core/scripts/gen-api-ref.ts` to regenerate.
5
+
6
+ ## Typed Errors
7
+
8
+ Every operation's error channel surfaces one of:
9
+
10
+ | Canonical | Status | Lang symbol | Notes |
11
+ | --------- | ------ | ----------- | ----- |
12
+ | `BadRequest` | 400 | `BadRequest` | invalid request payload |
13
+ | `Unauthorized` | 401 | `Unauthorized` | missing/invalid auth; triggers onAuthExpired + 1 retry |
14
+ | `Forbidden` | 403 | `Forbidden` | authenticated but not permitted |
15
+ | `NotFound` | 404 | `NotFound` | resource does not exist |
16
+ | `Conflict` | 409 | `Conflict` | state conflict / unique violation |
17
+ | `UnprocessableEntity` | 422 | `UnprocessableEntity` | schema-valid but semantically rejected |
18
+ | `TooManyRequests` | 429 | `TooManyRequests` | rate-limited; consumer handles backoff via hooks |
19
+ | `InternalServerError` | 500 | `InternalServerError` | server crash |
20
+ | `BadGateway` | 502 | `BadGateway` | upstream failure |
21
+ | `ServiceUnavailable` | 503 | `ServiceUnavailable` | server temporarily unavailable |
22
+ | `GatewayTimeout` | 504 | `GatewayTimeout` | upstream timeout |
23
+ | `StatusError` | * | `StatusError` | fallback for undeclared/other statuses |
24
+
25
+ Operations with declared `responses: { 4xx: { schema } }` parse the response body into the error's typed `data` field.
26
+
27
+ ## Operations
28
+
29
+ ### refreshToken
30
+
31
+ `POST /auth/refresh`
32
+
33
+ **Signature**
34
+
35
+ ```
36
+ refreshToken(body: { refresh_token: string }): Promise<{ access_token: string; expires_in: number }>
37
+ ```
38
+
39
+ **Request body**
40
+
41
+ - Content-type: `application/json`
42
+ - Required: yes
43
+ - Type: `{ refresh_token: string }`
44
+
45
+ **Responses**
46
+
47
+ | Status | Content-type | Type |
48
+ | ------ | ------------ | ---- |
49
+ | `200` | `application/json` | `{ access_token: string; expires_in: number }` |
50
+ | `401` | `application/json` | `{ status: number; message: string }` |
51
+
52
+ ---
53
+
54
+ ### getDeclaredError
55
+
56
+ `GET /declared-errors/{status}`
57
+
58
+ **Signature**
59
+
60
+ ```
61
+ getDeclaredError(status: string): Promise<void>
62
+ ```
63
+
64
+ **Path params**
65
+
66
+ | Name | Type |
67
+ | ---- | ---- |
68
+ | `status` | `string` |
69
+
70
+ **Responses**
71
+
72
+ | Status | Content-type | Type |
73
+ | ------ | ------------ | ---- |
74
+ | `400` | `application/json` | `{ status: number; message: string }` |
75
+ | `404` | `application/json` | `{ status: number; message: string }` |
76
+ | `default` | `application/json` | `{ status: number; message: string }` |
77
+
78
+ ---
79
+
80
+ ### getError
81
+
82
+ `GET /errors/{status}`
83
+
84
+ **Signature**
85
+
86
+ ```
87
+ getError(status: number): Promise<void>
88
+ ```
89
+
90
+ **Path params**
91
+
92
+ | Name | Type |
93
+ | ---- | ---- |
94
+ | `status` | `number` |
95
+
96
+ **Responses**
97
+
98
+ | Status | Content-type | Type |
99
+ | ------ | ------------ | ---- |
100
+ | `default` | `application/json` | `{ status: number; message: string }` |
101
+
102
+ ---
103
+
104
+ ### idempotentCreate
105
+
106
+ `POST /idempotent-create`
107
+
108
+ **Signature**
109
+
110
+ ```
111
+ idempotentCreate(): Promise<{ idempotencyKey: string }>
112
+ ```
113
+
114
+ **Responses**
115
+
116
+ | Status | Content-type | Type |
117
+ | ------ | ------------ | ---- |
118
+ | `200` | `application/json` | `{ idempotencyKey: string }` |
119
+
120
+ **Extensions**
121
+
122
+ - `x-idempotency-key`: yes — auto-sent if omitted
123
+
124
+ ---
125
+
126
+ ### connectRealtime
127
+
128
+ `GET /realtime`
129
+
130
+ **Signature**
131
+
132
+ ```
133
+ connectRealtime(reconnect_token: string): ResumableConnection
134
+ ```
135
+
136
+ **Query params**
137
+
138
+ | Name | Type |
139
+ | ---- | ---- |
140
+ | `reconnect_token` | `string` |
141
+
142
+ **Responses**
143
+
144
+ | Status | Content-type | Type |
145
+ | ------ | ------------ | ---- |
146
+ | `101` | `—` | — |
147
+
148
+ **Extensions**
149
+
150
+ - `x-realtime`: yes
151
+
152
+ ---
153
+
154
+ ### slow
155
+
156
+ `GET /slow`
157
+
158
+ **Signature**
159
+
160
+ ```
161
+ slow(ms: number): Promise<{ ok: boolean }>
162
+ ```
163
+
164
+ **Query params**
165
+
166
+ | Name | Type |
167
+ | ---- | ---- |
168
+ | `ms` | `number` |
169
+
170
+ **Responses**
171
+
172
+ | Status | Content-type | Type |
173
+ | ------ | ------------ | ---- |
174
+ | `200` | `application/json` | `{ ok: boolean }` |
175
+
176
+ ---
177
+
178
+ ### streamEvents
179
+
180
+ `GET /stream`
181
+
182
+ **Signature**
183
+
184
+ ```
185
+ streamEvents(Last-Event-ID: string): AsyncIterable<{ event?: string; data?: string; id?: string }>
186
+ ```
187
+
188
+ **Header params**
189
+
190
+ | Name | Type |
191
+ | ---- | ---- |
192
+ | `Last-Event-ID` | `string` |
193
+
194
+ **Responses**
195
+
196
+ | Status | Content-type | Type |
197
+ | ------ | ------------ | ---- |
198
+ | `200` | `text/event-stream` | `{ event?: string; data?: string; id?: string }` |
199
+ | `401` | `application/json` | `{ status: number; message: string }` |
200
+
201
+ **Extensions**
202
+
203
+ - SSE response detected (`text/event-stream`)
204
+
205
+ ---
206
+
207
+ ### uploadBlob
208
+
209
+ `POST /upload`
210
+
211
+ **Signature**
212
+
213
+ ```
214
+ uploadBlob(body: ReadableStream | Blob): Promise<{ size: number; hash: string }>
215
+ ```
216
+
217
+ **Request body**
218
+
219
+ - Content-type: `application/octet-stream`
220
+ - Required: yes
221
+ - Type: `ReadableStream | Blob`
222
+
223
+ **Responses**
224
+
225
+ | Status | Content-type | Type |
226
+ | ------ | ------------ | ---- |
227
+ | `200` | `application/json` | `{ size: number; hash: string }` |
228
+ | `401` | `application/json` | `{ status: number; message: string }` |
229
+
230
+ ---
231
+
232
+ ### listUsers
233
+
234
+ `GET /users`
235
+
236
+ **Signature**
237
+
238
+ ```
239
+ listUsers(): Promise<{ items: { id: string; name: string; email: string }[]; total: number }>
240
+ ```
241
+
242
+ **Responses**
243
+
244
+ | Status | Content-type | Type |
245
+ | ------ | ------------ | ---- |
246
+ | `200` | `application/json` | `{ items: { id: string; name: string; email: string }[]; total: number }` |
247
+ | `401` | `application/json` | `{ status: number; message: string }` |
248
+
249
+ ---
250
+
251
+ ### createUser
252
+
253
+ `POST /users`
254
+
255
+ **Signature**
256
+
257
+ ```
258
+ createUser(body: { name: string; email: string }): Promise<{ id: string; name: string; email: string }>
259
+ ```
260
+
261
+ **Request body**
262
+
263
+ - Content-type: `application/json`
264
+ - Required: yes
265
+ - Type: `{ name: string; email: string }`
266
+
267
+ **Responses**
268
+
269
+ | Status | Content-type | Type |
270
+ | ------ | ------------ | ---- |
271
+ | `201` | `application/json` | `{ id: string; name: string; email: string }` |
272
+ | `401` | `application/json` | `{ status: number; message: string }` |
273
+ | `422` | `application/json` | `{ status: number; message: string }` |
274
+
275
+ **Extensions**
276
+
277
+ - `x-invalidate`: `GET /users`
278
+
279
+ ---
280
+
281
+ ### deleteUser
282
+
283
+ `DELETE /users/{id}`
284
+
285
+ **Signature**
286
+
287
+ ```
288
+ deleteUser(id: string): Promise<void>
289
+ ```
290
+
291
+ **Path params**
292
+
293
+ | Name | Type |
294
+ | ---- | ---- |
295
+ | `id` | `string` |
296
+
297
+ **Responses**
298
+
299
+ | Status | Content-type | Type |
300
+ | ------ | ------------ | ---- |
301
+ | `204` | `—` | — |
302
+ | `401` | `application/json` | `{ status: number; message: string }` |
303
+ | `404` | `application/json` | `{ status: number; message: string }` |
304
+
305
+ **Extensions**
306
+
307
+ - `x-invalidate`: `GET /users`, `GET /users/{id}`
308
+
309
+ ---
310
+
311
+ ### getUser
312
+
313
+ `GET /users/{id}`
314
+
315
+ **Signature**
316
+
317
+ ```
318
+ getUser(id: string): Promise<{ id: string; name: string; email: string }>
319
+ ```
320
+
321
+ **Path params**
322
+
323
+ | Name | Type |
324
+ | ---- | ---- |
325
+ | `id` | `string` |
326
+
327
+ **Responses**
328
+
329
+ | Status | Content-type | Type |
330
+ | ------ | ------------ | ---- |
331
+ | `200` | `application/json` | `{ id: string; name: string; email: string }` |
332
+ | `401` | `application/json` | `{ status: number; message: string }` |
333
+ | `404` | `application/json` | `{ status: number; message: string }` |
334
+
335
+ ---
336
+
337
+ ### updateUser
338
+
339
+ `PUT /users/{id}`
340
+
341
+ **Signature**
342
+
343
+ ```
344
+ updateUser(id: string, body: { name?: string; email?: string }): Promise<{ id: string; name: string; email: string }>
345
+ ```
346
+
347
+ **Path params**
348
+
349
+ | Name | Type |
350
+ | ---- | ---- |
351
+ | `id` | `string` |
352
+
353
+ **Request body**
354
+
355
+ - Content-type: `application/json`
356
+ - Required: yes
357
+ - Type: `{ name?: string; email?: string }`
358
+
359
+ **Responses**
360
+
361
+ | Status | Content-type | Type |
362
+ | ------ | ------------ | ---- |
363
+ | `200` | `application/json` | `{ id: string; name: string; email: string }` |
364
+ | `401` | `application/json` | `{ status: number; message: string }` |
365
+ | `404` | `application/json` | `{ status: number; message: string }` |
366
+
367
+ **Extensions**
368
+
369
+ - `x-invalidate`: `GET /users`, `GET /users/{id}`
370
+
371
+ ---
372
+
373
+ ### connectWs
374
+
375
+ `GET /ws`
376
+
377
+ **Signature**
378
+
379
+ ```
380
+ connectWs(token: string): ResumableConnection
381
+ ```
382
+
383
+ **Query params**
384
+
385
+ | Name | Type |
386
+ | ---- | ---- |
387
+ | `token` | `string` |
388
+
389
+ **Responses**
390
+
391
+ | Status | Content-type | Type |
392
+ | ------ | ------------ | ---- |
393
+ | `101` | `—` | — |
394
+
395
+ **Extensions**
396
+
397
+ - `x-websocket`: yes
398
+
399
+ ---
@@ -0,0 +1,303 @@
1
+ # Honey Invalidate Spec
2
+
3
+ ## Status
4
+
5
+ Draft
6
+
7
+ ## Goal
8
+
9
+ Add first-class Honey metadata for declaring which route operations a mutation invalidates.
10
+
11
+ The design must:
12
+
13
+ - live in Honey base meta types
14
+ - be narrowed by generated route-aware types when available
15
+ - use canonical method-qualified route selectors
16
+ - remain simple to author and read
17
+ - serve as the source of truth for downstream SDK and query integrations
18
+
19
+ ## Non-Goals
20
+
21
+ - a generic key DSL
22
+ - tuple-based metadata forms
23
+ - route-pattern inference from REST naming
24
+ - read-side key declarations
25
+ - transport-level `x-fresh` behavior in the first metadata contract
26
+
27
+ ## Problem
28
+
29
+ Honey already knows the route graph and already generates route-aware types.
30
+
31
+ Mutations often make one or more read routes stale. We want a declarative way to express that relationship once, in Honey route metadata, so downstream tooling can use it for:
32
+
33
+ - query invalidation
34
+ - selective refetch behavior
35
+ - future SDK-level consistency helpers
36
+
37
+ The metadata should leverage Honey codegen rather than forcing authors to hand-maintain custom operation IDs, tuple formats, or synthetic cache keys.
38
+
39
+ ## Proposed API
40
+
41
+ Add `invalidate` to Honey base meta:
42
+
43
+ ```ts
44
+ type InvalidateMeta = {
45
+ invalidate?: readonly string[] | null
46
+ }
47
+ ```
48
+
49
+ This extends existing Honey base metadata rather than creating a separate metadata namespace.
50
+
51
+ ## Authoring Form
52
+
53
+ `invalidate` entries must always use canonical method-qualified route selectors:
54
+
55
+ ```ts
56
+ "GET /v1/organizations/:organization_id/projects"
57
+ ```
58
+
59
+ Not allowed in v1:
60
+
61
+ ```ts
62
+ "/v1/organizations/:organization_id/projects"
63
+ ```
64
+
65
+ Reason:
66
+
67
+ - no ambiguity
68
+ - no fallback “all methods on this path” behavior
69
+ - no special-case parser rules
70
+ - codegen and runtime matching stay trivial
71
+ - editor autocomplete stays exact
72
+
73
+ ## Examples
74
+
75
+ ### Mutation Invalidating Collection Read
76
+
77
+ ```ts
78
+ .meta({
79
+ operationId: "project.create",
80
+ invalidate: ["GET /v1/organizations/:organization_id/projects"],
81
+ })
82
+ ```
83
+
84
+ ### Mutation Invalidating Collection And Entity Reads
85
+
86
+ ```ts
87
+ .meta({
88
+ operationId: "project.update",
89
+ invalidate: [
90
+ "GET /v1/organizations/:organization_id/projects",
91
+ "GET /v1/organizations/:organization_id/projects/:project_id",
92
+ ],
93
+ })
94
+ ```
95
+
96
+ ### Mutation Invalidating Query-Like POST Reads
97
+
98
+ ```ts
99
+ .meta({
100
+ operationId: "project.import",
101
+ invalidate: [
102
+ "POST /v1/organizations/:organization_id/projects/search",
103
+ "GET /v1/organizations/:organization_id/projects",
104
+ ],
105
+ })
106
+ ```
107
+
108
+ This is intentional. `invalidate` should not be artificially limited to `GET`. If a downstream query package treats a `POST` endpoint as queryable, it should be a valid invalidation target.
109
+
110
+ ## Canonical Selector Format
111
+
112
+ Each selector must be:
113
+
114
+ ```ts
115
+ `${Uppercase<HttpMethod>} ${RoutePattern}`
116
+ ```
117
+
118
+ Examples:
119
+
120
+ - `GET /v1/users`
121
+ - `GET /v1/users/:id`
122
+ - `POST /v1/search`
123
+
124
+ Requirements:
125
+
126
+ - method must be uppercase
127
+ - path must be the Honey route pattern form
128
+ - path params must remain as route params, for example `:organization_id`
129
+
130
+ ## Type Model
131
+
132
+ Honey base meta should define `invalidate` with a permissive fallback type:
133
+
134
+ ```ts
135
+ type DefaultInvalidate = readonly string[] | null
136
+ ```
137
+
138
+ Generated apps should narrow this to an exact union of valid selectors when route-aware generated types exist.
139
+
140
+ Illustrative generated type:
141
+
142
+ ```ts
143
+ type GeneratedInvalidateRoute =
144
+ | "GET /v1/organizations/:organization_id/projects"
145
+ | "GET /v1/organizations/:organization_id/projects/:project_id"
146
+ | "POST /v1/organizations/:organization_id/projects/search"
147
+ ```
148
+
149
+ Then the generated meta type becomes effectively:
150
+
151
+ ```ts
152
+ invalidate?: readonly GeneratedInvalidateRoute[] | null
153
+ ```
154
+
155
+ If generated route-aware types are unavailable, the type falls back to:
156
+
157
+ ```ts
158
+ readonly string[] | null
159
+ ```
160
+
161
+ ## Honey Type Integration
162
+
163
+ This should extend existing base Honey metadata, not replace it.
164
+
165
+ Conceptually:
166
+
167
+ ```ts
168
+ type OpenApiMeta<TInvalidate extends readonly string[] | null = readonly string[] | null> = {
169
+ internal?: boolean
170
+ deprecated?: boolean
171
+ description?: string
172
+ invalidate?: TInvalidate
173
+ operationId?: string
174
+ security?: Array<Record<string, string[]>> | string | string[]
175
+ summary?: string
176
+ tags?: string | string[]
177
+ }
178
+ ```
179
+
180
+ This snippet is conceptual, not a required literal implementation. Honey currently exposes non-generic `OpenApiMeta` and `DefaultMeta`, so the real implementation should preserve that public shape and use Honey's existing generated type or augmentation pattern to narrow `invalidate`.
181
+
182
+ The behavior must be:
183
+
184
+ - base Honey always exposes `invalidate`
185
+ - generated Honey types narrow `invalidate` to the route-selector union when available
186
+
187
+ ## Codegen Responsibilities
188
+
189
+ Honey codegen should:
190
+
191
+ 1. collect all canonical route selectors from the route tree
192
+ 2. emit the selector union for generated type narrowing
193
+ 3. preserve `invalidate` metadata into generated outputs needed by downstream tooling
194
+ 4. validate that authored selectors exist in the route graph during generation
195
+
196
+ Canonical selectors must be generated from the same route inventory Honey already uses for route typing and OpenAPI emission. There must be one canonical string form for each route operation.
197
+
198
+ ## Runtime / Consumer Model
199
+
200
+ Honey core only defines and emits the metadata contract.
201
+
202
+ Downstream packages consume it.
203
+
204
+ Examples:
205
+
206
+ - a query package can map invalidated selectors to query families
207
+ - a future SDK helper can use invalidated selectors to decide which subsequent reads need stronger consistency handling
208
+
209
+ The important constraint is:
210
+
211
+ - authors declare invalidation in terms of actual Honey routes
212
+ - downstream tooling works from normalized route selectors
213
+
214
+ ## Why Route Selectors Instead Of Operation IDs
215
+
216
+ Operation IDs are useful but are a secondary naming layer.
217
+
218
+ Route selectors are better for this feature because:
219
+
220
+ - they are directly tied to the actual route graph
221
+ - Honey already understands them structurally
222
+ - they can be generated and validated without inventing another abstraction
223
+ - they naturally support query-like `POST` routes
224
+ - they are clearer in metadata than synthetic cache keys
225
+
226
+ ## Why No Tuple Shapes
227
+
228
+ Tuple forms are shorter but harder to read and easier to misuse.
229
+
230
+ This feature is authored metadata, not a performance-critical wire format.
231
+
232
+ Clarity matters more than terseness.
233
+
234
+ ## Why No Path-Only Form
235
+
236
+ Path-only selectors look convenient:
237
+
238
+ ```ts
239
+ "/v1/organizations/:organization_id/projects"
240
+ ```
241
+
242
+ but they create ambiguity:
243
+
244
+ - does it mean all methods?
245
+ - should `GET` and `HEAD` both match?
246
+ - should `POST` query-like reads match too?
247
+
248
+ Forcing the method in every selector avoids all of that.
249
+
250
+ ## Validation Rules
251
+
252
+ Validation must happen in two places:
253
+
254
+ - generated typing should narrow authored values to the exact selector union when available
255
+ - codegen should validate raw authored `invalidate` strings against the route graph during generation
256
+ - duplicates should be deduplicated in generated normalized metadata
257
+ - invalid selectors should fail type-checking when possible and fail generation otherwise
258
+
259
+ ## Backward Compatibility
260
+
261
+ This feature is fully opt-in.
262
+
263
+ If `invalidate` is not used:
264
+
265
+ - existing route metadata behavior is unchanged
266
+ - existing generated SDK behavior is unchanged
267
+ - existing query integrations are unchanged
268
+
269
+ ## Proposed Implementation Phases
270
+
271
+ ### Phase 1: Base Meta
272
+
273
+ - add `invalidate?: readonly string[] | null` to Honey base meta
274
+
275
+ ### Phase 2: Generated Narrowing
276
+
277
+ - generate canonical selector unions
278
+ - narrow `invalidate` using generated types when available
279
+
280
+ ### Phase 3: Metadata Emission
281
+
282
+ - preserve `invalidate` in generated artifacts needed by downstream consumers
283
+
284
+ ### Phase 4: Query Package
285
+
286
+ - consume generated `invalidate` metadata
287
+ - map selectors to query invalidation behavior
288
+ - later add convenience helpers
289
+
290
+ ### Phase 5: SDK Integration
291
+
292
+ - if needed, consume normalized invalidation metadata for stronger read-after-write handling
293
+
294
+ ## Recommendation
295
+
296
+ Ship the narrow version:
297
+
298
+ - `invalidate`
299
+ - method-qualified route selectors only
300
+ - generated route-aware narrowing
301
+ - downstream packages consume the emitted metadata
302
+
303
+ Do not add tuple forms, key DSLs, or path-only shortcuts in v1.