Skip to content

Commit 8988850

Browse files
committed
Addressed BugWatch review comments on consumer JWT support
1 parent 4fcbf6d commit 8988850

4 files changed

Lines changed: 71 additions & 15 deletions

File tree

js/README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ import NeetoJWT from "neeto-jwt";
6262

6363
const neetoJWT = new NeetoJWT({
6464
email: "consumer@example.com",
65-
workspace: "app",
6665
privateKey: "<your-private-key>",
6766
scope: "consumer",
6867
});
@@ -73,6 +72,12 @@ const loginUrl = neetoJWT.generateLoginUrl(
7372
// => https://app.neetoauth.com/consumers/auth/jwt?...
7473
```
7574

75+
When `scope: "consumer"` is set and `workspace` is omitted, the client defaults
76+
to `"app"` (the only workspace consumers belong to in NeetoAuth) instead of
77+
reading `NEETO_JWT_WORKSPACE`. This avoids accidentally pointing the consumer
78+
flow at a tenant-specific workspace if you've already configured the env var
79+
for the user-scope flow.
80+
7681
#### Identity is asserted, not pre-required
7782

7883
Unlike user-scope JWT (where the email must already be invited to the
@@ -92,9 +97,10 @@ be a Neeto subdomain — any URL the partner controls works.
9297
### Options
9398

9499
- `email` (string, required): The user's email address.
95-
- `workspace` (string, optional): The Neeto workspace. Defaults to the
96-
NEETO_JWT_WORKSPACE environment variable. For consumer scope, set this to
97-
`"app"`.
100+
- `workspace` (string, optional): The Neeto workspace. For user scope, defaults
101+
to the `NEETO_JWT_WORKSPACE` environment variable. For consumer scope, defaults
102+
to `"app"` (env var ignored) since all consumers live under that workspace.
103+
Pass an explicit value only if you need to override (e.g. staging tests).
98104
- `privateKey` (string, optional): The private key used to sign the JWT.
99105
Defaults to the NEETO_JWT_PRIVATE_KEY environment variable.
100106
- `scope` (string, optional): `"user"` (default) or `"consumer"`. Determines

js/src/index.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import jwt from "jsonwebtoken";
2-
import { Scope } from "./types.js";
2+
import type { Scope } from "./types.js";
33
import {
44
getClientAppName,
55
getLoginUri,
@@ -14,19 +14,23 @@ interface Options {
1414
scope?: Scope;
1515
}
1616

17+
const CONSUMER_WORKSPACE = "app";
18+
1719
class NeetoJWT {
1820
private email: string;
1921
private workspace: string;
2022
private privateKey: string;
2123
private scope: Scope;
2224

2325
constructor(options: Options) {
24-
const {
25-
email,
26-
workspace = process.env.NEETO_JWT_WORKSPACE,
27-
privateKey = process.env.NEETO_JWT_PRIVATE_KEY,
28-
scope = "user",
29-
} = options || {};
26+
const { email, privateKey = process.env.NEETO_JWT_PRIVATE_KEY } =
27+
options || {};
28+
const scope: Scope = options?.scope ?? "user";
29+
const workspace =
30+
options?.workspace ??
31+
(scope === "consumer"
32+
? CONSUMER_WORKSPACE
33+
: process.env.NEETO_JWT_WORKSPACE);
3034

3135
if (!email) throw new Error("Email is required.");
3236
if (!workspace) throw new Error("Workspace is required.");
@@ -69,11 +73,11 @@ class NeetoJWT {
6973

7074
// User scope assumes the redirectUri points at a Neeto sub-app, so the
7175
// shared NeetoAuth flow strips the leading subdomain. Consumer scope is
72-
// for arbitrary partner domains — we pass the URI through verbatim so
73-
// NeetoAuth can redirect back to the partner correctly.
76+
// for arbitrary partner domains — pass the URI through verbatim and let
77+
// URLSearchParams handle encoding.
7478
const redirect_uri =
7579
this.scope === "consumer"
76-
? encodeURI(redirectUri)
80+
? redirectUri
7781
: getRedirectUri(redirectUri);
7882

7983
const searchParams: SearchParams = {

js/src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
TLD,
77
USER_LOGIN_PATH,
88
} from "./constants.js";
9-
import { Scope } from "./types.js";
9+
import type { Scope } from "./types.js";
1010

1111
export type SearchParams = {
1212
jwt: string;

js/test/index.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,50 @@ describe("NeetoJWT", () => {
123123
})
124124
).toThrow("Scope must be either 'user' or 'consumer'.");
125125
});
126+
127+
it("should default consumer-scope workspace to 'app' when omitted, ignoring NEETO_JWT_WORKSPACE", () => {
128+
const previous = process.env.NEETO_JWT_WORKSPACE;
129+
process.env.NEETO_JWT_WORKSPACE = "tenant1";
130+
try {
131+
const neetoJWT = new NeetoJWT({ email, privateKey, scope: "consumer" });
132+
const loginUrl = neetoJWT.generateLoginUrl(
133+
"http://partner.example.com/post-login"
134+
);
135+
expect(loginUrl).toContain("https://app.neetoauth.com/consumers/auth/jwt");
136+
expect(loginUrl).not.toContain("tenant1");
137+
} finally {
138+
process.env.NEETO_JWT_WORKSPACE = previous;
139+
}
140+
});
141+
142+
it("should honour an explicit consumer-scope workspace override", () => {
143+
const neetoJWT = new NeetoJWT({
144+
email,
145+
privateKey,
146+
workspace: "staging-app",
147+
scope: "consumer",
148+
});
149+
const loginUrl = neetoJWT.generateLoginUrl("http://partner.example.com/cb");
150+
expect(loginUrl).toContain(
151+
"https://staging-app.neetoauth.com/consumers/auth/jwt"
152+
);
153+
});
154+
155+
it("should not double-encode the consumer redirect URI", () => {
156+
const neetoJWT = new NeetoJWT({
157+
email,
158+
workspace: "app",
159+
privateKey,
160+
scope: "consumer",
161+
});
162+
const loginUrl = neetoJWT.generateLoginUrl(
163+
"http://partner.example.com/path with space?q=1"
164+
);
165+
// URLSearchParams encodes a space as `+`, never as `%2520`.
166+
expect(loginUrl).not.toContain("%2520");
167+
const params = new URL(loginUrl).searchParams;
168+
expect(params.get("redirect_uri")).toBe(
169+
"http://partner.example.com/path with space?q=1"
170+
);
171+
});
126172
});

0 commit comments

Comments
 (0)