pairling 0.0.1 → 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/package.json +5 -1
- package/payload/mac/SOURCE_BRANCH +1 -0
- package/payload/mac/SOURCE_DIRTY +1 -0
- package/payload/mac/SOURCE_REVISION +1 -0
- package/payload/mac/VERSION +1 -0
- package/payload/mac/companiond/integrations/__init__.py +1 -0
- package/payload/mac/companiond/integrations/aperture_cli/__init__.py +23 -0
- package/payload/mac/companiond/integrations/aperture_cli/launch.py +456 -0
- package/payload/mac/companiond/integrations/aperture_cli/status.py +393 -0
- package/payload/mac/companiond/live_activity_publisher.py +380 -0
- package/payload/mac/companiond/llm_route.py +108 -0
- package/payload/mac/companiond/local_mcp_bridge.py +156 -0
- package/payload/mac/companiond/model_status_contract.py +101 -0
- package/payload/mac/companiond/pairdrop_store.py +920 -0
- package/payload/mac/companiond/pairling_connectd_status.py +149 -0
- package/payload/mac/companiond/pairling_devices.py +459 -0
- package/payload/mac/companiond/pairling_pairing.py +404 -0
- package/payload/mac/companiond/pairling_relay_claims.py +232 -0
- package/payload/mac/companiond/pairling_tools.py +706 -0
- package/payload/mac/companiond/pairlingd.py +18438 -0
- package/payload/mac/companiond/providers/__init__.py +1 -0
- package/payload/mac/companiond/providers/base.py +255 -0
- package/payload/mac/companiond/providers/claude.py +127 -0
- package/payload/mac/companiond/providers/codex.py +124 -0
- package/payload/mac/companiond/providers/external.py +46 -0
- package/payload/mac/companiond/providers/registry.py +70 -0
- package/payload/mac/companiond/pty_broker.py +887 -0
- package/payload/mac/companiond/push_dispatcher.py +1990 -0
- package/payload/mac/companiond/push_event_catalog.py +566 -0
- package/payload/mac/companiond/request_proof.py +142 -0
- package/payload/mac/companiond/runtime_contract.py +47 -0
- package/payload/mac/companiond/runtime_manifest.py +197 -0
- package/payload/mac/companiond/runtime_paths.py +87 -0
- package/payload/mac/companiond/safety_monitor.py +542 -0
- package/payload/mac/companiond/sentinel_notifications.py +491 -0
- package/payload/mac/companiond/standard_push_publisher.py +516 -0
- package/payload/mac/companiond/substrate_status_contract.py +139 -0
- package/payload/mac/companiond/terminal_screen_backend.py +332 -0
- package/payload/mac/companiond/terminal_text_sanitizer.py +54 -0
- package/payload/mac/companiond/workstate_feed_contract.py +108 -0
- package/payload/mac/connectd/cmd/pairling-connectd/auth_open_test.go +116 -0
- package/payload/mac/connectd/cmd/pairling-connectd/main.go +345 -0
- package/payload/mac/connectd/cmd/pairling-connectd/upstream_health_test.go +33 -0
- package/payload/mac/connectd/go.mod +51 -0
- package/payload/mac/connectd/go.sum +229 -0
- package/payload/mac/connectd/internal/gateway/proxy.go +597 -0
- package/payload/mac/connectd/internal/gateway/proxy_test.go +531 -0
- package/payload/mac/connectd/internal/runtime/config.go +99 -0
- package/payload/mac/connectd/internal/runtime/config_test.go +29 -0
- package/payload/mac/connectd/internal/status/status.go +300 -0
- package/payload/mac/connectd/internal/status/status_test.go +263 -0
- package/payload/mac/guardian/companion-power-guardian.py +613 -0
- package/payload/mac/guardian/guardian_contract.py +67 -0
- package/payload/mac/install/bootstrap-first-run.sh +206 -0
- package/payload/mac/install/doctor.sh +660 -0
- package/payload/mac/install/install-runtime.sh +1241 -0
- package/payload/mac/install/render-launchd.py +119 -0
- package/payload/mac/install/uninstall-runtime.sh +136 -0
- package/payload/mac/mcp/phone_tools.py +210 -0
- package/payload/mac/packaging/bin/pairling +63 -0
- package/payload-manifest.json +255 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
package gateway
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"errors"
|
|
6
|
+
"net"
|
|
7
|
+
"net/http"
|
|
8
|
+
"net/http/httputil"
|
|
9
|
+
"net/url"
|
|
10
|
+
"strings"
|
|
11
|
+
"sync"
|
|
12
|
+
"time"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const defaultMaxBodyBytes int64 = 1_000_000
|
|
16
|
+
const prePairMaxBodyBytes int64 = 16 * 1024
|
|
17
|
+
const pairDropSmallFileMaxBodyBytes int64 = 10 * 1024 * 1024
|
|
18
|
+
const pairDropUploadChunkMaxBodyBytes int64 = 1024 * 1024
|
|
19
|
+
|
|
20
|
+
// Chat attachment uploads (POST /upload) carry whole photos/short videos in
|
|
21
|
+
// one shot — the 1MB default rejected most camera photos with 413.
|
|
22
|
+
const chatUploadMaxBodyBytes int64 = 25 * 1024 * 1024
|
|
23
|
+
|
|
24
|
+
type ExposureMode string
|
|
25
|
+
|
|
26
|
+
const (
|
|
27
|
+
ExposureModePostPair ExposureMode = "post_pair"
|
|
28
|
+
ExposureModePrePair ExposureMode = "pre_pair"
|
|
29
|
+
ExposureModePairlingConnect ExposureMode = "pairling_connect"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Logger receives metadata-only gateway events. Event intentionally excludes
|
|
33
|
+
// request bodies, query values, authorization values, and proof material.
|
|
34
|
+
type Logger interface {
|
|
35
|
+
Log(Event)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Event struct {
|
|
39
|
+
Method string
|
|
40
|
+
Path string
|
|
41
|
+
Outcome string
|
|
42
|
+
Status int
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type Options struct {
|
|
46
|
+
Upstream *url.URL
|
|
47
|
+
MaxBodyBytes int64
|
|
48
|
+
Mode ExposureMode
|
|
49
|
+
Logger Logger
|
|
50
|
+
RateLimiter RateLimiter
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type Handler struct {
|
|
54
|
+
upstream *url.URL
|
|
55
|
+
maxBodyBytes int64
|
|
56
|
+
mode ExposureMode
|
|
57
|
+
logger Logger
|
|
58
|
+
rateLimiter RateLimiter
|
|
59
|
+
proxy *httputil.ReverseProxy
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type RateLimiter interface {
|
|
63
|
+
Allow(remoteAddr, method, path string) bool
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func NewHandler(opts Options) (*Handler, error) {
|
|
67
|
+
if opts.Upstream == nil {
|
|
68
|
+
return nil, errors.New("upstream is required")
|
|
69
|
+
}
|
|
70
|
+
if opts.Upstream.Scheme != "http" && opts.Upstream.Scheme != "https" {
|
|
71
|
+
return nil, errors.New("upstream scheme must be http or https")
|
|
72
|
+
}
|
|
73
|
+
if opts.Upstream.Host == "" {
|
|
74
|
+
return nil, errors.New("upstream host is required")
|
|
75
|
+
}
|
|
76
|
+
if !localUpstream(opts.Upstream) {
|
|
77
|
+
return nil, errors.New("upstream host must be loopback")
|
|
78
|
+
}
|
|
79
|
+
maxBody := opts.MaxBodyBytes
|
|
80
|
+
if maxBody <= 0 {
|
|
81
|
+
maxBody = defaultMaxBodyBytes
|
|
82
|
+
}
|
|
83
|
+
mode := opts.Mode
|
|
84
|
+
if mode == "" {
|
|
85
|
+
mode = ExposureModePostPair
|
|
86
|
+
}
|
|
87
|
+
if mode != ExposureModePostPair && mode != ExposureModePrePair && mode != ExposureModePairlingConnect {
|
|
88
|
+
return nil, errors.New("unknown exposure mode")
|
|
89
|
+
}
|
|
90
|
+
upstream := *opts.Upstream
|
|
91
|
+
h := &Handler{
|
|
92
|
+
upstream: &upstream,
|
|
93
|
+
maxBodyBytes: maxBody,
|
|
94
|
+
mode: mode,
|
|
95
|
+
logger: opts.Logger,
|
|
96
|
+
rateLimiter: opts.RateLimiter,
|
|
97
|
+
}
|
|
98
|
+
h.proxy = &httputil.ReverseProxy{
|
|
99
|
+
Rewrite: h.rewrite,
|
|
100
|
+
ErrorHandler: h.proxyError,
|
|
101
|
+
FlushInterval: -1,
|
|
102
|
+
}
|
|
103
|
+
return h, nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
107
|
+
path := r.URL.EscapedPath()
|
|
108
|
+
if path == "" {
|
|
109
|
+
path = "/"
|
|
110
|
+
}
|
|
111
|
+
if !supportedMethod(r.Method) {
|
|
112
|
+
h.reject(w, r, http.StatusMethodNotAllowed, "method_not_allowed")
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if !h.allowed(r.Method, path, r.Header) {
|
|
116
|
+
if h.allowedForAnyMethod(path, r.Header) {
|
|
117
|
+
h.reject(w, r, http.StatusMethodNotAllowed, "method_not_allowed")
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
h.reject(w, r, http.StatusNotFound, "path_not_allowed")
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
if h.rateLimiter != nil && h.rateLimitPath(r.Method, path) && !h.rateLimiter.Allow(r.RemoteAddr, r.Method, path) {
|
|
124
|
+
h.reject(w, r, http.StatusTooManyRequests, "rate_limited")
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
bodyLimit := h.requestBodyLimit(r.Method, path)
|
|
128
|
+
if bodyLimit > 0 {
|
|
129
|
+
if r.ContentLength > bodyLimit {
|
|
130
|
+
h.reject(w, r, http.StatusRequestEntityTooLarge, "request_too_large")
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
r.Body = http.MaxBytesReader(w, r.Body, bodyLimit)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
|
|
137
|
+
h.proxy.ServeHTTP(rec, r)
|
|
138
|
+
h.log(r, rec.status, "forwarded")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func (h *Handler) rewrite(r *httputil.ProxyRequest) {
|
|
142
|
+
in := r.In
|
|
143
|
+
r.SetURL(h.upstream)
|
|
144
|
+
r.Out.URL.Path = joinPath(h.upstream.Path, in.URL.Path)
|
|
145
|
+
r.Out.URL.RawPath = ""
|
|
146
|
+
if h.upstream.RawQuery == "" || in.URL.RawQuery == "" {
|
|
147
|
+
r.Out.URL.RawQuery = h.upstream.RawQuery + in.URL.RawQuery
|
|
148
|
+
} else {
|
|
149
|
+
r.Out.URL.RawQuery = h.upstream.RawQuery + "&" + in.URL.RawQuery
|
|
150
|
+
}
|
|
151
|
+
r.Out.Host = h.upstream.Host
|
|
152
|
+
r.Out.Header.Del("X-Forwarded-For")
|
|
153
|
+
r.Out.Header.Set("X-Pairling-Connect-Gateway", "pairling-connectd")
|
|
154
|
+
r.SetXForwarded()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
func (h *Handler) proxyError(w http.ResponseWriter, r *http.Request, err error) {
|
|
158
|
+
h.reject(w, r, http.StatusBadGateway, "upstream_error")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func (h *Handler) reject(w http.ResponseWriter, r *http.Request, status int, code string) {
|
|
162
|
+
h.log(r, status, code)
|
|
163
|
+
w.Header().Set("Content-Type", "application/json")
|
|
164
|
+
w.WriteHeader(status)
|
|
165
|
+
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
166
|
+
"ok": false,
|
|
167
|
+
"error": map[string]string{
|
|
168
|
+
"code": code,
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func (h *Handler) log(r *http.Request, status int, outcome string) {
|
|
174
|
+
if h.logger == nil {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
path := r.URL.EscapedPath()
|
|
178
|
+
if path == "" {
|
|
179
|
+
path = "/"
|
|
180
|
+
}
|
|
181
|
+
h.logger.Log(Event{
|
|
182
|
+
Method: r.Method,
|
|
183
|
+
Path: path,
|
|
184
|
+
Outcome: outcome,
|
|
185
|
+
Status: status,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type statusRecorder struct {
|
|
190
|
+
http.ResponseWriter
|
|
191
|
+
status int
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func (r *statusRecorder) WriteHeader(status int) {
|
|
195
|
+
r.status = status
|
|
196
|
+
r.ResponseWriter.WriteHeader(status)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
func supportedMethod(method string) bool {
|
|
200
|
+
return method == http.MethodGet || method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func (h *Handler) allowed(method, path string, header http.Header) bool {
|
|
204
|
+
switch h.mode {
|
|
205
|
+
case ExposureModePrePair:
|
|
206
|
+
return prePairAllowed(method, path)
|
|
207
|
+
case ExposureModePairlingConnect:
|
|
208
|
+
if path == "/pair/start" {
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
if prePairAllowed(method, path) {
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
return hasBearer(header) && Allowed(method, path)
|
|
215
|
+
default:
|
|
216
|
+
return Allowed(method, path)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func (h *Handler) allowedForAnyMethod(path string, header http.Header) bool {
|
|
221
|
+
switch h.mode {
|
|
222
|
+
case ExposureModePrePair:
|
|
223
|
+
return prePairAllowed(http.MethodGet, path) || prePairAllowed(http.MethodPost, path)
|
|
224
|
+
case ExposureModePairlingConnect:
|
|
225
|
+
if path == "/pair/start" {
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
if prePairAllowed(http.MethodGet, path) || prePairAllowed(http.MethodPost, path) {
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
return hasBearer(header) && allowedForAnyMethod(path)
|
|
232
|
+
default:
|
|
233
|
+
return allowedForAnyMethod(path)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
func (h *Handler) requestBodyLimit(method, path string) int64 {
|
|
238
|
+
if method == http.MethodPost && path == "/pair/claim" && (h.mode == ExposureModePrePair || h.mode == ExposureModePairlingConnect) {
|
|
239
|
+
if h.maxBodyBytes <= 0 || prePairMaxBodyBytes < h.maxBodyBytes {
|
|
240
|
+
return prePairMaxBodyBytes
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if method == http.MethodPost && path == "/pairdrop/files" {
|
|
244
|
+
if h.maxBodyBytes <= 0 || pairDropSmallFileMaxBodyBytes < h.maxBodyBytes {
|
|
245
|
+
return pairDropSmallFileMaxBodyBytes
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if method == http.MethodPost && path == "/upload" {
|
|
249
|
+
if h.maxBodyBytes <= 0 || h.maxBodyBytes < chatUploadMaxBodyBytes {
|
|
250
|
+
return chatUploadMaxBodyBytes
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if method == http.MethodPut && pairDropUploadBytesPath(path) {
|
|
254
|
+
if h.maxBodyBytes <= 0 || pairDropUploadChunkMaxBodyBytes < h.maxBodyBytes {
|
|
255
|
+
return pairDropUploadChunkMaxBodyBytes
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return h.maxBodyBytes
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
func (h *Handler) rateLimitPath(method, path string) bool {
|
|
262
|
+
return method == http.MethodPost && path == "/pair/claim" && (h.mode == ExposureModePrePair || h.mode == ExposureModePairlingConnect)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func prePairAllowed(method, path string) bool {
|
|
266
|
+
switch method {
|
|
267
|
+
case http.MethodGet:
|
|
268
|
+
return prePairGetPaths[path]
|
|
269
|
+
case http.MethodPost:
|
|
270
|
+
return prePairPostPaths[path]
|
|
271
|
+
default:
|
|
272
|
+
return false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func hasBearer(header http.Header) bool {
|
|
277
|
+
return strings.HasPrefix(header.Get("Authorization"), "Bearer ")
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
func Allowed(method, path string) bool {
|
|
281
|
+
if !supportedMethod(method) {
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
switch method {
|
|
285
|
+
case http.MethodGet:
|
|
286
|
+
return getPaths[path] || dynamicGETPath(path)
|
|
287
|
+
case http.MethodPost:
|
|
288
|
+
return postPaths[path] || dynamicPOSTPath(path)
|
|
289
|
+
case http.MethodPut:
|
|
290
|
+
return dynamicPUTPath(path)
|
|
291
|
+
case http.MethodDelete:
|
|
292
|
+
return dynamicDELETEPath(path)
|
|
293
|
+
default:
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func allowedForAnyMethod(path string) bool {
|
|
299
|
+
return getPaths[path] || postPaths[path] || dynamicGETPath(path) || dynamicPOSTPath(path) || dynamicPUTPath(path) || dynamicDELETEPath(path)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
func localUpstream(upstream *url.URL) bool {
|
|
303
|
+
host := upstream.Hostname()
|
|
304
|
+
if host == "localhost" {
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
ip := net.ParseIP(host)
|
|
308
|
+
return ip != nil && ip.IsLoopback()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
func dynamicGETPath(path string) bool {
|
|
312
|
+
return sessionExportPath(path) || orchestrationItemPath(path) || orchestrationStreamPath(path) || pairDropFileContentPath(path) || pairDropFileItemPath(path) || pairDropUploadItemPath(path)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
func dynamicPOSTPath(path string) bool {
|
|
316
|
+
return pickerMCPRestartPath(path) || orchestrationStopPath(path) || pairDropAttachPath(path) || pairDropUploadCompletePath(path)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
func dynamicPUTPath(path string) bool {
|
|
320
|
+
return pairDropUploadBytesPath(path)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
func dynamicDELETEPath(path string) bool {
|
|
324
|
+
return pairDropFileItemPath(path) || pairDropUploadItemPath(path)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
func sessionExportPath(path string) bool {
|
|
328
|
+
return strings.HasPrefix(path, "/sessions/") && strings.HasSuffix(path, "/export")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func pickerMCPRestartPath(path string) bool {
|
|
332
|
+
return strings.HasPrefix(path, "/pickers/mcp/") && strings.HasSuffix(path, "/restart")
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
func pairDropFileItemPath(path string) bool {
|
|
336
|
+
if !strings.HasPrefix(path, "/pairdrop/files/") {
|
|
337
|
+
return false
|
|
338
|
+
}
|
|
339
|
+
suffix := strings.TrimPrefix(path, "/pairdrop/files/")
|
|
340
|
+
return suffix != "" && !strings.Contains(suffix, "/")
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
func pairDropFileContentPath(path string) bool {
|
|
344
|
+
if !strings.HasPrefix(path, "/pairdrop/files/") || !strings.HasSuffix(path, "/content") {
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
inner := strings.TrimSuffix(strings.TrimPrefix(path, "/pairdrop/files/"), "/content")
|
|
348
|
+
inner = strings.Trim(inner, "/")
|
|
349
|
+
return inner != "" && !strings.Contains(inner, "/")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func pairDropAttachPath(path string) bool {
|
|
353
|
+
if !strings.HasPrefix(path, "/pairdrop/files/") || !strings.HasSuffix(path, "/attach") {
|
|
354
|
+
return false
|
|
355
|
+
}
|
|
356
|
+
inner := strings.TrimSuffix(strings.TrimPrefix(path, "/pairdrop/files/"), "/attach")
|
|
357
|
+
inner = strings.Trim(inner, "/")
|
|
358
|
+
return inner != "" && !strings.Contains(inner, "/")
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func pairDropUploadItemPath(path string) bool {
|
|
362
|
+
if !strings.HasPrefix(path, "/pairdrop/uploads/") {
|
|
363
|
+
return false
|
|
364
|
+
}
|
|
365
|
+
suffix := strings.TrimPrefix(path, "/pairdrop/uploads/")
|
|
366
|
+
return suffix != "" && !strings.Contains(suffix, "/")
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
func pairDropUploadBytesPath(path string) bool {
|
|
370
|
+
if !strings.HasPrefix(path, "/pairdrop/uploads/") || !strings.HasSuffix(path, "/bytes") {
|
|
371
|
+
return false
|
|
372
|
+
}
|
|
373
|
+
inner := strings.TrimSuffix(strings.TrimPrefix(path, "/pairdrop/uploads/"), "/bytes")
|
|
374
|
+
inner = strings.Trim(inner, "/")
|
|
375
|
+
return inner != "" && !strings.Contains(inner, "/")
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
func pairDropUploadCompletePath(path string) bool {
|
|
379
|
+
if !strings.HasPrefix(path, "/pairdrop/uploads/") || !strings.HasSuffix(path, "/complete") {
|
|
380
|
+
return false
|
|
381
|
+
}
|
|
382
|
+
inner := strings.TrimSuffix(strings.TrimPrefix(path, "/pairdrop/uploads/"), "/complete")
|
|
383
|
+
inner = strings.Trim(inner, "/")
|
|
384
|
+
return inner != "" && !strings.Contains(inner, "/")
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
func orchestrationItemPath(path string) bool {
|
|
388
|
+
if !strings.HasPrefix(path, "/orchestrations/") {
|
|
389
|
+
return false
|
|
390
|
+
}
|
|
391
|
+
suffix := strings.TrimPrefix(path, "/orchestrations/")
|
|
392
|
+
return suffix != "" && !strings.Contains(suffix, "/")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func orchestrationStopPath(path string) bool {
|
|
396
|
+
if !strings.HasPrefix(path, "/orchestrations/") {
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
399
|
+
parts := strings.Split(strings.TrimPrefix(path, "/orchestrations/"), "/")
|
|
400
|
+
return len(parts) == 2 && parts[0] != "" && parts[1] == "stop"
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
func orchestrationStreamPath(path string) bool {
|
|
404
|
+
if !strings.HasPrefix(path, "/orchestrations/") {
|
|
405
|
+
return false
|
|
406
|
+
}
|
|
407
|
+
parts := strings.Split(strings.TrimPrefix(path, "/orchestrations/"), "/")
|
|
408
|
+
return len(parts) == 2 && parts[0] != "" && parts[1] == "stream"
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
var getPaths = map[string]bool{
|
|
412
|
+
"/activity": true,
|
|
413
|
+
"/activity-stream": true,
|
|
414
|
+
"/aperture-cli/launch-contexts": true,
|
|
415
|
+
"/aperture-cli/providers": true,
|
|
416
|
+
"/aperture-cli/status": true,
|
|
417
|
+
"/commands": true,
|
|
418
|
+
"/commands-stream": true,
|
|
419
|
+
"/corpus": true,
|
|
420
|
+
"/filesystem/directories": true,
|
|
421
|
+
"/health": true,
|
|
422
|
+
"/health-stream": true,
|
|
423
|
+
"/healthz": true,
|
|
424
|
+
"/readyz": true,
|
|
425
|
+
"/routez": true,
|
|
426
|
+
"/invocations": true,
|
|
427
|
+
"/invocations-stream": true,
|
|
428
|
+
"/manifest": true,
|
|
429
|
+
"/mirror/conflicts": true,
|
|
430
|
+
"/mirror/projects": true,
|
|
431
|
+
"/mirror/status": true,
|
|
432
|
+
"/model-status": true,
|
|
433
|
+
"/orchestrations": true,
|
|
434
|
+
"/personal-context": true,
|
|
435
|
+
"/pairdrop/events": true,
|
|
436
|
+
"/pairdrop/files": true,
|
|
437
|
+
"/pickers/hooks": true,
|
|
438
|
+
"/pickers/mcp": true,
|
|
439
|
+
"/pickers/memory": true,
|
|
440
|
+
"/pickers/permissions": true,
|
|
441
|
+
"/pickers/resume": true,
|
|
442
|
+
"/pickers/resume/preview": true,
|
|
443
|
+
"/power-state": true,
|
|
444
|
+
"/provider-status": true,
|
|
445
|
+
"/push/status": true,
|
|
446
|
+
"/recent-projects": true,
|
|
447
|
+
"/safety/events": true,
|
|
448
|
+
"/safety/status": true,
|
|
449
|
+
"/search": true,
|
|
450
|
+
"/sentinel/events": true,
|
|
451
|
+
"/sentinel/preferences": true,
|
|
452
|
+
"/sentinel/status": true,
|
|
453
|
+
"/session-meta": true,
|
|
454
|
+
"/session-live-events": true,
|
|
455
|
+
"/session-source-diagnostics": true,
|
|
456
|
+
"/sessions": true,
|
|
457
|
+
"/sessions-stream": true,
|
|
458
|
+
"/sessions-visible": true,
|
|
459
|
+
"/session-runtime-truth": true,
|
|
460
|
+
"/session-runtime-truth-stream": true,
|
|
461
|
+
"/status": true,
|
|
462
|
+
"/substrate-feed": true,
|
|
463
|
+
"/substrate-status": true,
|
|
464
|
+
"/terminal-stream": true,
|
|
465
|
+
"/terminal-stream-diagnostics": true,
|
|
466
|
+
"/terminal-surface": true,
|
|
467
|
+
"/terminal-surface-stream": true,
|
|
468
|
+
"/terminal-surface-v2": true,
|
|
469
|
+
"/terminal-surface-stream-v2": true,
|
|
470
|
+
"/terminal-workspace": true,
|
|
471
|
+
"/terminal-workspace-stream": true,
|
|
472
|
+
"/tokens": true,
|
|
473
|
+
"/transcript": true,
|
|
474
|
+
"/transcript-stream": true,
|
|
475
|
+
"/turn-state-stream": true,
|
|
476
|
+
"/worker-stats": true,
|
|
477
|
+
"/workers": true,
|
|
478
|
+
"/workstate-feed": true,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
var postPaths = map[string]bool{
|
|
482
|
+
"/aperture-cli/open": true,
|
|
483
|
+
"/cross-provider-action": true,
|
|
484
|
+
"/inject": true,
|
|
485
|
+
"/inject-now": true,
|
|
486
|
+
"/interrupt": true,
|
|
487
|
+
"/llm-route": true,
|
|
488
|
+
"/llm-route-stream": true,
|
|
489
|
+
"/mirror/flush": true,
|
|
490
|
+
"/mirror/resume": true,
|
|
491
|
+
"/open": true,
|
|
492
|
+
"/orchestrations": true,
|
|
493
|
+
"/pair/claim": true,
|
|
494
|
+
"/pair/revoke": true,
|
|
495
|
+
"/pair/rotate-token": true,
|
|
496
|
+
"/pair/start": true,
|
|
497
|
+
"/pairling-tools/run": true,
|
|
498
|
+
"/pairdrop/files": true,
|
|
499
|
+
"/pairdrop/maintenance/cleanup-partials": true,
|
|
500
|
+
"/pairdrop/uploads": true,
|
|
501
|
+
"/phone-tools/availability": true,
|
|
502
|
+
"/phone-tools/next": true,
|
|
503
|
+
"/phone-tools/result": true,
|
|
504
|
+
"/push/live-activity-test": true,
|
|
505
|
+
"/push/live-activity-token": true,
|
|
506
|
+
"/push/preferences": true,
|
|
507
|
+
"/push/test": true,
|
|
508
|
+
"/resume-session": true,
|
|
509
|
+
"/safety/ack": true,
|
|
510
|
+
"/send-text": true,
|
|
511
|
+
"/sentinel/evaluate-now": true,
|
|
512
|
+
"/sentinel/preferences": true,
|
|
513
|
+
"/sentinel/snooze": true,
|
|
514
|
+
"/sigint": true,
|
|
515
|
+
"/sigterm": true,
|
|
516
|
+
"/spawn-session": true,
|
|
517
|
+
"/terminal-control": true,
|
|
518
|
+
"/upload": true,
|
|
519
|
+
"/worker-kill": true,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
var prePairGetPaths = map[string]bool{
|
|
523
|
+
"/health": true,
|
|
524
|
+
"/healthz": true,
|
|
525
|
+
"/readyz": true,
|
|
526
|
+
"/manifest": true,
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
var prePairPostPaths = map[string]bool{
|
|
530
|
+
"/pair/claim": true,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
type MemoryRateLimiter struct {
|
|
534
|
+
mu sync.Mutex
|
|
535
|
+
limit int
|
|
536
|
+
window time.Duration
|
|
537
|
+
hits map[string][]time.Time
|
|
538
|
+
now func() time.Time
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
func NewMemoryRateLimiter(limit int, window time.Duration) *MemoryRateLimiter {
|
|
542
|
+
if limit <= 0 {
|
|
543
|
+
limit = 20
|
|
544
|
+
}
|
|
545
|
+
if window <= 0 {
|
|
546
|
+
window = 5 * time.Minute
|
|
547
|
+
}
|
|
548
|
+
return &MemoryRateLimiter{
|
|
549
|
+
limit: limit,
|
|
550
|
+
window: window,
|
|
551
|
+
hits: map[string][]time.Time{},
|
|
552
|
+
now: time.Now,
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
func (l *MemoryRateLimiter) Allow(remoteAddr, method, path string) bool {
|
|
557
|
+
if l == nil {
|
|
558
|
+
return true
|
|
559
|
+
}
|
|
560
|
+
l.mu.Lock()
|
|
561
|
+
defer l.mu.Unlock()
|
|
562
|
+
now := l.now()
|
|
563
|
+
cutoff := now.Add(-l.window)
|
|
564
|
+
key := rateLimitKey(remoteAddr, method, path)
|
|
565
|
+
existing := l.hits[key]
|
|
566
|
+
kept := existing[:0]
|
|
567
|
+
for _, ts := range existing {
|
|
568
|
+
if ts.After(cutoff) {
|
|
569
|
+
kept = append(kept, ts)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if len(kept) >= l.limit {
|
|
573
|
+
l.hits[key] = kept
|
|
574
|
+
return false
|
|
575
|
+
}
|
|
576
|
+
kept = append(kept, now)
|
|
577
|
+
l.hits[key] = kept
|
|
578
|
+
return true
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
func rateLimitKey(remoteAddr, method, path string) string {
|
|
582
|
+
host, _, err := net.SplitHostPort(remoteAddr)
|
|
583
|
+
if err != nil || host == "" {
|
|
584
|
+
host = remoteAddr
|
|
585
|
+
}
|
|
586
|
+
return host + "|" + method + "|" + path
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
func joinPath(base, path string) string {
|
|
590
|
+
if base == "" || base == "/" {
|
|
591
|
+
if path == "" {
|
|
592
|
+
return "/"
|
|
593
|
+
}
|
|
594
|
+
return path
|
|
595
|
+
}
|
|
596
|
+
return strings.TrimRight(base, "/") + "/" + strings.TrimLeft(path, "/")
|
|
597
|
+
}
|