@corespec/vite-plugin-api-auto-login 0.0.0-stage → 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/README.md CHANGED
@@ -1,3 +1,254 @@
1
- # Temporary Holding Version
1
+ # @corespec/vite-plugin-api-auto-login
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm version](https://img.shields.io/npm/v/@corespec/vite-plugin-api-auto-login)](https://www.npmjs.com/package/@corespec/vite-plugin-api-auto-login)
4
+ [![CI](https://github.com/core-spec-tech/vite-plugin-api-auto-login/actions/workflows/ci.yml/badge.svg?branch=main&event=push)](https://github.com/core-spec-tech/vite-plugin-api-auto-login/actions/workflows/ci.yml)
5
+ [![Security checks](https://github.com/core-spec-tech/vite-plugin-api-auto-login/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/core-spec-tech/vite-plugin-api-auto-login/actions/workflows/security.yml)
6
+ [![License](https://img.shields.io/npm/l/@corespec/vite-plugin-api-auto-login)](https://www.npmjs.com/package/@corespec/vite-plugin-api-auto-login)
7
+ [![Vite compatibility](https://registry.vite.dev/api/badges?package=%40corespec%2Fvite-plugin-api-auto-login&tool=vite)](https://registry.vite.dev/)
8
+ [![Rollup compatibility](https://registry.vite.dev/api/badges?package=%40corespec%2Fvite-plugin-api-auto-login&tool=rollup)](https://registry.vite.dev/)
9
+
10
+ A reusable Vite plugin that automatically logs in to an API from the Node.js development server.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install -D @corespec/vite-plugin-api-auto-login
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Add the plugin to your Vite configuration:
21
+
22
+ ```ts
23
+ import { defineConfig } from 'vite'
24
+ import { viteAPIAutoLogin } from '@corespec/vite-plugin-api-auto-login'
25
+
26
+ export default defineConfig({
27
+ plugins: [viteAPIAutoLogin()],
28
+ })
29
+ ```
30
+
31
+ The plugin runs only in Vite's development server. It reads environment variables prefixed with `API_AUTO_LOGIN_` and injects an authentication cookie into responses to matching HTML document requests.
32
+
33
+ ## Customizing form field names
34
+
35
+ Use `formFields` to rename either credential field without writing a strategy:
36
+
37
+ ```ts
38
+ viteAPIAutoLogin({
39
+ formFields: { username: 'email' },
40
+ })
41
+ ```
42
+
43
+ The default names are `username` and `password`. For example,
44
+ `formFields: { username: 'login', password: 'passcode' }` changes both.
45
+ Values still come from `API_AUTO_LOGIN_USERNAME` and `API_AUTO_LOGIN_PASSWORD`;
46
+ only the outgoing form field names change. Names must be non-empty and distinct.
47
+ The `User-Agent` header is unaffected.
48
+
49
+ Use `formFields` only with the default strategy; it cannot be combined with
50
+ `request` or `createAuthRequest`. When constructing the exported strategy directly,
51
+ use `new FormDataAuthRequest(config, { username: 'email' })`.
52
+ The `FormFieldNames` type is exported for reusable mappings.
53
+
54
+ ## Customizing the login request
55
+
56
+ Use `createAuthRequest(config)` to create a `AuthRequest` strategy for each
57
+ Vite server initialization. The strategy controls the URL, HTTP method, headers,
58
+ and body. A class can keep any additional configuration it needs:
59
+
60
+ ```ts
61
+ import { defineConfig } from 'vite'
62
+ import {
63
+ viteAPIAutoLogin,
64
+ type PluginEnv,
65
+ type AuthRequest,
66
+ type AuthRequestOptions,
67
+ } from '@corespec/vite-plugin-api-auto-login'
68
+
69
+ class JsonAuthRequest implements AuthRequest {
70
+ create(config: Readonly<PluginEnv>): AuthRequestOptions {
71
+ if (!config.loginUrl || !config.username || !config.password) {
72
+ throw new Error('JSON login requires an endpoint, username, and password')
73
+ }
74
+
75
+ return {
76
+ url: config.loginUrl,
77
+ init: {
78
+ method: 'POST',
79
+ headers: { 'content-type': 'application/json' },
80
+ body: JSON.stringify({
81
+ username: config.username,
82
+ password: config.password,
83
+ }),
84
+ },
85
+ }
86
+ }
87
+ }
88
+
89
+ export default defineConfig({
90
+ plugins: [viteAPIAutoLogin({ createAuthRequest: () => new JsonAuthRequest() })],
91
+ })
92
+ ```
93
+
94
+ The factory receives the readonly parsed configuration and runs once per
95
+ `configureServer` invocation, not once per request. Return a fresh instance to
96
+ keep mutable strategy state separate between servers. Factory errors are logged
97
+ and prevent middleware registration for that server. The factory is synchronous.
98
+
99
+ Use `request: new JsonAuthRequest()` for
100
+ shared strategy instances. Supply either `request` or `createAuthRequest`, never
101
+ both; TypeScript and runtime validation enforce this.
102
+
103
+ `create` can return a promise. It runs whenever a new token is needed, so create
104
+ a fresh body for each call. Concurrent login requests are deduplicated, and
105
+ valid JWT tokens are cached until their `exp` timestamp. Tokens without a
106
+ readable JWT expiry are not cached.
107
+
108
+ Without this option, `FormDataAuthRequest` sends a multipart POST
109
+ with `username` and `password` fields and a `User-Agent` header. This class is also exported
110
+ for consumers that want to extend the default request.
111
+
112
+ The `init` object supports Undici fetch options, including `signal` and
113
+ `redirect`. The plugin manages `dispatcher` through its TLS configuration.
114
+ The response must still be successful HTTP JSON containing a non-empty
115
+ `token` string. Custom strategies validate their own authentication requirements and can omit
116
+ all three default login variables.
117
+
118
+ ## Environment variables
119
+
120
+ Define these variables in your Vite environment files, such as `.env.local`, `.env.development`, or another file in `envDir` that Vite loads for the current mode.
121
+
122
+ Required by the default `FormDataAuthRequest` strategy only:
123
+
124
+ | Variable | Description |
125
+ | -------------------------- | -------------------------------------------------------------------------------------------- |
126
+ | `API_AUTO_LOGIN_LOGIN_URL` | Login endpoint URL used to request an authentication token. |
127
+ | `API_AUTO_LOGIN_USERNAME` | Account username sent to the login endpoint; may be an email address if the API supports it. |
128
+ | `API_AUTO_LOGIN_PASSWORD` | Account password sent to the login endpoint. |
129
+
130
+ Optional variables:
131
+
132
+ | Variable | Default | Description |
133
+ | ----------------------------------- | ----------------- | ----------------------------------------------------------------------------- |
134
+ | `API_AUTO_LOGIN_COOKIE_NAME` | `your_token_name` | Name of the cookie added to the response with the authentication token. |
135
+ | `API_AUTO_LOGIN_USER_AGENT` | `vite` | Value sent in the login request's `User-Agent` HTTP header. |
136
+ | `API_AUTO_LOGIN_ALLOW_INSECURE_TLS` | `false` | Set to `true` to disable TLS certificate verification for the login endpoint. |
137
+
138
+ ## Configuration parsing
139
+
140
+ `PluginConfig.fromEnv(env)` converts the environment values returned by Vite's
141
+ `loadEnv` into readonly configuration properties. It applies the shared defaults
142
+ and parses the TLS flag. Use this named factory instead of constructing
143
+ `PluginConfig` directly.
144
+
145
+ Login URL, username, and password are optional in the shared configuration.
146
+ `FormDataAuthRequest` validates them when the plugin creates the default
147
+ strategy and whenever it builds a request. A custom strategy or factory is
148
+ responsible for validating the fields it uses.
149
+
150
+ The login variables deliberately avoid the `VITE_` prefix so they remain on the server with Vite's default configuration. Do not add `API_AUTO_LOGIN_` to Vite's `envPrefix`, as this would expose credentials to client code.
151
+
152
+ ## Vue + MSW example
153
+
154
+ Run `npm ci` and `npm run example` from this repository, then open
155
+ [http://127.0.0.1:5173](http://127.0.0.1:5173).
156
+
157
+ The [example guide](examples/vue-msw/README.md) explains the mock API, cookie
158
+ flow, and `formFields: { username: 'email' }` mapping. No external API or real
159
+ credentials are needed.
160
+
161
+ ## Development
162
+
163
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for code conventions, design guidelines,
164
+ and the required checks.
165
+
166
+ Use the Node.js version pinned in `.nvmrc`. GitHub CI and publishing use the same
167
+ file. With nvm installed:
168
+
169
+ ```bash
170
+ nvm install
171
+ nvm use
172
+ npm ci
173
+ npm run check
174
+ npm run build
175
+ ```
176
+
177
+ `npm run check` runs formatting checks, linting, the full project type check, and all tests once.
178
+ Use `npm test` for watch mode or `npm run coverage` for a coverage report.
179
+ The build produces ESM, CommonJS, and TypeScript declarations.
180
+ Use `npm run format` to format files with Prettier (100-character print width).
181
+
182
+ Tests are grouped under `tests/auth`, `tests/config`, and `tests/http`.
183
+ `tests/plugin.spec.ts` covers plugin integration, with shared HTTP fixtures in
184
+ `tests/helpers`. Editor formatting defaults are defined in `.editorconfig`.
185
+ Local environment overrides (`.env.local` and `.env.*.local`) are ignored by Git.
186
+
187
+ ### Source imports
188
+
189
+ Use `@/` for imports from `src/`, including imports in tests:
190
+
191
+ ```ts
192
+ import { PluginConfig } from '@/config/PluginConfig'
193
+ ```
194
+
195
+ TypeScript and Vite/Vitest resolve this alias locally. The declaration build
196
+ uses `tsc-alias` to replace aliases with relative paths in published types;
197
+ consumers do not need to configure `@/`. Imports between test helpers stay relative.
198
+
199
+ ## Local linking
200
+
201
+ When developing this package alongside a consuming Vite application, run the linking commands from each repository's root directory:
202
+
203
+ ```bash
204
+ cd vite-plugin-api-auto-login
205
+ npm link
206
+
207
+ cd ../your-app
208
+ npm link @corespec/vite-plugin-api-auto-login
209
+ ```
210
+
211
+ ## Security scanning
212
+
213
+ GitHub Actions runs CodeQL for TypeScript and workflow code, dependency review
214
+ on pull requests, and scheduled npm audits. Run `npm run security:audit` locally;
215
+ high and critical dependency findings also block publishing.
216
+ See [security setup](CONTRIBUTING.md#security-checks) for thresholds and the GitHub
217
+ settings needed for secret scanning and merge protection.
218
+
219
+ ## Publishing
220
+
221
+ The `.github/workflows/publish.yml` workflow stages releases for approval when a version tag (`v*`) or package-specific tag (`@corespec/vite-plugin-api-auto-login@*`) is pushed. The tag must match the version in `package.json`. The workflow runs linting, type checking, tests, and the build before staging the package on npm. A maintainer approves the staged version with 2FA to publish it.
222
+
223
+ The initial version is `0.1.0`. Releases use Semantic Versioning and Conventional
224
+ Commits; see [CONTRIBUTING.md](CONTRIBUTING.md#versioning-and-releases) for the
225
+ release commands and the `0.x` versioning policy.
226
+
227
+ Release setup:
228
+
229
+ 1. Store a granular npm publishing token with access to the `@corespec` scope
230
+ as the repository Actions secret `NPM_TOKEN`. Use a stage-only token; CI does
231
+ not need to bypass 2FA. Keep local tokens in an ignored `.env` file.
232
+ 2. Run `npm ci`, `npm run check`, and `npm run example:build`.
233
+ 3. The renamed package's initial release uses the tag
234
+ `@corespec/vite-plugin-api-auto-login@0.1.0`, preserving the old package's
235
+ `v0.1.0` tag. For later releases, use `npm run release` or an explicit
236
+ bump command described in CONTRIBUTING.md.
237
+ 4. Push the release commit and tag with `git push --follow-tags`. The Publish
238
+ workflow checks, builds, and stages the package with provenance. The npm
239
+ token is available only to its staging step.
240
+ 5. Review the version in npm's Staged Packages tab and approve it with 2FA,
241
+ or run `npm stage approve <stage-id>` while logged in as a maintainer.
242
+ A successful staging workflow does not mean the version is publicly available.
243
+
244
+ To retry an existing tag, manually run the Publish workflow and supply that tag
245
+ (for example `v0.1.0`). It checks out the tagged source and verifies the version.
246
+
247
+ Trusted publishing can replace the token after configuring owner `core-spec-tech`,
248
+ repository `vite-plugin-api-auto-login`, and workflow `publish.yml` in npm's package
249
+ settings. Then remove `NODE_AUTH_TOKEN` from the staging step and delete
250
+ the repository secret.
251
+
252
+ GitHub CI runs on pushes and pull requests. You can configure Renovate through its GitHub app using `renovate.json`.
253
+
254
+ See [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) for details.
@@ -0,0 +1,10 @@
1
+ import type { RequestInit } from 'undici';
2
+ import type { PluginEnv } from '../config/types.js';
3
+ export type AuthRequestOptions = {
4
+ url: string | URL;
5
+ init: Omit<RequestInit, 'dispatcher'>;
6
+ };
7
+ export interface AuthRequest {
8
+ create(config: Readonly<PluginEnv>): AuthRequestOptions | Promise<AuthRequestOptions>;
9
+ }
10
+ //# sourceMappingURL=AuthRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthRequest.d.ts","sourceRoot":"","sources":["../../src/auth/AuthRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAEzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;IACjB,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;CACtC,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CACtF"}
@@ -0,0 +1,24 @@
1
+ import { fetch as undiciFetch } from 'undici';
2
+ import type { Dispatcher } from 'undici';
3
+ import type { PluginEnv } from '../config/types.js';
4
+ import type { AuthRequest } from './AuthRequest.js';
5
+ type FetchLike = typeof undiciFetch;
6
+ type AuthSessionDependencies = {
7
+ request?: AuthRequest;
8
+ createDispatcher?: () => Dispatcher;
9
+ fetch?: FetchLike;
10
+ };
11
+ export declare class AuthSession {
12
+ private readonly env;
13
+ private readonly request;
14
+ private readonly fetchImpl;
15
+ private readonly dispatcher;
16
+ private cachedToken;
17
+ private pendingLogin;
18
+ constructor(env: PluginEnv, dependencies?: AuthSessionDependencies);
19
+ getToken(): Promise<string>;
20
+ private requestToken;
21
+ private parseJwtExpiry;
22
+ }
23
+ export {};
24
+ //# sourceMappingURL=AuthSession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthSession.d.ts","sourceRoot":"","sources":["../../src/auth/AuthSession.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAExC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAOrD,KAAK,SAAS,GAAG,OAAO,WAAW,CAAA;AAEnC,KAAK,uBAAuB,GAAG;IAC7B,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,gBAAgB,CAAC,EAAE,MAAM,UAAU,CAAA;IACnC,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB,CAAA;AAED,qBAAa,WAAW;IAQpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,YAAY,CAA6B;gBAG9B,GAAG,EAAE,SAAS,EAC/B,YAAY,GAAE,uBAA4B;IAUtC,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC;YAiBnB,YAAY;IAuB1B,OAAO,CAAC,cAAc;CASvB"}
@@ -0,0 +1,9 @@
1
+ import type { FormFieldNames, PluginEnv } from '../config/types.js';
2
+ import type { AuthRequest, AuthRequestOptions } from './AuthRequest.js';
3
+ export declare class FormDataAuthRequest implements AuthRequest {
4
+ private readonly fields;
5
+ constructor(config?: Readonly<PluginEnv>, fields?: Readonly<FormFieldNames>);
6
+ create(config: Readonly<PluginEnv>): AuthRequestOptions;
7
+ private validate;
8
+ }
9
+ //# sourceMappingURL=FormDataAuthRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FormDataAuthRequest.d.ts","sourceRoot":"","sources":["../../src/auth/FormDataAuthRequest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAEzE,qBAAa,mBAAoB,YAAW,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;gBAErC,MAAM,CAAC,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAE,QAAQ,CAAC,cAAc,CAAM;IAkB/E,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,kBAAkB;IAgBvD,OAAO,CAAC,QAAQ;CASjB"}
@@ -0,0 +1,14 @@
1
+ import type { PluginEnv, PluginEnvInput } from './types.js';
2
+ export declare const DEFAULT_COOKIE_NAME = "your_token_name";
3
+ export declare const DEFAULT_USER_AGENT = "vite";
4
+ export declare class PluginConfig implements PluginEnv {
5
+ readonly allowInsecureTls: boolean;
6
+ readonly cookieName: string;
7
+ readonly username: string | undefined;
8
+ readonly loginUrl: string | undefined;
9
+ readonly password: string | undefined;
10
+ readonly userAgent: string;
11
+ static fromEnv(env: PluginEnvInput): PluginConfig;
12
+ private constructor();
13
+ }
14
+ //# sourceMappingURL=PluginConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PluginConfig.d.ts","sourceRoot":"","sources":["../../src/config/PluginConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAE/D,eAAO,MAAM,mBAAmB,oBAAoB,CAAA;AACpD,eAAO,MAAM,kBAAkB,SAAS,CAAA;AAExC,qBAAa,YAAa,YAAW,SAAS;IAC5C,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAE1B,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IAIjD,OAAO;CAQR"}
@@ -0,0 +1,29 @@
1
+ import type { AuthRequest } from '../auth/AuthRequest.js';
2
+ export type PluginEnv = {
3
+ allowInsecureTls: boolean;
4
+ cookieName: string;
5
+ username?: string;
6
+ loginUrl?: string;
7
+ password?: string;
8
+ userAgent: string;
9
+ };
10
+ export type PluginEnvInput = Readonly<Record<string, string | undefined>>;
11
+ export type AuthRequestFactory = (config: Readonly<PluginEnv>) => AuthRequest;
12
+ export type FormFieldNames = {
13
+ username?: string;
14
+ password?: string;
15
+ };
16
+ export type AutoLoginOptions = {
17
+ formFields?: FormFieldNames;
18
+ request?: never;
19
+ createAuthRequest?: never;
20
+ } | {
21
+ formFields?: never;
22
+ request: AuthRequest;
23
+ createAuthRequest?: never;
24
+ } | {
25
+ formFields?: never;
26
+ request?: never;
27
+ createAuthRequest: AuthRequestFactory;
28
+ };
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAErD,MAAM,MAAM,SAAS,GAAG;IACtB,gBAAgB,EAAE,OAAO,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;AAEzE,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,KAAK,WAAW,CAAA;AAE7E,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,UAAU,CAAC,EAAE,cAAc,CAAC;IAAC,OAAO,CAAC,EAAE,KAAK,CAAC;IAAC,iBAAiB,CAAC,EAAE,KAAK,CAAA;CAAE,GAC3E;IAAE,UAAU,CAAC,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,WAAW,CAAC;IAAC,iBAAiB,CAAC,EAAE,KAAK,CAAA;CAAE,GACvE;IAAE,UAAU,CAAC,EAAE,KAAK,CAAC;IAAC,OAAO,CAAC,EAAE,KAAK,CAAC;IAAC,iBAAiB,EAAE,kBAAkB,CAAA;CAAE,CAAA"}
@@ -0,0 +1,2 @@
1
+ export declare const PLUGIN_NAME = "vite-plugin-api-auto-login";
2
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,+BAA+B,CAAA"}
@@ -0,0 +1,7 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ export declare class AuthResponse {
3
+ private readonly response;
4
+ constructor(response: ServerResponse);
5
+ setAuthCookie(cookieName: string, token: string): void;
6
+ }
7
+ //# sourceMappingURL=AuthResponse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthResponse.d.ts","sourceRoot":"","sources":["../../src/http/AuthResponse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAE/C,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,cAAc;IAErD,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;CAkBvD"}
@@ -0,0 +1,5 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export declare class HtmlDocumentRequest {
3
+ static matches(req: IncomingMessage): boolean;
4
+ }
5
+ //# sourceMappingURL=HtmlDocumentRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HtmlDocumentRequest.d.ts","sourceRoot":"","sources":["../../src/http/HtmlDocumentRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAIhD,qBAAa,mBAAmB;IAC9B,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO;CAW9C"}
@@ -0,0 +1,6 @@
1
+ export { viteAPIAutoLogin } from './plugin.js';
2
+ export { FormDataAuthRequest } from './auth/FormDataAuthRequest.js';
3
+ export type { AuthRequest, AuthRequestOptions } from './auth/AuthRequest.js';
4
+ export type { AutoLoginOptions, PluginEnv, AuthRequestFactory, FormFieldNames, } from './config/types.js';
5
+ export { PluginConfig } from './config/PluginConfig.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACzE,YAAY,EACV,gBAAgB,EAChB,SAAS,EACT,kBAAkB,EAClB,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA"}
@@ -0,0 +1,12 @@
1
+ import type { Connect, Logger } from 'vite';
2
+ import type { AuthRequest } from '../auth/AuthRequest.js';
3
+ import type { PluginEnv } from '../config/types.js';
4
+ export declare class AutoLoginMiddleware {
5
+ private readonly config;
6
+ private readonly logger;
7
+ private readonly request;
8
+ private session;
9
+ constructor(config: PluginEnv, logger: Pick<Logger, 'error'>, request: AuthRequest);
10
+ readonly handle: Connect.NextHandleFunction;
11
+ }
12
+ //# sourceMappingURL=AutoLoginMiddleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AutoLoginMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/AutoLoginMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAG3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAK/C,qBAAa,mBAAmB;IAI5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,OAAO,CAAyB;gBAGrB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW;IAGvC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAiB1C;CACF"}
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { AutoLoginOptions } from './config/types.js';
3
+ export declare function viteAPIAutoLogin(options?: AutoLoginOptions): Plugin;
4
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAMlC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAGtD,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAoCvE"}
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("vite"),t=require("undici"),n=require("jose");var r=class{fields;constructor(e,t={}){if(this.fields={username:t.username??`username`,password:t.password??`password`},!this.fields.username.trim()||!this.fields.password.trim()||this.fields.username===this.fields.password)throw Error(`Form field names must be non-empty and distinct`);e&&this.validate(e)}create(e){let{username:n,password:r,loginUrl:i}=this.validate(e),a=new t.FormData;return a.set(this.fields.username,n),a.set(this.fields.password,r),{url:i,init:{method:`POST`,headers:{"User-Agent":e.userAgent},body:a}}}validate(e){let{loginUrl:t,username:n,password:r}=e;if(!t||!n||!r)throw Error(`Missing required env vars: API_AUTO_LOGIN_LOGIN_URL, API_AUTO_LOGIN_USERNAME, API_AUTO_LOGIN_PASSWORD`);return{loginUrl:t,username:n,password:r}}},i=`vite-plugin-api-auto-login`,a=class e{allowInsecureTls;cookieName;username;loginUrl;password;userAgent;static fromEnv(t){return new e(t)}constructor(e){this.allowInsecureTls=e.API_AUTO_LOGIN_ALLOW_INSECURE_TLS===`true`,this.cookieName=e.API_AUTO_LOGIN_COOKIE_NAME??`your_token_name`,this.username=e.API_AUTO_LOGIN_USERNAME,this.loginUrl=e.API_AUTO_LOGIN_LOGIN_URL,this.password=e.API_AUTO_LOGIN_PASSWORD,this.userAgent=e.API_AUTO_LOGIN_USER_AGENT??`vite`}},o=class{env;request;fetchImpl;dispatcher;cachedToken;pendingLogin;constructor(e,n={}){this.env=e,this.request=n.request??new r,this.fetchImpl=n.fetch??t.fetch,e.allowInsecureTls&&(this.dispatcher=n.createDispatcher?.()??new t.Agent({connect:{rejectUnauthorized:!1}}))}async getToken(){return this.cachedToken?.expiresAt!==void 0&&this.cachedToken.expiresAt>Date.now()?this.cachedToken.value:(this.pendingLogin??=this.requestToken().then(e=>(this.cachedToken=e,e.value)).finally(()=>{this.pendingLogin=void 0}),this.pendingLogin)}async requestToken(){let{url:e,init:t}=await this.request.create(this.env),n=await this.fetchImpl(e,{...t,dispatcher:this.dispatcher});if(!n.ok)throw Error(`Login request failed with status ${n.status}`);let r=await n.json();if(typeof r.token!=`string`||r.token.length===0)throw Error(`Login response did not include a token`);return{expiresAt:this.parseJwtExpiry(r.token),value:r.token}}parseJwtExpiry(e){try{let{exp:t}=(0,n.decodeJwt)(e);return typeof t==`number`?t*1e3:void 0}catch{return}}},s=class{response;constructor(e){this.response=e}setAuthCookie(e,t){let n=`${e}=${t}; Path=/; SameSite=Lax`,r=this.response.getHeader(`Set-Cookie`);if(!r){this.response.setHeader(`Set-Cookie`,n);return}let i;i=Array.isArray(r)?r:[String(r)],this.response.setHeader(`Set-Cookie`,[...i,n])}},c=`text/html`,l=class{static matches(e){return e.headers.upgrade?.toLowerCase()===`websocket`||e.method!==`GET`&&e.method!==`HEAD`?!1:e.headers.accept?.includes(c)??!1}},u=class{config;logger;request;session;constructor(e,t,n){this.config=e,this.logger=t,this.request=n}handle=async(e,t,n)=>{if(!l.matches(e))n();else{try{this.session??=new o(this.config,{request:this.request});let e=await this.session.getToken();new s(t).setAuthCookie(this.config.cookieName,e)}catch(e){let t=e instanceof Error?e.message:String(e),n=this.config.loginUrl?` (${this.config.loginUrl})`:``;this.logger.error(`[${i}] ${t}${n}`)}n()}}};function d(t={}){if(t.request!==void 0&&t.createAuthRequest!==void 0)throw Error(`Use either request or createAuthRequest, not both`);if(t.formFields!==void 0&&(t.request!==void 0||t.createAuthRequest!==void 0))throw Error(`Use formFields only with the default request strategy`);return{name:i,apply:`serve`,configureServer(n){let{mode:o,envDir:s,logger:c}=n.config;try{let i=a.fromEnv((0,e.loadEnv)(o,s,`API_AUTO_LOGIN_`)),l=t.request;if(t.createAuthRequest?l=t.createAuthRequest(i):l||=new r(i,t.formFields),!l||typeof l.create!=`function`)throw Error(`Token request must provide a create method`);let d=new u(i,c,l);n.middlewares.use(d.handle)}catch(e){c.error(`[${i}] ${e instanceof Error?e.message:String(e)}`)}}}}exports.FormDataAuthRequest=r,exports.PluginConfig=a,exports.viteAPIAutoLogin=d;
@@ -0,0 +1,144 @@
1
+ import { loadEnv as e } from "vite";
2
+ import { Agent as t, FormData as n, fetch as r } from "undici";
3
+ import { decodeJwt as i } from "jose";
4
+ // src/auth/FormDataAuthRequest.ts
5
+ var a = class {
6
+ fields;
7
+ constructor(e, t = {}) {
8
+ if (this.fields = {
9
+ username: t.username ?? "username",
10
+ password: t.password ?? "password"
11
+ }, !this.fields.username.trim() || !this.fields.password.trim() || this.fields.username === this.fields.password) throw Error("Form field names must be non-empty and distinct");
12
+ e && this.validate(e);
13
+ }
14
+ create(e) {
15
+ let { username: t, password: r, loginUrl: i } = this.validate(e), a = new n();
16
+ return a.set(this.fields.username, t), a.set(this.fields.password, r), {
17
+ url: i,
18
+ init: {
19
+ method: "POST",
20
+ headers: { "User-Agent": e.userAgent },
21
+ body: a
22
+ }
23
+ };
24
+ }
25
+ validate(e) {
26
+ let { loginUrl: t, username: n, password: r } = e;
27
+ if (!t || !n || !r) throw Error("Missing required env vars: API_AUTO_LOGIN_LOGIN_URL, API_AUTO_LOGIN_USERNAME, API_AUTO_LOGIN_PASSWORD");
28
+ return {
29
+ loginUrl: t,
30
+ username: n,
31
+ password: r
32
+ };
33
+ }
34
+ }, o = "vite-plugin-api-auto-login", s = class e {
35
+ allowInsecureTls;
36
+ cookieName;
37
+ username;
38
+ loginUrl;
39
+ password;
40
+ userAgent;
41
+ static fromEnv(t) {
42
+ return new e(t);
43
+ }
44
+ constructor(e) {
45
+ this.allowInsecureTls = e.API_AUTO_LOGIN_ALLOW_INSECURE_TLS === "true", this.cookieName = e.API_AUTO_LOGIN_COOKIE_NAME ?? "your_token_name", this.username = e.API_AUTO_LOGIN_USERNAME, this.loginUrl = e.API_AUTO_LOGIN_LOGIN_URL, this.password = e.API_AUTO_LOGIN_PASSWORD, this.userAgent = e.API_AUTO_LOGIN_USER_AGENT ?? "vite";
46
+ }
47
+ }, c = class {
48
+ env;
49
+ request;
50
+ fetchImpl;
51
+ dispatcher;
52
+ cachedToken;
53
+ pendingLogin;
54
+ constructor(e, n = {}) {
55
+ this.env = e, this.request = n.request ?? new a(), this.fetchImpl = n.fetch ?? r, e.allowInsecureTls && (this.dispatcher = n.createDispatcher?.() ?? new t({ connect: { rejectUnauthorized: !1 } }));
56
+ }
57
+ async getToken() {
58
+ return this.cachedToken?.expiresAt !== void 0 && this.cachedToken.expiresAt > Date.now() ? this.cachedToken.value : (this.pendingLogin ??= this.requestToken().then((e) => (this.cachedToken = e, e.value)).finally(() => {
59
+ this.pendingLogin = void 0;
60
+ }), this.pendingLogin);
61
+ }
62
+ async requestToken() {
63
+ let { url: e, init: t } = await this.request.create(this.env), n = await this.fetchImpl(e, {
64
+ ...t,
65
+ dispatcher: this.dispatcher
66
+ });
67
+ if (!n.ok) throw Error(`Login request failed with status ${n.status}`);
68
+ let r = await n.json();
69
+ if (typeof r.token != "string" || r.token.length === 0) throw Error("Login response did not include a token");
70
+ return {
71
+ expiresAt: this.parseJwtExpiry(r.token),
72
+ value: r.token
73
+ };
74
+ }
75
+ parseJwtExpiry(e) {
76
+ try {
77
+ let { exp: t } = i(e);
78
+ return typeof t == "number" ? t * 1e3 : void 0;
79
+ } catch {
80
+ return;
81
+ }
82
+ }
83
+ }, l = class {
84
+ response;
85
+ constructor(e) {
86
+ this.response = e;
87
+ }
88
+ setAuthCookie(e, t) {
89
+ let n = `${e}=${t}; Path=/; SameSite=Lax`, r = this.response.getHeader("Set-Cookie");
90
+ if (!r) {
91
+ this.response.setHeader("Set-Cookie", n);
92
+ return;
93
+ }
94
+ let i;
95
+ i = Array.isArray(r) ? r : [String(r)], this.response.setHeader("Set-Cookie", [...i, n]);
96
+ }
97
+ }, u = "text/html", d = class {
98
+ static matches(e) {
99
+ return e.headers.upgrade?.toLowerCase() === "websocket" || e.method !== "GET" && e.method !== "HEAD" ? !1 : e.headers.accept?.includes(u) ?? !1;
100
+ }
101
+ }, f = class {
102
+ config;
103
+ logger;
104
+ request;
105
+ session;
106
+ constructor(e, t, n) {
107
+ this.config = e, this.logger = t, this.request = n;
108
+ }
109
+ handle = async (e, t, n) => {
110
+ if (!d.matches(e)) n();
111
+ else {
112
+ try {
113
+ this.session ??= new c(this.config, { request: this.request });
114
+ let e = await this.session.getToken();
115
+ new l(t).setAuthCookie(this.config.cookieName, e);
116
+ } catch (e) {
117
+ let t = e instanceof Error ? e.message : String(e), n = this.config.loginUrl ? ` (${this.config.loginUrl})` : "";
118
+ this.logger.error(`[${o}] ${t}${n}`);
119
+ }
120
+ n();
121
+ }
122
+ };
123
+ };
124
+ // src/plugin.ts
125
+ function p(t = {}) {
126
+ if (t.request !== void 0 && t.createAuthRequest !== void 0) throw Error("Use either request or createAuthRequest, not both");
127
+ if (t.formFields !== void 0 && (t.request !== void 0 || t.createAuthRequest !== void 0)) throw Error("Use formFields only with the default request strategy");
128
+ return {
129
+ name: o,
130
+ apply: "serve",
131
+ configureServer(n) {
132
+ let { mode: r, envDir: i, logger: c } = n.config;
133
+ try {
134
+ let o = s.fromEnv(e(r, i, "API_AUTO_LOGIN_")), l = t.request;
135
+ if (t.createAuthRequest ? l = t.createAuthRequest(o) : l ||= new a(o, t.formFields), !l || typeof l.create != "function") throw Error("Token request must provide a create method");
136
+ let u = new f(o, c, l);
137
+ n.middlewares.use(u.handle);
138
+ } catch (e) {
139
+ c.error(`[${o}] ${e instanceof Error ? e.message : String(e)}`);
140
+ }
141
+ }
142
+ };
143
+ }
144
+ export { a as FormDataAuthRequest, s as PluginConfig, p as viteAPIAutoLogin };
package/package.json CHANGED
@@ -1,6 +1,113 @@
1
1
  {
2
2
  "name": "@corespec/vite-plugin-api-auto-login",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Reusable Vite plugin for API auto login during local development",
5
+ "type": "module",
6
+ "main": "./dist/vite-plugin-api-auto-login.cjs",
7
+ "module": "./dist/vite-plugin-api-auto-login.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/vite-plugin-api-auto-login.js",
16
+ "require": "./dist/vite-plugin-api-auto-login.cjs"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "vite build && npm run build:types",
21
+ "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly && tsc-alias -p tsconfig.build.json",
22
+ "test": "vitest",
23
+ "coverage": "vitest run --coverage",
24
+ "release": "commit-and-tag-version",
25
+ "lint:file:fix": "npm-run-all \"eslint -- {1} --fix\" --",
26
+ "lint:file": "npm-run-all \"eslint -- {1}\" --",
27
+ "eslint": "eslint",
28
+ "lint:fix": "eslint . --fix",
29
+ "lint": "eslint .",
30
+ "typecheck": "tsc --noEmit && vue-tsc --noEmit -p examples/vue-msw/tsconfig.json",
31
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm test -- --run",
32
+ "format": "prettier . --write",
33
+ "format:check": "prettier . --check",
34
+ "example": "npm run build && node examples/vue-msw/dev.ts",
35
+ "example:build": "npm run build && vue-tsc --noEmit -p examples/vue-msw/tsconfig.json && vite build --config examples/vue-msw/vite.config.ts",
36
+ "release:first": "npm run release -- --first-release",
37
+ "release:patch": "npm run release -- --release-as patch",
38
+ "release:minor": "npm run release -- --release-as minor",
39
+ "release:major": "npm run release -- --release-as major",
40
+ "security:audit": "npm audit --package-lock-only --include=dev --audit-level=high"
41
+ },
42
+ "license": "ISC",
43
+ "engines": {
44
+ "node": ">=22.23.4"
45
+ },
46
+ "devDependencies": {
47
+ "@eslint/js": "^10.0.1",
48
+ "@msw/serve": "^0.11.0",
49
+ "@stylistic/eslint-plugin": "^5.10.0",
50
+ "@types/node": "^22.20.5",
51
+ "@vitejs/plugin-vue": "^6.0.10",
52
+ "@vitest/coverage-v8": "^5.0.5",
53
+ "@vitest/eslint-plugin": "^1.6.28",
54
+ "commit-and-tag-version": "^13.3.0",
55
+ "eslint": "^10.14.0",
56
+ "eslint-config-prettier": "10.1.8",
57
+ "eslint-plugin-vue": "^10.12.0",
58
+ "globals": "^17.14.0",
59
+ "msw": "^3.0.4",
60
+ "npm-run-all2": "^9.0.4",
61
+ "prettier": "3.9.10",
62
+ "tsc-alias": "^1.9.10",
63
+ "typescript": "^6.0.3",
64
+ "typescript-eslint": "^8.72.1",
65
+ "vite": "^8.3.5",
66
+ "vitest": "^5.0.5",
67
+ "vue": "^3.5.45",
68
+ "vue-tsc": "^3.3.14"
69
+ },
70
+ "dependencies": {
71
+ "jose": "^6.2.15",
72
+ "undici": "^8.12.3"
73
+ },
74
+ "peerDependencies": {
75
+ "vite": "^7.0.0 || ^8.0.0"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public",
79
+ "registry": "https://registry.npmjs.org/"
80
+ },
81
+ "keywords": [
82
+ "vite-plugin",
83
+ "authentication",
84
+ "auto-login"
85
+ ],
86
+ "compatiblePackages": {
87
+ "schemaVersion": 1,
88
+ "vite": {
89
+ "type": "compatible",
90
+ "versions": "^7.0.0 || ^8.0.0",
91
+ "note": "Development server only; requires Vite configureServer and middleware APIs."
92
+ },
93
+ "rollup": {
94
+ "type": "incompatible",
95
+ "reason": "Requires the Vite development server and its configureServer hook."
96
+ },
97
+ "rolldown": {
98
+ "type": "incompatible",
99
+ "reason": "Requires the Vite development server and its configureServer hook."
100
+ }
101
+ },
102
+ "overrides": {
103
+ "minimatch@^10.0.0": "^11.0.5"
104
+ },
105
+ "repository": {
106
+ "type": "git",
107
+ "url": "git+https://github.com/core-spec-tech/vite-plugin-api-auto-login.git"
108
+ },
109
+ "homepage": "https://github.com/core-spec-tech/vite-plugin-api-auto-login#readme",
110
+ "bugs": {
111
+ "url": "https://github.com/core-spec-tech/vite-plugin-api-auto-login/issues"
112
+ }
113
+ }