@specific.dev/spectest 0.41.0 → 0.44.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/dist/browser.d.ts +21 -8
- package/dist/browser.js +78 -36
- package/dist/components/apple.d.ts +47 -0
- package/dist/components/apple.js +59 -0
- package/dist/components/emulate/service.d.ts +126 -0
- package/dist/components/emulate/service.js +153 -0
- package/dist/components/github.d.ts +52 -0
- package/dist/components/github.js +90 -0
- package/dist/components/google.d.ts +57 -0
- package/dist/components/google.js +92 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +5 -0
- package/dist/components/microsoft.d.ts +55 -0
- package/dist/components/microsoft.js +76 -0
- package/dist/components/okta.d.ts +51 -0
- package/dist/components/okta.js +78 -0
- package/dist/daemon.js +76 -46
- package/dist/index.d.ts +55 -13
- package/dist/mobile.d.ts +9 -5
- package/dist/mobile.js +7 -6
- package/dist/recorder.d.ts +10 -0
- package/package.json +1 -1
- package/src/browser.ts +88 -33
- package/src/components/apple.ts +65 -0
- package/src/components/emulate/entry.mjs +244 -0
- package/src/components/emulate/service.ts +235 -0
- package/src/components/github.ts +96 -0
- package/src/components/google.ts +104 -0
- package/src/components/index.ts +14 -0
- package/src/components/microsoft.ts +89 -0
- package/src/components/okta.ts +93 -0
- package/src/daemon.ts +87 -39
- package/src/index.ts +55 -13
- package/src/mobile.ts +10 -5
- package/src/recorder.ts +10 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ProviderOptions } from "./emulate/service.js";
|
|
2
|
+
export type GithubOptions = ProviderOptions;
|
|
3
|
+
/**
|
|
4
|
+
* GitHub, answering at its real endpoints. Drop into
|
|
5
|
+
* `environment.services`:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { github } from "@specific.dev/spectest/components";
|
|
9
|
+
*
|
|
10
|
+
* services: {
|
|
11
|
+
* github: github({
|
|
12
|
+
* users: [{ email: "bob@example.com", name: "Bob Example", login: "bob" }],
|
|
13
|
+
* client: {
|
|
14
|
+
* clientId: "Iv1.example",
|
|
15
|
+
* clientSecret: "ghs_example",
|
|
16
|
+
* redirectUris: ["https://app.test/callback/github"],
|
|
17
|
+
* },
|
|
18
|
+
* }),
|
|
19
|
+
* app: { …, dependsOn: ["github"] },
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The app keeps its production configuration — it sends a browser to
|
|
24
|
+
* `https://github.com/login/oauth/authorize`, exchanges the code at
|
|
25
|
+
* `https://github.com/login/oauth/access_token`, and reads the account from
|
|
26
|
+
* `https://api.github.com/user`.
|
|
27
|
+
*
|
|
28
|
+
* The account's test id on the picker page is its **email**, even though
|
|
29
|
+
* GitHub identifies it by login — so a test reads the same whichever
|
|
30
|
+
* provider it drives.
|
|
31
|
+
*/
|
|
32
|
+
export declare function github(opts?: GithubOptions): {
|
|
33
|
+
image: {
|
|
34
|
+
type: "dockerfile";
|
|
35
|
+
content: string;
|
|
36
|
+
};
|
|
37
|
+
command: string;
|
|
38
|
+
files: {
|
|
39
|
+
path: string;
|
|
40
|
+
content: string;
|
|
41
|
+
}[];
|
|
42
|
+
env: {
|
|
43
|
+
SPECTEST_AUTH_CONFIG: string;
|
|
44
|
+
};
|
|
45
|
+
ports: number[];
|
|
46
|
+
readyCheck: {
|
|
47
|
+
type: "http";
|
|
48
|
+
port: number;
|
|
49
|
+
path: string;
|
|
50
|
+
timeoutSecs: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// `github()` — GitHub, emulated inside the environment, answering at its
|
|
2
|
+
// real endpoints.
|
|
3
|
+
//
|
|
4
|
+
// Today that is Sign in with GitHub — the authorize page, the token
|
|
5
|
+
// exchange, and the profile reads every GitHub sign-in makes (`/user`,
|
|
6
|
+
// `/user/emails`). The component is named for the provider rather than for
|
|
7
|
+
// the feature: the backing emulator carries most of the REST API — repos,
|
|
8
|
+
// issues, pull requests, checks, webhooks — and reaching it is a matter of
|
|
9
|
+
// what this component chooses to expose, not of new plumbing.
|
|
10
|
+
//
|
|
11
|
+
// Landmine for whoever extends this: the claim is env-wide. `github.com`
|
|
12
|
+
// and `api.github.com` stop reaching the internet for every container in
|
|
13
|
+
// the environment, so anything else that talks to GitHub — a build step
|
|
14
|
+
// that clones a repository, an unrelated API call — reaches the emulator
|
|
15
|
+
// and gets a 404 outside the surface it serves.
|
|
16
|
+
import { emulatorService, localPart, resolveUsers } from "./emulate/service.js";
|
|
17
|
+
function spec(users, opts) {
|
|
18
|
+
const client = opts.client;
|
|
19
|
+
return {
|
|
20
|
+
name: "github",
|
|
21
|
+
module: "@emulators/github",
|
|
22
|
+
pluginExport: "githubPlugin",
|
|
23
|
+
baseUrl: "https://github.com",
|
|
24
|
+
// api.github.com cannot be avoided: every GitHub sign-in reads the
|
|
25
|
+
// profile from /user and the addresses from /user/emails.
|
|
26
|
+
hosts: ["github.com", "api.github.com"],
|
|
27
|
+
// OAuth 2.0, not OIDC — there is no id_token to re-issue and no JWKS.
|
|
28
|
+
oidc: false,
|
|
29
|
+
// GitHub identifies an account by login, not by address.
|
|
30
|
+
aliases: Object.fromEntries(users.map((u) => [u.login ?? localPart(u.email), u.email])),
|
|
31
|
+
fallbackUser: {
|
|
32
|
+
login: users[0].login ?? localPart(users[0].email),
|
|
33
|
+
id: 1,
|
|
34
|
+
scopes: ["user", "user:email"],
|
|
35
|
+
},
|
|
36
|
+
seed: {
|
|
37
|
+
users: users.map((u) => ({
|
|
38
|
+
login: u.login ?? localPart(u.email),
|
|
39
|
+
name: u.name,
|
|
40
|
+
email: u.email,
|
|
41
|
+
})),
|
|
42
|
+
// GitHub calls them oauth_apps, and its own field is `oauth_apps`.
|
|
43
|
+
...(client
|
|
44
|
+
? {
|
|
45
|
+
oauth_apps: [
|
|
46
|
+
{
|
|
47
|
+
client_id: client.clientId,
|
|
48
|
+
client_secret: client.clientSecret,
|
|
49
|
+
name: "spectest",
|
|
50
|
+
redirect_uris: client.redirectUris ?? [],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* GitHub, answering at its real endpoints. Drop into
|
|
60
|
+
* `environment.services`:
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { github } from "@specific.dev/spectest/components";
|
|
64
|
+
*
|
|
65
|
+
* services: {
|
|
66
|
+
* github: github({
|
|
67
|
+
* users: [{ email: "bob@example.com", name: "Bob Example", login: "bob" }],
|
|
68
|
+
* client: {
|
|
69
|
+
* clientId: "Iv1.example",
|
|
70
|
+
* clientSecret: "ghs_example",
|
|
71
|
+
* redirectUris: ["https://app.test/callback/github"],
|
|
72
|
+
* },
|
|
73
|
+
* }),
|
|
74
|
+
* app: { …, dependsOn: ["github"] },
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* The app keeps its production configuration — it sends a browser to
|
|
79
|
+
* `https://github.com/login/oauth/authorize`, exchanges the code at
|
|
80
|
+
* `https://github.com/login/oauth/access_token`, and reads the account from
|
|
81
|
+
* `https://api.github.com/user`.
|
|
82
|
+
*
|
|
83
|
+
* The account's test id on the picker page is its **email**, even though
|
|
84
|
+
* GitHub identifies it by login — so a test reads the same whichever
|
|
85
|
+
* provider it drives.
|
|
86
|
+
*/
|
|
87
|
+
export function github(opts = {}) {
|
|
88
|
+
const users = resolveUsers("github", opts.users);
|
|
89
|
+
return emulatorService(spec(users, opts));
|
|
90
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ProviderOptions } from "./emulate/service.js";
|
|
2
|
+
export type GoogleOptions = ProviderOptions;
|
|
3
|
+
/**
|
|
4
|
+
* Google, answering at its real endpoints. Drop into
|
|
5
|
+
* `environment.services`:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { google } from "@specific.dev/spectest/components";
|
|
9
|
+
*
|
|
10
|
+
* services: {
|
|
11
|
+
* // Accounts default to Alice and Bob Example; declare `users` to
|
|
12
|
+
* // choose your own. A `client` makes the environment validate the
|
|
13
|
+
* // app's client id, secret and callback URL.
|
|
14
|
+
* google: google({
|
|
15
|
+
* client: {
|
|
16
|
+
* clientId: "1234.apps.googleusercontent.com",
|
|
17
|
+
* clientSecret: "GOCSPX-test",
|
|
18
|
+
* redirectUris: ["https://app.test/callback/google"],
|
|
19
|
+
* },
|
|
20
|
+
* }),
|
|
21
|
+
* app: { …, dependsOn: ["google"] },
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* The app keeps its production configuration — it sends a browser to
|
|
26
|
+
* `https://accounts.google.com/o/oauth2/v2/auth`, exchanges the code at
|
|
27
|
+
* `https://oauth2.googleapis.com/token`, and verifies the id_token against
|
|
28
|
+
* `https://www.googleapis.com/oauth2/v3/certs`, exactly as it does live.
|
|
29
|
+
*
|
|
30
|
+
* A test signs in the way a user does, through the browser:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* await page.getByTestId("login-google").click();
|
|
34
|
+
* await page.getByTestId("spectest-user-alice@example.com").click();
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function google(opts?: GoogleOptions): {
|
|
38
|
+
image: {
|
|
39
|
+
type: "dockerfile";
|
|
40
|
+
content: string;
|
|
41
|
+
};
|
|
42
|
+
command: string;
|
|
43
|
+
files: {
|
|
44
|
+
path: string;
|
|
45
|
+
content: string;
|
|
46
|
+
}[];
|
|
47
|
+
env: {
|
|
48
|
+
SPECTEST_AUTH_CONFIG: string;
|
|
49
|
+
};
|
|
50
|
+
ports: number[];
|
|
51
|
+
readyCheck: {
|
|
52
|
+
type: "http";
|
|
53
|
+
port: number;
|
|
54
|
+
path: string;
|
|
55
|
+
timeoutSecs: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// `google()` — Google, emulated inside the environment, answering at its
|
|
2
|
+
// real endpoints.
|
|
3
|
+
//
|
|
4
|
+
// Today that is Sign in with Google: OpenID Connect discovery, the account
|
|
5
|
+
// picker, PKCE, the token exchange, refresh, and userinfo. The component is
|
|
6
|
+
// named for the provider rather than for the feature, because the backing
|
|
7
|
+
// emulator also carries Gmail, Calendar and Drive — claiming
|
|
8
|
+
// `gmail.googleapis.com` and routing it here is a table entry, not a new
|
|
9
|
+
// design.
|
|
10
|
+
import { emulatorService, familyName, givenName, oauthClients, resolveUsers, } from "./emulate/service.js";
|
|
11
|
+
function spec(users, opts) {
|
|
12
|
+
return {
|
|
13
|
+
name: "google",
|
|
14
|
+
module: "@emulators/google",
|
|
15
|
+
pluginExport: "googlePlugin",
|
|
16
|
+
baseUrl: "https://accounts.google.com",
|
|
17
|
+
hosts: [
|
|
18
|
+
"accounts.google.com",
|
|
19
|
+
"oauth2.googleapis.com",
|
|
20
|
+
"www.googleapis.com",
|
|
21
|
+
"openidconnect.googleapis.com",
|
|
22
|
+
],
|
|
23
|
+
discovery: {
|
|
24
|
+
issuer: "https://accounts.google.com",
|
|
25
|
+
authorization_endpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
26
|
+
token_endpoint: "https://oauth2.googleapis.com/token",
|
|
27
|
+
userinfo_endpoint: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
28
|
+
revocation_endpoint: "https://oauth2.googleapis.com/revoke",
|
|
29
|
+
jwks_uri: "https://www.googleapis.com/oauth2/v3/certs",
|
|
30
|
+
},
|
|
31
|
+
jwksPath: "^/oauth2/v3/certs$",
|
|
32
|
+
rewrites: [
|
|
33
|
+
// oauth2.googleapis.com serves these at the root; the emulator puts
|
|
34
|
+
// them under /oauth2.
|
|
35
|
+
{ from: "^/token$", to: "/oauth2/token" },
|
|
36
|
+
{ from: "^/revoke$", to: "/oauth2/revoke" },
|
|
37
|
+
// openidconnect.googleapis.com and the v3 alias both mean userinfo.
|
|
38
|
+
{ from: "^/v1/userinfo$", to: "/oauth2/v2/userinfo" },
|
|
39
|
+
{ from: "^/oauth2/v3/userinfo$", to: "/oauth2/v2/userinfo" },
|
|
40
|
+
],
|
|
41
|
+
fallbackUser: { login: users[0].email, id: 1, scopes: ["openid", "email", "profile"] },
|
|
42
|
+
seed: {
|
|
43
|
+
users: users.map((u) => ({
|
|
44
|
+
email: u.email,
|
|
45
|
+
name: u.name,
|
|
46
|
+
given_name: givenName(u),
|
|
47
|
+
family_name: familyName(u),
|
|
48
|
+
picture: u.picture,
|
|
49
|
+
email_verified: true,
|
|
50
|
+
})),
|
|
51
|
+
...oauthClients(opts.client),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Google, answering at its real endpoints. Drop into
|
|
57
|
+
* `environment.services`:
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { google } from "@specific.dev/spectest/components";
|
|
61
|
+
*
|
|
62
|
+
* services: {
|
|
63
|
+
* // Accounts default to Alice and Bob Example; declare `users` to
|
|
64
|
+
* // choose your own. A `client` makes the environment validate the
|
|
65
|
+
* // app's client id, secret and callback URL.
|
|
66
|
+
* google: google({
|
|
67
|
+
* client: {
|
|
68
|
+
* clientId: "1234.apps.googleusercontent.com",
|
|
69
|
+
* clientSecret: "GOCSPX-test",
|
|
70
|
+
* redirectUris: ["https://app.test/callback/google"],
|
|
71
|
+
* },
|
|
72
|
+
* }),
|
|
73
|
+
* app: { …, dependsOn: ["google"] },
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* The app keeps its production configuration — it sends a browser to
|
|
78
|
+
* `https://accounts.google.com/o/oauth2/v2/auth`, exchanges the code at
|
|
79
|
+
* `https://oauth2.googleapis.com/token`, and verifies the id_token against
|
|
80
|
+
* `https://www.googleapis.com/oauth2/v3/certs`, exactly as it does live.
|
|
81
|
+
*
|
|
82
|
+
* A test signs in the way a user does, through the browser:
|
|
83
|
+
*
|
|
84
|
+
* ```ts
|
|
85
|
+
* await page.getByTestId("login-google").click();
|
|
86
|
+
* await page.getByTestId("spectest-user-alice@example.com").click();
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function google(opts = {}) {
|
|
90
|
+
const users = resolveUsers("google", opts.users);
|
|
91
|
+
return emulatorService(spec(users, opts));
|
|
92
|
+
}
|
|
@@ -6,4 +6,10 @@ export { expo, type ExpoOptions, type ExpoHelpers, } from "./expo.js";
|
|
|
6
6
|
export { supabase, type SupabaseOptions, type SupabaseHelpers, type SupabaseStack, } from "./supabase.js";
|
|
7
7
|
export { email, type EmailOptions, type EmailHelpers, type EmailMessage, type EmailSummary, type EmailMatch, type EmailAttachment, } from "./email.js";
|
|
8
8
|
export { aws, type AwsOptions, type LambdaOptions, } from "./aws.js";
|
|
9
|
+
export { type ProviderOptions, type ProviderUser, type OAuthClient, } from "./emulate/service.js";
|
|
10
|
+
export { google, type GoogleOptions } from "./google.js";
|
|
11
|
+
export { github, type GithubOptions } from "./github.js";
|
|
12
|
+
export { apple, type AppleOptions } from "./apple.js";
|
|
13
|
+
export { microsoft, type MicrosoftOptions } from "./microsoft.js";
|
|
14
|
+
export { okta, type OktaOptions } from "./okta.js";
|
|
9
15
|
export { replayFake, type ReplayFakeOptions, type ReplayMatch, type InjectRule, type InjectMatch, type StringMatch, type SignConfig, type AwsSigV4Sign, type ReplayHelpers, type ReplaySummary, type Cassette, type CassetteInteraction, type CassetteRequest, type CassetteResponse, type CassetteState, } from "./replayFake.js";
|
package/dist/components/index.js
CHANGED
|
@@ -16,4 +16,9 @@ export { expo, } from "./expo.js";
|
|
|
16
16
|
export { supabase, } from "./supabase.js";
|
|
17
17
|
export { email, } from "./email.js";
|
|
18
18
|
export { aws, } from "./aws.js";
|
|
19
|
+
export { google } from "./google.js";
|
|
20
|
+
export { github } from "./github.js";
|
|
21
|
+
export { apple } from "./apple.js";
|
|
22
|
+
export { microsoft } from "./microsoft.js";
|
|
23
|
+
export { okta } from "./okta.js";
|
|
19
24
|
export { replayFake, } from "./replayFake.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ProviderOptions } from "./emulate/service.js";
|
|
2
|
+
export interface MicrosoftOptions extends ProviderOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Directory (tenant) the app signs in against — whatever its authority
|
|
5
|
+
* URL uses. It appears in the issuer and in every endpoint path, so a
|
|
6
|
+
* client that checks the issuer sees what it expects. Default `"common"`.
|
|
7
|
+
*/
|
|
8
|
+
tenantId?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Microsoft, answering at its real endpoints. Drop into
|
|
12
|
+
* `environment.services`:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { microsoft } from "@specific.dev/spectest/components";
|
|
16
|
+
*
|
|
17
|
+
* services: {
|
|
18
|
+
* microsoft: microsoft({
|
|
19
|
+
* tenantId: "11111111-2222-3333-4444-555555555555",
|
|
20
|
+
* users: [{ email: "bob@example.com", name: "Bob Example" }],
|
|
21
|
+
* client: {
|
|
22
|
+
* clientId: "6f1a2b3c-…",
|
|
23
|
+
* clientSecret: "secret",
|
|
24
|
+
* redirectUris: ["https://app.test/callback/microsoft"],
|
|
25
|
+
* },
|
|
26
|
+
* }),
|
|
27
|
+
* app: { …, dependsOn: ["microsoft"] },
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* The app keeps its production configuration — discovery at
|
|
32
|
+
* `https://login.microsoftonline.com/<tenant>/v2.0/.well-known/openid-configuration`,
|
|
33
|
+
* and the account read back from `https://graph.microsoft.com/v1.0/me`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function microsoft(opts?: MicrosoftOptions): {
|
|
36
|
+
image: {
|
|
37
|
+
type: "dockerfile";
|
|
38
|
+
content: string;
|
|
39
|
+
};
|
|
40
|
+
command: string;
|
|
41
|
+
files: {
|
|
42
|
+
path: string;
|
|
43
|
+
content: string;
|
|
44
|
+
}[];
|
|
45
|
+
env: {
|
|
46
|
+
SPECTEST_AUTH_CONFIG: string;
|
|
47
|
+
};
|
|
48
|
+
ports: number[];
|
|
49
|
+
readyCheck: {
|
|
50
|
+
type: "http";
|
|
51
|
+
port: number;
|
|
52
|
+
path: string;
|
|
53
|
+
timeoutSecs: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// `microsoft()` — Microsoft, emulated inside the environment, answering at
|
|
2
|
+
// its real endpoints.
|
|
3
|
+
//
|
|
4
|
+
// Today that is Sign in with Microsoft (Entra ID) plus the part of Graph an
|
|
5
|
+
// app reads straight after: discovery, the account picker, the token
|
|
6
|
+
// exchange, `/oidc/userinfo` and `/v1.0/me`. The component is named for the
|
|
7
|
+
// provider rather than for the feature — Graph is already claimed, and
|
|
8
|
+
// widening what it serves is a table entry.
|
|
9
|
+
import { emulatorService, oauthClients, resolveUsers } from "./emulate/service.js";
|
|
10
|
+
function spec(users, opts) {
|
|
11
|
+
const authority = `https://login.microsoftonline.com/${opts.tenantId || "common"}`;
|
|
12
|
+
return {
|
|
13
|
+
name: "microsoft",
|
|
14
|
+
module: "@emulators/microsoft",
|
|
15
|
+
pluginExport: "microsoftPlugin",
|
|
16
|
+
baseUrl: "https://login.microsoftonline.com",
|
|
17
|
+
hosts: ["login.microsoftonline.com", "graph.microsoft.com"],
|
|
18
|
+
// The emulator has no tenant, so its issuer is the bare host. A client
|
|
19
|
+
// that checks the issuer against the authority it configured would
|
|
20
|
+
// reject that; the re-issue step corrects it.
|
|
21
|
+
issuer: `${authority}/v2.0`,
|
|
22
|
+
discovery: {
|
|
23
|
+
issuer: `${authority}/v2.0`,
|
|
24
|
+
authorization_endpoint: `${authority}/oauth2/v2.0/authorize`,
|
|
25
|
+
token_endpoint: `${authority}/oauth2/v2.0/token`,
|
|
26
|
+
jwks_uri: `${authority}/discovery/v2.0/keys`,
|
|
27
|
+
end_session_endpoint: `${authority}/oauth2/v2.0/logout`,
|
|
28
|
+
userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo",
|
|
29
|
+
},
|
|
30
|
+
jwksPath: "^/discovery/v2\\.0/keys$",
|
|
31
|
+
rewrites: [
|
|
32
|
+
// Real Entra paths carry the tenant; the emulator's do not. The
|
|
33
|
+
// discovery route is the exception — it handles the tenant itself.
|
|
34
|
+
{ from: "^/[^/]+/(oauth2/v2\\.0/.*)$", to: "/$1" },
|
|
35
|
+
{ from: "^/[^/]+/(discovery/v2\\.0/keys)$", to: "/$1" },
|
|
36
|
+
],
|
|
37
|
+
fallbackUser: {
|
|
38
|
+
login: users[0].email,
|
|
39
|
+
id: 1,
|
|
40
|
+
scopes: ["openid", "email", "profile", "User.Read"],
|
|
41
|
+
},
|
|
42
|
+
seed: {
|
|
43
|
+
users: users.map((u) => ({ email: u.email, name: u.name })),
|
|
44
|
+
...oauthClients(opts.client),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Microsoft, answering at its real endpoints. Drop into
|
|
50
|
+
* `environment.services`:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { microsoft } from "@specific.dev/spectest/components";
|
|
54
|
+
*
|
|
55
|
+
* services: {
|
|
56
|
+
* microsoft: microsoft({
|
|
57
|
+
* tenantId: "11111111-2222-3333-4444-555555555555",
|
|
58
|
+
* users: [{ email: "bob@example.com", name: "Bob Example" }],
|
|
59
|
+
* client: {
|
|
60
|
+
* clientId: "6f1a2b3c-…",
|
|
61
|
+
* clientSecret: "secret",
|
|
62
|
+
* redirectUris: ["https://app.test/callback/microsoft"],
|
|
63
|
+
* },
|
|
64
|
+
* }),
|
|
65
|
+
* app: { …, dependsOn: ["microsoft"] },
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* The app keeps its production configuration — discovery at
|
|
70
|
+
* `https://login.microsoftonline.com/<tenant>/v2.0/.well-known/openid-configuration`,
|
|
71
|
+
* and the account read back from `https://graph.microsoft.com/v1.0/me`.
|
|
72
|
+
*/
|
|
73
|
+
export function microsoft(opts = {}) {
|
|
74
|
+
const users = resolveUsers("microsoft", opts.users);
|
|
75
|
+
return emulatorService(spec(users, opts));
|
|
76
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ProviderOptions } from "./emulate/service.js";
|
|
2
|
+
export interface OktaOptions extends ProviderOptions {
|
|
3
|
+
/** The org's domain, e.g. `"dev-12345.okta.com"`. */
|
|
4
|
+
domain: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Okta, answering at your org's real domain. Drop into
|
|
8
|
+
* `environment.services`:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { okta } from "@specific.dev/spectest/components";
|
|
12
|
+
*
|
|
13
|
+
* services: {
|
|
14
|
+
* okta: okta({
|
|
15
|
+
* domain: "dev-12345.okta.com",
|
|
16
|
+
* users: [{ email: "alice@example.com", name: "Alice Example" }],
|
|
17
|
+
* client: {
|
|
18
|
+
* clientId: "0oaexample",
|
|
19
|
+
* clientSecret: "secret",
|
|
20
|
+
* redirectUris: ["https://app.test/callback/okta"],
|
|
21
|
+
* },
|
|
22
|
+
* }),
|
|
23
|
+
* app: { …, dependsOn: ["okta"] },
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The app keeps its production configuration — discovery at
|
|
28
|
+
* `https://dev-12345.okta.com/.well-known/openid-configuration`, and
|
|
29
|
+
* everything it names.
|
|
30
|
+
*/
|
|
31
|
+
export declare function okta(opts: OktaOptions): {
|
|
32
|
+
image: {
|
|
33
|
+
type: "dockerfile";
|
|
34
|
+
content: string;
|
|
35
|
+
};
|
|
36
|
+
command: string;
|
|
37
|
+
files: {
|
|
38
|
+
path: string;
|
|
39
|
+
content: string;
|
|
40
|
+
}[];
|
|
41
|
+
env: {
|
|
42
|
+
SPECTEST_AUTH_CONFIG: string;
|
|
43
|
+
};
|
|
44
|
+
ports: number[];
|
|
45
|
+
readyCheck: {
|
|
46
|
+
type: "http";
|
|
47
|
+
port: number;
|
|
48
|
+
path: string;
|
|
49
|
+
timeoutSecs: number;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// `okta()` — Okta, emulated inside the environment, answering at your org's
|
|
2
|
+
// real domain.
|
|
3
|
+
//
|
|
4
|
+
// Today that is Okta sign-in: discovery, the account picker, the token
|
|
5
|
+
// exchange, userinfo, introspection and revocation, on both the org
|
|
6
|
+
// authorization server (`/oauth2/v1/…`) and a custom one
|
|
7
|
+
// (`/oauth2/default/…`). The component is named for the provider rather
|
|
8
|
+
// than for the feature — the backing emulator also carries the users,
|
|
9
|
+
// groups and apps management API.
|
|
10
|
+
//
|
|
11
|
+
// Okta has no fixed hostname: every organization gets its own domain, so
|
|
12
|
+
// `domain` is required where the other providers need nothing.
|
|
13
|
+
import { emulatorService, familyName, givenName, oauthClients, resolveUsers, } from "./emulate/service.js";
|
|
14
|
+
/** Okta's picker identifies an account by its opaque Okta id, so the id has
|
|
15
|
+
* to be seeded rather than generated — otherwise nothing on the page can be
|
|
16
|
+
* mapped back to an address. */
|
|
17
|
+
function oktaId(email) {
|
|
18
|
+
return `u-${email.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`;
|
|
19
|
+
}
|
|
20
|
+
function spec(users, opts) {
|
|
21
|
+
const domain = opts.domain.toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
|
|
22
|
+
return {
|
|
23
|
+
name: "okta",
|
|
24
|
+
module: "@emulators/okta",
|
|
25
|
+
pluginExport: "oktaPlugin",
|
|
26
|
+
baseUrl: `https://${domain}`,
|
|
27
|
+
hosts: [domain],
|
|
28
|
+
// Okta's issuer is per authorization server and the emulator already
|
|
29
|
+
// builds it from the base URL, which here is the real org domain.
|
|
30
|
+
jwksPath: "^/oauth2/(?:[^/]+/)?v1/keys$",
|
|
31
|
+
aliases: Object.fromEntries(users.map((u) => [oktaId(u.email), u.email])),
|
|
32
|
+
fallbackUser: { login: users[0].email, id: 1, scopes: ["openid", "profile", "email"] },
|
|
33
|
+
seed: {
|
|
34
|
+
users: users.map((u) => ({
|
|
35
|
+
okta_id: oktaId(u.email),
|
|
36
|
+
// An Okta login is the address, not the GitHub-style handle that
|
|
37
|
+
// `login` carries — it comes back as `preferred_username`.
|
|
38
|
+
login: u.email,
|
|
39
|
+
email: u.email,
|
|
40
|
+
first_name: givenName(u),
|
|
41
|
+
last_name: familyName(u),
|
|
42
|
+
display_name: u.name,
|
|
43
|
+
})),
|
|
44
|
+
...oauthClients(opts.client),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Okta, answering at your org's real domain. Drop into
|
|
50
|
+
* `environment.services`:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* import { okta } from "@specific.dev/spectest/components";
|
|
54
|
+
*
|
|
55
|
+
* services: {
|
|
56
|
+
* okta: okta({
|
|
57
|
+
* domain: "dev-12345.okta.com",
|
|
58
|
+
* users: [{ email: "alice@example.com", name: "Alice Example" }],
|
|
59
|
+
* client: {
|
|
60
|
+
* clientId: "0oaexample",
|
|
61
|
+
* clientSecret: "secret",
|
|
62
|
+
* redirectUris: ["https://app.test/callback/okta"],
|
|
63
|
+
* },
|
|
64
|
+
* }),
|
|
65
|
+
* app: { …, dependsOn: ["okta"] },
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* The app keeps its production configuration — discovery at
|
|
70
|
+
* `https://dev-12345.okta.com/.well-known/openid-configuration`, and
|
|
71
|
+
* everything it names.
|
|
72
|
+
*/
|
|
73
|
+
export function okta(opts) {
|
|
74
|
+
const users = resolveUsers("okta", opts.users);
|
|
75
|
+
if (!opts.domain)
|
|
76
|
+
throw new Error("okta(): `domain` is required (e.g. \"dev-12345.okta.com\")");
|
|
77
|
+
return emulatorService(spec(users, opts));
|
|
78
|
+
}
|