@cursor/july 0.1.31 → 0.1.33
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/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.DigB_9cQ.js → app.CgZqLyWL.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.B1uHGxBZ.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BCMX25Bv.js → VPLocalSearchBox.C4KigoQn.js} +1 -1
- package/dist/docs/assets/chunks/{theme.BDDWeELx.js → theme.C_Ty4yT7.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/files-backends/agent-store-presigned-url.d.ts +9 -98
- package/dist/files-backends/agent-store-presigned-url.d.ts.map +1 -1
- package/dist/files-backends/agent-store-presigned-url.js +199 -314
- package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/files-backends/cursor-hosted.js +25 -2
- package/dist/internal/cursor-relay-core.d.ts +12 -0
- package/dist/internal/cursor-relay-core.d.ts.map +1 -1
- package/dist/internal/cursor-relay-core.js +42 -8
- package/dist/internal/review-comments.d.ts +9 -1
- package/dist/internal/review-comments.d.ts.map +1 -1
- package/dist/internal/review-comments.js +76 -0
- package/dist/internal/server.js +8 -1
- package/dist/playground/assets/index-BRhBbnd4.css +1 -0
- package/dist/playground/assets/index-CVlCAcUx.js +85 -0
- package/dist/playground/index.html +2 -2
- package/package.json +3 -23
- package/src/files-backends/agent-store-presigned-url.ts +9 -401
- package/src/files-backends/cursor-hosted.ts +32 -6
- package/src/internal/cursor-relay-core.ts +62 -3
- package/src/internal/review-comments.ts +102 -2
- package/src/internal/server.ts +9 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.CnFFl07y.js +0 -1
- package/dist/playground/assets/index-50PKeJlG.css +0 -1
- package/dist/playground/assets/index-C0f2Wl1q.js +0 -85
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
10
|
<title>agent-serve playground</title>
|
|
11
|
-
<script type="module" crossorigin src="./assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
11
|
+
<script type="module" crossorigin src="./assets/index-CVlCAcUx.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BRhBbnd4.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cursor/july",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
|
@@ -39,147 +39,126 @@
|
|
|
39
39
|
"exports": {
|
|
40
40
|
".": {
|
|
41
41
|
"anysphere-source": "./src/index.ts",
|
|
42
|
-
"bun": "./src/index.ts",
|
|
43
42
|
"types": "./dist/index.d.ts",
|
|
44
43
|
"import": "./dist/index.js",
|
|
45
44
|
"default": "./dist/index.js"
|
|
46
45
|
},
|
|
47
46
|
"./tools": {
|
|
48
47
|
"anysphere-source": "./src/tools.ts",
|
|
49
|
-
"bun": "./src/tools.ts",
|
|
50
48
|
"types": "./dist/tools.d.ts",
|
|
51
49
|
"import": "./dist/tools.js",
|
|
52
50
|
"default": "./dist/tools.js"
|
|
53
51
|
},
|
|
54
52
|
"./channels": {
|
|
55
53
|
"anysphere-source": "./src/channels.ts",
|
|
56
|
-
"bun": "./src/channels.ts",
|
|
57
54
|
"types": "./dist/channels.d.ts",
|
|
58
55
|
"import": "./dist/channels.js",
|
|
59
56
|
"default": "./dist/channels.js"
|
|
60
57
|
},
|
|
61
58
|
"./channels/slack": {
|
|
62
59
|
"anysphere-source": "./src/channels/slack/index.ts",
|
|
63
|
-
"bun": "./src/channels/slack/index.ts",
|
|
64
60
|
"types": "./dist/channels/slack/index.d.ts",
|
|
65
61
|
"import": "./dist/channels/slack/index.js",
|
|
66
62
|
"default": "./dist/channels/slack/index.js"
|
|
67
63
|
},
|
|
68
64
|
"./channels/github": {
|
|
69
65
|
"anysphere-source": "./src/channels/github/index.ts",
|
|
70
|
-
"bun": "./src/channels/github/index.ts",
|
|
71
66
|
"types": "./dist/channels/github/index.d.ts",
|
|
72
67
|
"import": "./dist/channels/github/index.js",
|
|
73
68
|
"default": "./dist/channels/github/index.js"
|
|
74
69
|
},
|
|
75
70
|
"./skills": {
|
|
76
71
|
"anysphere-source": "./src/skills.ts",
|
|
77
|
-
"bun": "./src/skills.ts",
|
|
78
72
|
"types": "./dist/skills.d.ts",
|
|
79
73
|
"import": "./dist/skills.js",
|
|
80
74
|
"default": "./dist/skills.js"
|
|
81
75
|
},
|
|
82
76
|
"./schedules": {
|
|
83
77
|
"anysphere-source": "./src/schedules.ts",
|
|
84
|
-
"bun": "./src/schedules.ts",
|
|
85
78
|
"types": "./dist/schedules.d.ts",
|
|
86
79
|
"import": "./dist/schedules.js",
|
|
87
80
|
"default": "./dist/schedules.js"
|
|
88
81
|
},
|
|
89
82
|
"./reminders": {
|
|
90
83
|
"anysphere-source": "./src/reminders.ts",
|
|
91
|
-
"bun": "./src/reminders.ts",
|
|
92
84
|
"types": "./dist/reminders.d.ts",
|
|
93
85
|
"import": "./dist/reminders.js",
|
|
94
86
|
"default": "./dist/reminders.js"
|
|
95
87
|
},
|
|
96
88
|
"./connections": {
|
|
97
89
|
"anysphere-source": "./src/connections.ts",
|
|
98
|
-
"bun": "./src/connections.ts",
|
|
99
90
|
"types": "./dist/connections.d.ts",
|
|
100
91
|
"import": "./dist/connections.js",
|
|
101
92
|
"default": "./dist/connections.js"
|
|
102
93
|
},
|
|
103
94
|
"./hooks": {
|
|
104
95
|
"anysphere-source": "./src/hooks.ts",
|
|
105
|
-
"bun": "./src/hooks.ts",
|
|
106
96
|
"types": "./dist/hooks.d.ts",
|
|
107
97
|
"import": "./dist/hooks.js",
|
|
108
98
|
"default": "./dist/hooks.js"
|
|
109
99
|
},
|
|
110
100
|
"./memory": {
|
|
111
101
|
"anysphere-source": "./src/memory.ts",
|
|
112
|
-
"bun": "./src/memory.ts",
|
|
113
102
|
"types": "./dist/memory.d.ts",
|
|
114
103
|
"import": "./dist/memory.js",
|
|
115
104
|
"default": "./dist/memory.js"
|
|
116
105
|
},
|
|
117
106
|
"./prompt": {
|
|
118
107
|
"anysphere-source": "./src/prompt.ts",
|
|
119
|
-
"bun": "./src/prompt.ts",
|
|
120
108
|
"types": "./dist/prompt.d.ts",
|
|
121
109
|
"import": "./dist/prompt.js",
|
|
122
110
|
"default": "./dist/prompt.js"
|
|
123
111
|
},
|
|
124
112
|
"./evals": {
|
|
125
113
|
"anysphere-source": "./src/evals.ts",
|
|
126
|
-
"bun": "./src/evals.ts",
|
|
127
114
|
"types": "./dist/evals.d.ts",
|
|
128
115
|
"import": "./dist/evals.js",
|
|
129
116
|
"default": "./dist/evals.js"
|
|
130
117
|
},
|
|
131
118
|
"./evals/reporters": {
|
|
132
119
|
"anysphere-source": "./src/evals/reporters.ts",
|
|
133
|
-
"bun": "./src/evals/reporters.ts",
|
|
134
120
|
"types": "./dist/evals/reporters.d.ts",
|
|
135
121
|
"import": "./dist/evals/reporters.js",
|
|
136
122
|
"default": "./dist/evals/reporters.js"
|
|
137
123
|
},
|
|
138
124
|
"./evals/loaders": {
|
|
139
125
|
"anysphere-source": "./src/evals/loaders.ts",
|
|
140
|
-
"bun": "./src/evals/loaders.ts",
|
|
141
126
|
"types": "./dist/evals/loaders.d.ts",
|
|
142
127
|
"import": "./dist/evals/loaders.js",
|
|
143
128
|
"default": "./dist/evals/loaders.js"
|
|
144
129
|
},
|
|
145
130
|
"./ab": {
|
|
146
131
|
"anysphere-source": "./src/ab.ts",
|
|
147
|
-
"bun": "./src/ab.ts",
|
|
148
132
|
"types": "./dist/ab.d.ts",
|
|
149
133
|
"import": "./dist/ab.js",
|
|
150
134
|
"default": "./dist/ab.js"
|
|
151
135
|
},
|
|
152
136
|
"./artifacts": {
|
|
153
137
|
"anysphere-source": "./src/artifacts.ts",
|
|
154
|
-
"bun": "./src/artifacts.ts",
|
|
155
138
|
"types": "./dist/artifacts.d.ts",
|
|
156
139
|
"import": "./dist/artifacts.js",
|
|
157
140
|
"default": "./dist/artifacts.js"
|
|
158
141
|
},
|
|
159
142
|
"./storage": {
|
|
160
143
|
"anysphere-source": "./src/storage.ts",
|
|
161
|
-
"bun": "./src/storage.ts",
|
|
162
144
|
"types": "./dist/storage.d.ts",
|
|
163
145
|
"import": "./dist/storage.js",
|
|
164
146
|
"default": "./dist/storage.js"
|
|
165
147
|
},
|
|
166
148
|
"./storage/file-kv": {
|
|
167
149
|
"anysphere-source": "./src/storage-backends/file-kv.ts",
|
|
168
|
-
"bun": "./src/storage-backends/file-kv.ts",
|
|
169
150
|
"types": "./dist/storage-backends/file-kv.d.ts",
|
|
170
151
|
"import": "./dist/storage-backends/file-kv.js",
|
|
171
152
|
"default": "./dist/storage-backends/file-kv.js"
|
|
172
153
|
},
|
|
173
154
|
"./storage/postgres-kv": {
|
|
174
155
|
"anysphere-source": "./src/storage-backends/postgres-kv.ts",
|
|
175
|
-
"bun": "./src/storage-backends/postgres-kv.ts",
|
|
176
156
|
"types": "./dist/storage-backends/postgres-kv.d.ts",
|
|
177
157
|
"import": "./dist/storage-backends/postgres-kv.js",
|
|
178
158
|
"default": "./dist/storage-backends/postgres-kv.js"
|
|
179
159
|
},
|
|
180
160
|
"./storage/cursor-hosted": {
|
|
181
161
|
"anysphere-source": "./src/storage-backends/cursor-hosted.ts",
|
|
182
|
-
"bun": "./src/storage-backends/cursor-hosted.ts",
|
|
183
162
|
"types": "./dist/storage-backends/cursor-hosted.d.ts",
|
|
184
163
|
"import": "./dist/storage-backends/cursor-hosted.js",
|
|
185
164
|
"default": "./dist/storage-backends/cursor-hosted.js"
|
|
@@ -193,7 +172,7 @@
|
|
|
193
172
|
}
|
|
194
173
|
},
|
|
195
174
|
"scripts": {
|
|
196
|
-
"build": "tsgo -p tsconfig.build.json && pnpm run build:playground && pnpm run docs:build",
|
|
175
|
+
"build": "tsgo -p tsconfig.build.json && node scripts/bundle-publish-shims.mjs && pnpm run build:playground && pnpm run docs:build",
|
|
197
176
|
"build:playground": "vite build --config playground/vite.config.ts",
|
|
198
177
|
"build:playground:central": "vite build --mode central --config playground/vite.config.ts",
|
|
199
178
|
"dev:playground": "vite --config playground/vite.config.ts",
|
|
@@ -229,6 +208,7 @@
|
|
|
229
208
|
"zod-to-json-schema": "^3.24.5"
|
|
230
209
|
},
|
|
231
210
|
"devDependencies": {
|
|
211
|
+
"@anysphere/agent-store-sync": "workspace:*",
|
|
232
212
|
"@anysphere/ui": "workspace:*",
|
|
233
213
|
"@base-ui/react": "1.6.0",
|
|
234
214
|
"@stylexjs/stylex": "0.18.3",
|
|
@@ -1,402 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export type PresignedUrlValidator = (url: URL) => void;
|
|
13
|
-
|
|
14
|
-
export type PresignedUrlRejectionCode =
|
|
15
|
-
| "scheme_not_allowed"
|
|
16
|
-
| "userinfo_not_allowed"
|
|
17
|
-
| "host_not_allowed"
|
|
18
|
-
| "private_host_not_allowed";
|
|
19
|
-
|
|
20
|
-
export interface PresignedUrlRejectionOptions {
|
|
21
|
-
code: PresignedUrlRejectionCode;
|
|
22
|
-
message: string;
|
|
23
|
-
url?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class PresignedUrlRejectedError extends Error {
|
|
27
|
-
public readonly code: PresignedUrlRejectionCode;
|
|
28
|
-
/** The host that triggered the rejection (never the full URL). */
|
|
29
|
-
public readonly url: string | undefined;
|
|
30
|
-
|
|
31
|
-
constructor(options: PresignedUrlRejectionOptions) {
|
|
32
|
-
super(options.message);
|
|
33
|
-
this.name = "PresignedUrlRejectedError";
|
|
34
|
-
this.code = options.code;
|
|
35
|
-
this.url = options.url;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Strict default: accepts `https:` URLs to an AWS S3 endpoint with no
|
|
41
|
-
* userinfo and no private/internal address. Rejects DNS hosts that
|
|
42
|
-
* resolve into the private IP space and IPv4-mapped IPv6 literals.
|
|
43
|
-
*/
|
|
44
|
-
export function defaultPresignedUrlValidator(url: URL): void {
|
|
45
|
-
assertSafeAuthority(url);
|
|
46
|
-
if (url.protocol !== "https:") {
|
|
47
|
-
throw new PresignedUrlRejectedError({
|
|
48
|
-
code: "scheme_not_allowed",
|
|
49
|
-
message: `Refused presigned URL with non-https scheme: ${url.protocol}`,
|
|
50
|
-
url: redactPresignedUrlString(url),
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
const hostname = stripIPv6Brackets(url.hostname);
|
|
54
|
-
if (isLocalOrInternalHost(hostname)) {
|
|
55
|
-
throw new PresignedUrlRejectedError({
|
|
56
|
-
code: "private_host_not_allowed",
|
|
57
|
-
message: `Refused https presigned URL with private/internal host: ${hostname}`,
|
|
58
|
-
url: redactPresignedUrlString(url),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
if (!isAwsS3Hostname(hostname)) {
|
|
62
|
-
throw new PresignedUrlRejectedError({
|
|
63
|
-
code: "host_not_allowed",
|
|
64
|
-
message: `Refused presigned URL whose host is not an AWS S3 endpoint: ${hostname}`,
|
|
65
|
-
url: redactPresignedUrlString(url),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* True when `hostname` is an AWS S3 object-storage endpoint:
|
|
72
|
-
* `*.amazonaws.com` with at least one label in
|
|
73
|
-
* {@link S3_OBJECT_LABELS}. Covers modern virtual-host, path-style,
|
|
74
|
-
* dualstack, accelerate, and FIPS URLs. Rejects the S3-named control
|
|
75
|
-
* planes (`s3-control`, `s3-outposts`, `s3-object-lambda`) and the
|
|
76
|
-
* legacy regional virtual-host form AWS deprecated in 2019, both of
|
|
77
|
-
* which would otherwise let a tampered presign issuer turn the sync
|
|
78
|
-
* client into a signed-request gadget against non-storage APIs.
|
|
79
|
-
*/
|
|
80
|
-
export function isAwsS3Hostname(hostname: string): boolean {
|
|
81
|
-
const lowered = hostname.toLowerCase();
|
|
82
|
-
if (lowered !== "amazonaws.com" && !lowered.endsWith(".amazonaws.com")) {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
const labels = lowered.split(".");
|
|
86
|
-
return labels.some(label => S3_OBJECT_LABELS.has(label));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const S3_OBJECT_LABELS = new Set(["s3", "s3-accelerate", "s3-fips"]);
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Like {@link defaultPresignedUrlValidator} plus a case-insensitive
|
|
93
|
-
* exact-match check against `allowedHosts` (no subdomain wildcards).
|
|
94
|
-
*/
|
|
95
|
-
export function createHostAllowlistValidator(
|
|
96
|
-
allowedHosts: readonly string[]
|
|
97
|
-
): PresignedUrlValidator {
|
|
98
|
-
const allowed = new Set(allowedHosts.map(host => host.toLowerCase()));
|
|
99
|
-
return (url: URL): void => {
|
|
100
|
-
defaultPresignedUrlValidator(url);
|
|
101
|
-
if (!allowed.has(url.hostname.toLowerCase())) {
|
|
102
|
-
throw new PresignedUrlRejectedError({
|
|
103
|
-
code: "host_not_allowed",
|
|
104
|
-
message: `Refused presigned URL whose host is not in the allowlist: ${url.hostname}`,
|
|
105
|
-
url: redactPresignedUrlString(url),
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Virtual-host S3 endpoints for the production `agent-stores` bucket
|
|
113
|
-
* (matches backend `getAgentStoreBucket()` in
|
|
114
|
-
* `backend/server/src/agent-stores/utils/agentStoreS3.ts`, which presigns
|
|
115
|
-
* with a `us-east-1` `S3Client` and the SDK's default virtual-host
|
|
116
|
-
* addressing). Both the regional form emitted by modern AWS SDK v3 and
|
|
117
|
-
* the legacy global form are listed so a presign issuer upgrade or
|
|
118
|
-
* downgrade can't trip the allowlist. Path-style (`s3.amazonaws.com/agent-stores/…`)
|
|
119
|
-
* is intentionally not listed: the server never sets `forcePathStyle`,
|
|
120
|
-
* and admitting bare `s3.amazonaws.com` would re-open the "any bucket"
|
|
121
|
-
* gap this allowlist exists to close.
|
|
122
|
-
*/
|
|
123
|
-
export const BCS_AGENT_STORE_BUCKET_HOSTS: readonly string[] = [
|
|
124
|
-
"agent-stores.s3.us-east-1.amazonaws.com",
|
|
125
|
-
"agent-stores.s3.amazonaws.com",
|
|
126
|
-
];
|
|
127
|
-
|
|
128
|
-
const PLAYGROUND_AGENT_STORE_BUCKET_HOSTS: readonly string[] = [
|
|
129
|
-
"agent-stores-928182716709-us-west-2-an.s3.us-west-2.amazonaws.com",
|
|
130
|
-
"agent-stores-928182716709-us-west-2-an.s3.amazonaws.com",
|
|
131
|
-
];
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Production validator for the BCS agent-store sync transport: strict
|
|
135
|
-
* default (https, S3 endpoint, no private hosts, no userinfo) plus the
|
|
136
|
-
* {@link BCS_AGENT_STORE_BUCKET_HOSTS} pin. Pre-built so callers don't
|
|
137
|
-
* accidentally pass {@link defaultPresignedUrlValidator}, which would
|
|
138
|
-
* accept any AWS S3 bucket on the internet and let a buggy or
|
|
139
|
-
* compromised presign issuer redirect sync traffic away from the real
|
|
140
|
-
* `agent-stores` bucket.
|
|
141
|
-
*/
|
|
142
|
-
export function createBcsPresignedUrlValidator(): PresignedUrlValidator {
|
|
143
|
-
return createHostAllowlistValidator(BCS_AGENT_STORE_BUCKET_HOSTS);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* True when `backendUrl` targets a local dev backend (localhost or
|
|
148
|
-
* lclhst.build). Matches IDE checks in `cursor-always-local` /
|
|
149
|
-
* `cursorCredsService`.
|
|
150
|
-
*/
|
|
151
|
-
export function isLocalAgentStoreBackendUrl(backendUrl: string): boolean {
|
|
152
|
-
const trimmed = backendUrl.trim();
|
|
153
|
-
if (trimmed === "") {
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
return trimmed.includes("localhost") || trimmed.includes("lclhst.build");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function isPlaygroundAgentStoreBackendUrl(backendUrl: string): boolean {
|
|
160
|
-
try {
|
|
161
|
-
const hostname = new URL(backendUrl).hostname.toLowerCase();
|
|
162
|
-
return (
|
|
163
|
-
hostname === "playground.cursor.sh" ||
|
|
164
|
-
hostname.endsWith(".playground.cursor.sh")
|
|
165
|
-
);
|
|
166
|
-
} catch {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Presign validator for the sync client. Production/staging and playground
|
|
173
|
-
* backends are pinned to their respective agent-store buckets. For a local dev API (see
|
|
174
|
-
* {@link isLocalAgentStoreBackendUrl}), allow `http:` to loopback only
|
|
175
|
-
* (LocalStack) and still pin all other URLs to {@link BCS_AGENT_STORE_BUCKET_HOSTS}.
|
|
176
|
-
*/
|
|
177
|
-
export function createAgentStorePresignedUrlValidatorForBackend(
|
|
178
|
-
backendUrl: string
|
|
179
|
-
): PresignedUrlValidator {
|
|
180
|
-
if (isLocalAgentStoreBackendUrl(backendUrl)) {
|
|
181
|
-
return createLocalDevAgentStorePresignedUrlValidator();
|
|
182
|
-
}
|
|
183
|
-
if (isPlaygroundAgentStoreBackendUrl(backendUrl)) {
|
|
184
|
-
return createHostAllowlistValidator(PLAYGROUND_AGENT_STORE_BUCKET_HOSTS);
|
|
185
|
-
}
|
|
186
|
-
return createBcsPresignedUrlValidator();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function createLocalDevAgentStorePresignedUrlValidator(): PresignedUrlValidator {
|
|
190
|
-
const bcsValidator = createBcsPresignedUrlValidator();
|
|
191
|
-
return (url: URL): void => {
|
|
192
|
-
assertSafeAuthority(url);
|
|
193
|
-
const hostname = stripIPv6Brackets(url.hostname);
|
|
194
|
-
if (url.protocol === "http:" && isLoopbackHostName(hostname)) {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
bcsValidator(url);
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Like {@link defaultPresignedUrlValidator} plus `http://localhost` /
|
|
203
|
-
* `127.0.0.1` / `[::1]` (any port) for LocalStack-backed unit tests.
|
|
204
|
-
* Non-loopback `https:` URLs fall through to the strict default (any S3
|
|
205
|
-
* bucket). Do not use for IDE wiring — use
|
|
206
|
-
* {@link createAgentStorePresignedUrlValidatorForBackend} instead.
|
|
207
|
-
*/
|
|
208
|
-
export function localhostPermissivePresignedUrlValidator(url: URL): void {
|
|
209
|
-
assertSafeAuthority(url);
|
|
210
|
-
const hostname = stripIPv6Brackets(url.hostname);
|
|
211
|
-
if (url.protocol === "http:" && isLoopbackHostName(hostname)) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
defaultPresignedUrlValidator(url);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function assertSafeAuthority(url: URL): void {
|
|
218
|
-
if (url.username !== "" || url.password !== "") {
|
|
219
|
-
throw new PresignedUrlRejectedError({
|
|
220
|
-
code: "userinfo_not_allowed",
|
|
221
|
-
message: "Refused presigned URL with embedded userinfo",
|
|
222
|
-
url: redactPresignedUrlString(url),
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Strip the query string and userinfo from a presigned URL so the AWS
|
|
229
|
-
* SigV4 signature never reaches error messages or telemetry.
|
|
230
|
-
*/
|
|
231
|
-
export function redactPresignedUrlString(url: URL | string): string {
|
|
232
|
-
try {
|
|
233
|
-
const parsed = typeof url === "string" ? new URL(url) : url;
|
|
234
|
-
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
235
|
-
} catch {
|
|
236
|
-
return "<unparseable-presigned-url>";
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Parse and validate a server-returned presigned URL before fetch. Uses
|
|
242
|
-
* {@link relPath} in error text (never the raw URL) so SigV4 credentials
|
|
243
|
-
* cannot leak into messages.
|
|
244
|
-
*/
|
|
245
|
-
export function assertPresignedUrlSafe(args: {
|
|
246
|
-
readonly rawUrl: string;
|
|
247
|
-
readonly relPath: string;
|
|
248
|
-
readonly validatePresignedUrl: PresignedUrlValidator;
|
|
249
|
-
}): void {
|
|
250
|
-
let parsed: URL;
|
|
251
|
-
try {
|
|
252
|
-
parsed = new URL(args.rawUrl);
|
|
253
|
-
} catch {
|
|
254
|
-
throw new Error(`Refusing unparseable presigned URL for ${args.relPath}`);
|
|
255
|
-
}
|
|
256
|
-
try {
|
|
257
|
-
args.validatePresignedUrl(parsed);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
throw new Error(
|
|
260
|
-
`Refusing presigned URL for ${args.relPath}: ${
|
|
261
|
-
error instanceof Error ? error.message : String(error)
|
|
262
|
-
}`,
|
|
263
|
-
{ cause: error instanceof Error ? error : undefined }
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function isLoopbackHostName(hostname: string): boolean {
|
|
269
|
-
const lowered = hostname.toLowerCase();
|
|
270
|
-
return (
|
|
271
|
-
lowered === "localhost" ||
|
|
272
|
-
lowered === "ip6-localhost" ||
|
|
273
|
-
lowered === "127.0.0.1" ||
|
|
274
|
-
lowered === "::1" ||
|
|
275
|
-
lowered === "0:0:0:0:0:0:0:1"
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function isLocalOrInternalHost(hostname: string): boolean {
|
|
280
|
-
if (hostname.length === 0) {
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
if (isLoopbackHostName(hostname)) {
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
if (isIPv4Address(hostname)) {
|
|
287
|
-
return isPrivateOrInternalIPv4(hostname);
|
|
288
|
-
}
|
|
289
|
-
if (isIPv6Address(hostname)) {
|
|
290
|
-
return isPrivateOrInternalIPv6(hostname);
|
|
291
|
-
}
|
|
292
|
-
// DNS hostnames can't be classified without resolving; the caller
|
|
293
|
-
// relies on transport-level allowlists for those.
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function stripIPv6Brackets(hostname: string): string {
|
|
298
|
-
return hostname.startsWith("[") && hostname.endsWith("]")
|
|
299
|
-
? hostname.slice(1, -1)
|
|
300
|
-
: hostname;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function isIPv4Address(hostname: string): boolean {
|
|
304
|
-
const parts = hostname.split(".");
|
|
305
|
-
if (parts.length !== 4) {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
return parts.every(part => {
|
|
309
|
-
if (part.length === 0 || part.length > 3) {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
if (!/^\d+$/.test(part)) {
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
const n = Number(part);
|
|
316
|
-
return Number.isInteger(n) && n >= 0 && n <= 255;
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function isPrivateOrInternalIPv4(hostname: string): boolean {
|
|
321
|
-
const [a, b] = hostname.split(".").map(Number);
|
|
322
|
-
if (a === undefined || b === undefined) {
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
// RFC1918, loopback, link-local, multicast, unspecified, broadcast,
|
|
326
|
-
// carrier-grade NAT, and reserved blocks.
|
|
327
|
-
if (a === 0) return true;
|
|
328
|
-
if (a === 10) return true;
|
|
329
|
-
if (a === 127) return true;
|
|
330
|
-
if (a === 169 && b === 254) return true;
|
|
331
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
332
|
-
if (a === 192 && b === 168) return true;
|
|
333
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
334
|
-
if (a >= 224 && a <= 239) return true;
|
|
335
|
-
if (a >= 240) return true;
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function isIPv6Address(hostname: string): boolean {
|
|
340
|
-
// Loose check — just enough to route the host to the IPv6 classifier.
|
|
341
|
-
return hostname.includes(":");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function isPrivateOrInternalIPv6(hostname: string): boolean {
|
|
345
|
-
const lowered = hostname.toLowerCase();
|
|
346
|
-
if (lowered === "::" || lowered === "::1") {
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
// IPv4-mapped IPv6 (RFC4291) routes back through the IPv4 classifier
|
|
350
|
-
// so `[::ffff:169.254.169.254]` can't slip past the private-IP check.
|
|
351
|
-
const mapped = extractIPv4MappedAddress(lowered);
|
|
352
|
-
if (mapped !== undefined) {
|
|
353
|
-
return isPrivateOrInternalIPv4(mapped);
|
|
354
|
-
}
|
|
355
|
-
// ULA fc00::/7, link-local fe80::/10, multicast ff00::/8.
|
|
356
|
-
if (lowered.startsWith("fc") || lowered.startsWith("fd")) {
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
if (lowered.startsWith("fe8") || lowered.startsWith("fe9")) {
|
|
360
|
-
return true;
|
|
361
|
-
}
|
|
362
|
-
if (lowered.startsWith("fea") || lowered.startsWith("feb")) {
|
|
363
|
-
return true;
|
|
364
|
-
}
|
|
365
|
-
if (lowered.startsWith("ff")) {
|
|
366
|
-
return true;
|
|
367
|
-
}
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Convert an IPv4-mapped IPv6 literal (`::ffff:a.b.c.d` or
|
|
372
|
-
// `::ffff:hhhh:hhhh`) to its dotted-quad form. Returns undefined otherwise.
|
|
373
|
-
function extractIPv4MappedAddress(hostname: string): string | undefined {
|
|
374
|
-
const dotted = hostname.match(
|
|
375
|
-
/^(?:0:0:0:0:0|::):?ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i
|
|
376
|
-
);
|
|
377
|
-
if (dotted) {
|
|
378
|
-
return dotted[1];
|
|
379
|
-
}
|
|
380
|
-
if (hostname.startsWith("::ffff:")) {
|
|
381
|
-
const tail = hostname.slice("::ffff:".length);
|
|
382
|
-
if (isIPv4Address(tail)) {
|
|
383
|
-
return tail;
|
|
384
|
-
}
|
|
385
|
-
const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
386
|
-
if (hex) {
|
|
387
|
-
const high = Number.parseInt(hex[1], 16);
|
|
388
|
-
const low = Number.parseInt(hex[2], 16);
|
|
389
|
-
if (
|
|
390
|
-
Number.isFinite(high) &&
|
|
391
|
-
Number.isFinite(low) &&
|
|
392
|
-
high >= 0 &&
|
|
393
|
-
high <= 0xffff &&
|
|
394
|
-
low >= 0 &&
|
|
395
|
-
low <= 0xffff
|
|
396
|
-
) {
|
|
397
|
-
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return undefined;
|
|
402
|
-
}
|
|
2
|
+
* Re-export bundled into dist by `scripts/bundle-publish-shims.mjs`.
|
|
3
|
+
*
|
|
4
|
+
* Imports the `presigned-url` entry point rather than the package root: the
|
|
5
|
+
* root is a barrel over the whole library, and bundling through it inlines
|
|
6
|
+
* every module the barrel can reach (sync engine, local sqlite index, secure
|
|
7
|
+
* open) into the published tarball. This module has no imports of its own, so
|
|
8
|
+
* the bundle stays limited to the validator.
|
|
9
|
+
*/
|
|
10
|
+
export { createAgentStorePresignedUrlValidatorForBackend } from "@anysphere/agent-store-sync/presigned-url";
|
|
@@ -437,11 +437,22 @@ export function cursorHostedFiles(
|
|
|
437
437
|
|
|
438
438
|
async get(key): Promise<FileSinkBody | undefined> {
|
|
439
439
|
return await withFiles(async (files) => {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
440
|
+
// Presigning a read of a path the store has no live object for — never
|
|
441
|
+
// written, or deleted and now holding a tombstone — is a 404. That is
|
|
442
|
+
// absence, which `read()` reports as undefined, not a failure.
|
|
443
|
+
let presigned: ApiPresignResponse;
|
|
444
|
+
try {
|
|
445
|
+
presigned = await storePost<ApiPresignResponse>(
|
|
446
|
+
options,
|
|
447
|
+
files.path("/download"),
|
|
448
|
+
{ paths: [key] }
|
|
449
|
+
);
|
|
450
|
+
} catch (error) {
|
|
451
|
+
if (error instanceof StoreApiError && error.status === 404) {
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
445
456
|
const url = presigned.files?.[0]?.url;
|
|
446
457
|
if (url === undefined) {
|
|
447
458
|
return undefined;
|
|
@@ -661,13 +672,28 @@ async function sendJson<T>(
|
|
|
661
672
|
);
|
|
662
673
|
}
|
|
663
674
|
if (!response.ok) {
|
|
664
|
-
throw new
|
|
675
|
+
throw new StoreApiError(
|
|
676
|
+
response.status,
|
|
665
677
|
`cursorHostedFiles ${url}: ${describeError(parsed, response.status)}`
|
|
666
678
|
);
|
|
667
679
|
}
|
|
668
680
|
return parsed as T;
|
|
669
681
|
}
|
|
670
682
|
|
|
683
|
+
/**
|
|
684
|
+
* A non-2xx answer from the files API, carrying the status so a caller can
|
|
685
|
+
* tell "absent" apart from a failure it must not swallow.
|
|
686
|
+
*/
|
|
687
|
+
class StoreApiError extends Error {
|
|
688
|
+
constructor(
|
|
689
|
+
readonly status: number,
|
|
690
|
+
message: string
|
|
691
|
+
) {
|
|
692
|
+
super(message);
|
|
693
|
+
this.name = "StoreApiError";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
671
697
|
function describeError(parsed: unknown, status: number): string {
|
|
672
698
|
if (
|
|
673
699
|
parsed !== null &&
|