@corespec/vite-plugin-api-auto-login 0.0.0-stage → 0.2.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,273 @@
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 request; it cannot be combined with a custom
50
+ `request`. The exported `createFormDataRequest(config, fields)` function returns
51
+ a request callback for consumers who want to compose the default behavior.
52
+
53
+ ## Customizing the login request
54
+
55
+ Pass a function that receives the parsed configuration and returns the URL and
56
+ Undici fetch options. It can return a promise. Create a fresh request body each
57
+ time the callback runs:
58
+
59
+ ```ts
60
+ import { defineConfig } from 'vite'
61
+ import { viteAPIAutoLogin } from '@corespec/vite-plugin-api-auto-login'
62
+
63
+ export default defineConfig({
64
+ plugins: [
65
+ viteAPIAutoLogin({
66
+ request: (config) => {
67
+ if (!config.loginUrl || !config.username || !config.password) {
68
+ throw new Error('JSON login requires an endpoint, username, and password')
69
+ }
70
+ return {
71
+ url: config.loginUrl,
72
+ init: {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({
76
+ email: config.username,
77
+ password: config.password,
78
+ }),
79
+ },
80
+ }
81
+ },
82
+ }),
83
+ ],
84
+ })
85
+ ```
86
+
87
+ The callback runs lazily when a login is needed. Concurrent requests share one
88
+ login through RxJS, and each Vite server has its own token cache. JWTs are reused
89
+ until their `exp` timestamp; opaque tokens and tokens without readable expiry are
90
+ not cached between completed logins. Errors are logged and requests continue;
91
+ the next request can attempt authentication again. There are no automatic retries.
92
+
93
+ Custom callbacks validate their own requirements and may omit the default login
94
+ variables. Without a callback, the default sends a multipart POST with the mapped
95
+ username/password fields and a `User-Agent` header.
96
+
97
+ `init` supports Undici options including `signal` and `redirect`. The plugin
98
+ combines your signal with its lifecycle cancellation and manages `dispatcher`
99
+ through the TLS configuration. The response must be successful JSON containing
100
+ a non-empty `token` string.
101
+
102
+ ## Cookie options
103
+
104
+ ```ts
105
+ viteAPIAutoLogin({
106
+ cookie: {
107
+ path: '/api',
108
+ sameSite: 'strict',
109
+ secure: false,
110
+ httpOnly: true,
111
+ },
112
+ })
113
+ ```
114
+
115
+ Defaults preserve the original behavior: `path: '/'`, `sameSite: 'lax'`,
116
+ `secure: false`, and `httpOnly: false`. `sameSite` accepts `lax`, `strict`, or
117
+ `none`; `none` requires `secure: true`. Enable `httpOnly` when your frontend does
118
+ not need JavaScript access to the cookie. `secure` cookies require HTTPS in the
119
+ normal browser cookie model.
120
+
121
+ The name still comes from `API_AUTO_LOGIN_COOKIE_NAME`. Cookie `path` controls
122
+ where the browser sends the cookie, not which page requests trigger login.
123
+ Cookies are serialized with `cookie` and appended without replacing existing
124
+ `Set-Cookie` headers. Disconnected responses unsubscribe without continuing the
125
+ middleware chain. When the last waiting response disconnects, the login is aborted;
126
+ a later request starts a fresh login. Vite shutdown also cancels pending authentication.
127
+
128
+ ## Migrating from the class-based API
129
+
130
+ Version `0.2.0` changes the public API under the project's pre-1.0 versioning policy:
131
+
132
+ - Replace `request: instance` with `request: (config) => instance.create(config)`,
133
+ or inline the request construction as shown above.
134
+ - Replace `createAuthRequest` with a `request(config)` callback. The callback runs
135
+ per login, rather than once during server configuration.
136
+ - Replace `PluginConfig.fromEnv(env)` with `loadPluginConfig(env)`.
137
+ - Replace `new FormDataAuthRequest(config, fields)` with
138
+ `createFormDataRequest(config, fields)`. Call the returned function with `config`.
139
+ - `AuthRequest` is now a function type; `AuthRequestFactory` is removed.
140
+ - `AuthCookieOptions` is exported for reusable cookie configuration.
141
+
142
+ ## Environment variables
143
+
144
+ 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.
145
+
146
+ Required by the default form-data request only:
147
+
148
+ | Variable | Description |
149
+ | -------------------------- | -------------------------------------------------------------------------------------------- |
150
+ | `API_AUTO_LOGIN_LOGIN_URL` | Login endpoint URL used to request an authentication token. |
151
+ | `API_AUTO_LOGIN_USERNAME` | Account username sent to the login endpoint; may be an email address if the API supports it. |
152
+ | `API_AUTO_LOGIN_PASSWORD` | Account password sent to the login endpoint. |
153
+
154
+ Optional variables:
155
+
156
+ | Variable | Default | Description |
157
+ | ----------------------------------- | ----------------- | ----------------------------------------------------------------------------- |
158
+ | `API_AUTO_LOGIN_COOKIE_NAME` | `your_token_name` | Name of the cookie added to the response with the authentication token. |
159
+ | `API_AUTO_LOGIN_USER_AGENT` | `vite` | Value sent in the login request's `User-Agent` HTTP header. |
160
+ | `API_AUTO_LOGIN_ALLOW_INSECURE_TLS` | `false` | Set to `true` to disable TLS certificate verification for the login endpoint. |
161
+
162
+ ## Configuration parsing
163
+
164
+ `loadPluginConfig(env)` converts the environment values returned by Vite's
165
+ `loadEnv` into a frozen configuration object. It applies shared defaults and
166
+ parses the TLS flag.
167
+
168
+ Login URL, username, and password are optional in the shared configuration.
169
+ `createFormDataRequest` validates them when constructing the default callback.
170
+ Custom request callbacks validate their own requirements.
171
+
172
+ ## Vue + MSW example
173
+
174
+ Run `npm ci` and `npm run example` from this repository, then open
175
+ [http://127.0.0.1:5173](http://127.0.0.1:5173).
176
+
177
+ The [example guide](examples/vue-msw/README.md) explains the mock API, cookie
178
+ flow, and `formFields: { username: 'email' }` mapping. No external API or real
179
+ credentials are needed.
180
+
181
+ ## Development
182
+
183
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for code conventions, design guidelines,
184
+ and the required checks.
185
+
186
+ Use the Node.js version pinned in `.nvmrc`. GitHub CI and publishing use the same
187
+ file. With nvm installed:
188
+
189
+ ```bash
190
+ nvm install
191
+ nvm use
192
+ npm ci
193
+ npm run check
194
+ npm run build
195
+ ```
196
+
197
+ `npm run check` runs formatting checks, linting, the full project type check, and all tests once.
198
+ Use `npm test` for watch mode or `npm run coverage` for a coverage report.
199
+ The build produces ESM, CommonJS, and TypeScript declarations.
200
+ Use `npm run format` to format files with Prettier (100-character print width).
201
+
202
+ Tests are grouped under `tests/auth`, `tests/config`, and `tests/http`.
203
+ `tests/plugin.spec.ts` covers plugin integration, with shared HTTP fixtures in
204
+ `tests/helpers`. Editor formatting defaults are defined in `.editorconfig`.
205
+ Local environment overrides (`.env.local` and `.env.*.local`) are ignored by Git.
206
+
207
+ ### Source imports
208
+
209
+ Use `@/` for imports from `src/`, including imports in tests:
210
+
211
+ ```ts
212
+ import { loadPluginConfig } from '@/config/loadPluginConfig'
213
+ ```
214
+
215
+ TypeScript and Vite/Vitest resolve this alias locally. The declaration build
216
+ uses `tsc-alias` to replace aliases with relative paths in published types;
217
+ consumers do not need to configure `@/`. Imports between test helpers stay relative.
218
+
219
+ ## Local linking
220
+
221
+ When developing this package alongside a consuming Vite application, run the linking commands from each repository's root directory:
222
+
223
+ ```bash
224
+ cd vite-plugin-api-auto-login
225
+ npm link
226
+
227
+ cd ../your-app
228
+ npm link @corespec/vite-plugin-api-auto-login
229
+ ```
230
+
231
+ ## Security scanning
232
+
233
+ GitHub Actions runs CodeQL for TypeScript and workflow code, dependency review
234
+ on pull requests, and scheduled npm audits. Run `npm run security:audit` locally;
235
+ high and critical dependency findings also block publishing.
236
+ See [security setup](CONTRIBUTING.md#security-checks) for thresholds and the GitHub
237
+ settings needed for secret scanning and merge protection.
238
+
239
+ ## Publishing
240
+
241
+ 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.
242
+
243
+ The initial version is `0.1.0`. Releases use Semantic Versioning and Conventional
244
+ Commits; see [CONTRIBUTING.md](CONTRIBUTING.md#versioning-and-releases) for the
245
+ release commands and the `0.x` versioning policy.
246
+
247
+ Release setup:
248
+
249
+ 1. Store a granular npm publishing token with access to the `@corespec` scope
250
+ as the repository Actions secret `NPM_TOKEN`. Use a stage-only token; CI does
251
+ not need to bypass 2FA. Keep local tokens in an ignored `.env` file.
252
+ 2. Run `npm ci`, `npm run check`, and `npm run example:build`.
253
+ 3. Use `npm run release` or an explicit bump command described in CONTRIBUTING.md.
254
+ Releases use version tags such as `v0.2.0`. Historical release tags were removed
255
+ during the repository history reset; published npm versions remain available.
256
+ 4. Push the release commit and tag with `git push --follow-tags`. The Publish
257
+ workflow checks, builds, and stages the package with provenance. The npm
258
+ token is available only to its staging step.
259
+ 5. Review the version in npm's Staged Packages tab and approve it with 2FA,
260
+ or run `npm stage approve <stage-id>` while logged in as a maintainer.
261
+ A successful staging workflow does not mean the version is publicly available.
262
+
263
+ To retry an existing tag, manually run the Publish workflow and supply that tag
264
+ (for example `v0.2.0`). It checks out the tagged source and verifies the version.
265
+
266
+ Trusted publishing can replace the token after configuring owner `core-spec-tech`,
267
+ repository `vite-plugin-api-auto-login`, and workflow `publish.yml` in npm's package
268
+ settings. Then remove `NODE_AUTH_TOKEN` from the staging step and delete
269
+ the repository secret.
270
+
271
+ GitHub CI runs on pushes and pull requests. You can configure Renovate through its GitHub app using `renovate.json`.
272
+
273
+ See [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) for details.
@@ -0,0 +1,8 @@
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 type AuthRequest = (config: Readonly<PluginEnv>) => AuthRequestOptions | Promise<AuthRequestOptions>;
8
+ //# 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;AACzC,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,MAAM,WAAW,GAAG,CACxB,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,KACxB,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { FormFieldNames, PluginEnv } from '../config/types.js';
2
+ import type { AuthRequest } from './AuthRequest.js';
3
+ export declare function createFormDataRequest(config: Readonly<PluginEnv>, fields?: Readonly<FormFieldNames>): AuthRequest;
4
+ //# sourceMappingURL=createFormDataRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createFormDataRequest.d.ts","sourceRoot":"","sources":["../../src/auth/createFormDataRequest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAErD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,EAC3B,MAAM,GAAE,QAAQ,CAAC,cAAc,CAAM,GACpC,WAAW,CAkBb"}
@@ -0,0 +1,14 @@
1
+ import type { Observable } from 'rxjs';
2
+ import { fetch as undiciFetch } from 'undici';
3
+ import type { Dispatcher } from 'undici';
4
+ import type { PluginEnv } from '../config/types.js';
5
+ import type { AuthRequest } from './AuthRequest.js';
6
+ type TokenStreamDependencies = {
7
+ request?: AuthRequest;
8
+ fetch?: typeof undiciFetch;
9
+ createDispatcher?: () => Dispatcher;
10
+ signal?: AbortSignal;
11
+ };
12
+ export declare function createTokenStream(config: Readonly<PluginEnv>, dependencies?: TokenStreamDependencies): Observable<string>;
13
+ export {};
14
+ //# sourceMappingURL=createTokenStream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createTokenStream.d.ts","sourceRoot":"","sources":["../../src/auth/createTokenStream.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AACtC,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;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIrD,KAAK,uBAAuB,GAAG;IAC7B,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,KAAK,CAAC,EAAE,OAAO,WAAW,CAAA;IAC1B,gBAAgB,CAAC,EAAE,MAAM,UAAU,CAAA;IACnC,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB,CAAA;AAED,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,EAC3B,YAAY,GAAE,uBAA4B,GACzC,UAAU,CAAC,MAAM,CAAC,CAoDpB"}
@@ -0,0 +1,5 @@
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 function loadPluginConfig(env: PluginEnvInput): Readonly<PluginEnv>;
5
+ //# sourceMappingURL=loadPluginConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadPluginConfig.d.ts","sourceRoot":"","sources":["../../src/config/loadPluginConfig.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,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,GAAG,QAAQ,CAAC,SAAS,CAAC,CASzE"}
@@ -0,0 +1,30 @@
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 FormFieldNames = {
12
+ username?: string;
13
+ password?: string;
14
+ };
15
+ export type AuthCookieOptions = {
16
+ path?: string;
17
+ sameSite?: 'lax' | 'strict' | 'none';
18
+ secure?: boolean;
19
+ httpOnly?: boolean;
20
+ };
21
+ export type AutoLoginOptions = {
22
+ cookie?: AuthCookieOptions;
23
+ } & ({
24
+ formFields?: FormFieldNames;
25
+ request?: never;
26
+ } | {
27
+ formFields?: never;
28
+ request: AuthRequest;
29
+ });
30
+ //# 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,cAAc,GAAG;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;IACpC,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAAE,GAAG,CAC9D;IAAE,UAAU,CAAC,EAAE,cAAc,CAAC;IAAC,OAAO,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,UAAU,CAAC,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,WAAW,CAAA;CAAE,CAChG,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,3 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+ export declare function isHtmlDocumentRequest(req: IncomingMessage): boolean;
3
+ //# sourceMappingURL=isHtmlDocumentRequest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isHtmlDocumentRequest.d.ts","sourceRoot":"","sources":["../../src/http/isHtmlDocumentRequest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAKnE"}
@@ -0,0 +1,6 @@
1
+ export { viteAPIAutoLogin } from './plugin.js';
2
+ export { createFormDataRequest } from './auth/createFormDataRequest.js';
3
+ export { loadPluginConfig } from './config/loadPluginConfig.js';
4
+ export type { AuthRequest, AuthRequestOptions } from './auth/AuthRequest.js';
5
+ export type { AutoLoginOptions, PluginEnv, FormFieldNames, AuthCookieOptions } from './config/types.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,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAC5D,YAAY,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACzE,YAAY,EAAE,gBAAgB,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1,5 @@
1
+ import type { Observable } from 'rxjs';
2
+ import type { Connect, Logger } from 'vite';
3
+ import type { AuthCookieOptions, PluginEnv } from '../config/types.js';
4
+ export declare function createAutoLoginMiddleware(config: Readonly<PluginEnv>, cookie: Required<AuthCookieOptions>, token$: Observable<string>, logger: Pick<Logger, 'error'>, signal: AbortSignal): Connect.NextHandleFunction;
5
+ //# sourceMappingURL=createAutoLoginMiddleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createAutoLoginMiddleware.d.ts","sourceRoot":"","sources":["../../src/middleware/createAutoLoginMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AACtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE3C,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAIlE,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC,EAC3B,MAAM,EAAE,QAAQ,CAAC,iBAAiB,CAAC,EACnC,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,EAC1B,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,kBAAkB,CA0C5B"}
@@ -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":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAOlC,OAAO,KAAK,EAAqB,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAGzE,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAoDvE"}
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("cookie"),t=require("vite"),n=require("undici"),r=require("jose"),i=require("rxjs");function a(e,t={}){let r=t.username??`username`,i=t.password??`password`;if(!r.trim()||!i.trim()||r===i)throw Error(`Form field names must be non-empty and distinct`);return o(e),e=>{let{loginUrl:t,username:a,password:s}=o(e),c=new n.FormData;return c.set(r,a),c.set(i,s),{url:t,init:{method:`POST`,headers:{"User-Agent":e.userAgent},body:c}}}}function o(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}}function s(e,t={}){let r=t.request??a(e),o=t.fetch??n.fetch,s;return(0,i.defer)(()=>{if(t.signal?.aborted)return i.EMPTY;if(s?.expiresAt!==void 0&&s.expiresAt>Date.now())return(0,i.of)(s.value);let a=new AbortController,l;return e.allowInsecureTls&&(l=t.createDispatcher?.()??new n.Agent({connect:{rejectUnauthorized:!1}})),(0,i.defer)(()=>(0,i.from)(Promise.resolve(r(e)))).pipe((0,i.switchMap)(({url:e,init:n})=>{let r=[a.signal];return n.signal&&r.push(n.signal),t.signal&&r.push(t.signal),(0,i.from)(o(e,{...n,dispatcher:l,signal:AbortSignal.any(r)}))}),(0,i.switchMap)(e=>{if(!e.ok)throw Error(`Login request failed with status ${e.status}`);return(0,i.from)(e.json())}),(0,i.map)(c),(0,i.tap)(e=>{s=e}),(0,i.map)(e=>e.value),(0,i.takeUntil)(t.signal?(0,i.fromEvent)(t.signal,`abort`):i.EMPTY),(0,i.finalize)(()=>{a.abort(),l&&l.destroy().catch(()=>{})}))}).pipe((0,i.share)({connector:()=>new i.ReplaySubject(1),resetOnError:!0,resetOnComplete:!0,resetOnRefCountZero:!0}))}function c(e){if(!e||typeof e!=`object`||!(`token`in e)||typeof e.token!=`string`||e.token.length===0)throw Error(`Login response did not include a token`);let t=e.token;try{let{exp:e}=(0,r.decodeJwt)(t);return{value:t,expiresAt:typeof e==`number`?e*1e3:void 0}}catch{return{value:t}}}var l=`vite-plugin-api-auto-login`;function u(e){return Object.freeze({allowInsecureTls:e.API_AUTO_LOGIN_ALLOW_INSECURE_TLS===`true`,cookieName:e.API_AUTO_LOGIN_COOKIE_NAME??`your_token_name`,username:e.API_AUTO_LOGIN_USERNAME,loginUrl:e.API_AUTO_LOGIN_LOGIN_URL,password:e.API_AUTO_LOGIN_PASSWORD,userAgent:e.API_AUTO_LOGIN_USER_AGENT??`vite`})}function d(e){let t=e.method===`GET`||e.method===`HEAD`,n=e.headers.accept?.includes(`text/html`)??!1,r=e.headers.upgrade?.toLowerCase()===`websocket`;return t&&n&&!r}function f(t,n,r,a,o){return(s,c,u)=>{if(o.aborted||c.destroyed||c.writableEnded)return;let f=new i.Subscription,p=()=>f.unsubscribe(),m=()=>{o.aborted||c.destroyed||c.writableEnded||f.closed||u()};c.once(`close`,p),o.addEventListener(`abort`,p,{once:!0}),f.add(()=>{c.off(`close`,p),o.removeEventListener(`abort`,p)}),f.add((0,i.of)(s).pipe((0,i.filter)(d),(0,i.switchMap)(()=>r),(0,i.take)(1),(0,i.map)(r=>(0,e.stringifySetCookie)({...n,name:t.cookieName,value:r})),(0,i.tap)(e=>{c.headersSent||c.destroyed||c.writableEnded||c.appendHeader(`Set-Cookie`,e)})).subscribe({error:e=>{let n=e instanceof Error?e.message:String(e),r=t.loginUrl?` (${t.loginUrl})`:``;a.error(`[${l}] ${n}${r}`),m(),f.unsubscribe()},complete:()=>{m(),f.unsubscribe()}}))}}function p(n={}){if(`createAuthRequest`in n)throw Error(`createAuthRequest was removed; use request(config) instead`);if(n.request!==void 0&&typeof n.request!=`function`)throw Error(`request must be a function: request(config)`);if(n.formFields!==void 0&&n.request!==void 0)throw Error(`Use formFields only with the default request`);let r={path:n.cookie?.path??`/`,sameSite:n.cookie?.sameSite??`lax`,secure:n.cookie?.secure??!1,httpOnly:n.cookie?.httpOnly??!1};if(r.sameSite===`none`&&!r.secure)throw Error(`cookie.sameSite "none" requires cookie.secure to be true`);(0,e.stringifySetCookie)({...r,name:`validation`,value:``});let i=new Set;return{name:l,apply:`serve`,configureServer(o){let{mode:c,envDir:d,logger:p}=o.config;try{let l=u((0,t.loadEnv)(c,d,`API_AUTO_LOGIN_`));(0,e.stringifySetCookie)({...r,name:l.cookieName,value:``});let m=n.request??a(l,n.formFields),h=new AbortController,g=()=>{h.abort(),o.httpServer?.off(`close`,g),i.delete(g)};i.add(g),o.httpServer?.once(`close`,g);let _=s(l,{request:m,signal:h.signal});o.middlewares.use(f(l,r,_,p,h.signal))}catch(e){p.error(`[${l}] ${e instanceof Error?e.message:String(e)}`)}},closeBundle(){for(let e of i)e()}}}exports.createFormDataRequest=a,exports.loadPluginConfig=u,exports.viteAPIAutoLogin=p;
@@ -0,0 +1,164 @@
1
+ import { stringifySetCookie as e } from "cookie";
2
+ import { loadEnv as t } from "vite";
3
+ import { Agent as n, FormData as r, fetch as i } from "undici";
4
+ import { decodeJwt as a } from "jose";
5
+ import { EMPTY as o, ReplaySubject as s, Subscription as c, defer as l, filter as u, finalize as d, from as f, fromEvent as p, map as m, of as h, share as g, switchMap as _, take as v, takeUntil as y, tap as b } from "rxjs";
6
+ // src/auth/createFormDataRequest.ts
7
+ function x(e, t = {}) {
8
+ let n = t.username ?? "username", i = t.password ?? "password";
9
+ if (!n.trim() || !i.trim() || n === i) throw Error("Form field names must be non-empty and distinct");
10
+ return S(e), (e) => {
11
+ let { loginUrl: t, username: a, password: o } = S(e), s = new r();
12
+ return s.set(n, a), s.set(i, o), {
13
+ url: t,
14
+ init: {
15
+ method: "POST",
16
+ headers: { "User-Agent": e.userAgent },
17
+ body: s
18
+ }
19
+ };
20
+ };
21
+ }
22
+ function S(e) {
23
+ let { loginUrl: t, username: n, password: r } = e;
24
+ if (!t || !n || !r) throw Error("Missing required env vars: API_AUTO_LOGIN_LOGIN_URL, API_AUTO_LOGIN_USERNAME, API_AUTO_LOGIN_PASSWORD");
25
+ return {
26
+ loginUrl: t,
27
+ username: n,
28
+ password: r
29
+ };
30
+ }
31
+ // src/auth/createTokenStream.ts
32
+ function C(e, t = {}) {
33
+ let r = t.request ?? x(e), a = t.fetch ?? i, c;
34
+ return l(() => {
35
+ if (t.signal?.aborted) return o;
36
+ if (c?.expiresAt !== void 0 && c.expiresAt > Date.now()) return h(c.value);
37
+ let i = new AbortController(), s;
38
+ return e.allowInsecureTls && (s = t.createDispatcher?.() ?? new n({ connect: { rejectUnauthorized: !1 } })), l(() => f(Promise.resolve(r(e)))).pipe(_(({ url: e, init: n }) => {
39
+ let r = [i.signal];
40
+ return n.signal && r.push(n.signal), t.signal && r.push(t.signal), f(a(e, {
41
+ ...n,
42
+ dispatcher: s,
43
+ signal: AbortSignal.any(r)
44
+ }));
45
+ }), _((e) => {
46
+ if (!e.ok) throw Error(`Login request failed with status ${e.status}`);
47
+ return f(e.json());
48
+ }), m(w), b((e) => {
49
+ c = e;
50
+ }), m((e) => e.value), y(t.signal ? p(t.signal, "abort") : o), d(() => {
51
+ i.abort(), s && s.destroy().catch(() => {});
52
+ }));
53
+ }).pipe(g({
54
+ connector: () => new s(1),
55
+ resetOnError: !0,
56
+ resetOnComplete: !0,
57
+ resetOnRefCountZero: !0
58
+ }));
59
+ }
60
+ function w(e) {
61
+ if (!e || typeof e != "object" || !("token" in e) || typeof e.token != "string" || e.token.length === 0) throw Error("Login response did not include a token");
62
+ let t = e.token;
63
+ try {
64
+ let { exp: e } = a(t);
65
+ return {
66
+ value: t,
67
+ expiresAt: typeof e == "number" ? e * 1e3 : void 0
68
+ };
69
+ } catch {
70
+ return { value: t };
71
+ }
72
+ }
73
+ // src/constants.ts
74
+ var T = "vite-plugin-api-auto-login";
75
+ function E(e) {
76
+ return Object.freeze({
77
+ allowInsecureTls: e.API_AUTO_LOGIN_ALLOW_INSECURE_TLS === "true",
78
+ cookieName: e.API_AUTO_LOGIN_COOKIE_NAME ?? "your_token_name",
79
+ username: e.API_AUTO_LOGIN_USERNAME,
80
+ loginUrl: e.API_AUTO_LOGIN_LOGIN_URL,
81
+ password: e.API_AUTO_LOGIN_PASSWORD,
82
+ userAgent: e.API_AUTO_LOGIN_USER_AGENT ?? "vite"
83
+ });
84
+ }
85
+ // src/http/isHtmlDocumentRequest.ts
86
+ function D(e) {
87
+ let t = e.method === "GET" || e.method === "HEAD", n = e.headers.accept?.includes("text/html") ?? !1, r = e.headers.upgrade?.toLowerCase() === "websocket";
88
+ return t && n && !r;
89
+ }
90
+ // src/middleware/createAutoLoginMiddleware.ts
91
+ function O(t, n, r, i, a) {
92
+ return (o, s, l) => {
93
+ if (a.aborted || s.destroyed || s.writableEnded) return;
94
+ let d = new c(), f = () => d.unsubscribe(), p = () => {
95
+ a.aborted || s.destroyed || s.writableEnded || d.closed || l();
96
+ };
97
+ s.once("close", f), a.addEventListener("abort", f, { once: !0 }), d.add(() => {
98
+ s.off("close", f), a.removeEventListener("abort", f);
99
+ }), d.add(h(o).pipe(u(D), _(() => r), v(1), m((r) => e({
100
+ ...n,
101
+ name: t.cookieName,
102
+ value: r
103
+ })), b((e) => {
104
+ s.headersSent || s.destroyed || s.writableEnded || s.appendHeader("Set-Cookie", e);
105
+ })).subscribe({
106
+ error: (e) => {
107
+ let n = e instanceof Error ? e.message : String(e), r = t.loginUrl ? ` (${t.loginUrl})` : "";
108
+ i.error(`[${T}] ${n}${r}`), p(), d.unsubscribe();
109
+ },
110
+ complete: () => {
111
+ p(), d.unsubscribe();
112
+ }
113
+ }));
114
+ };
115
+ }
116
+ // src/plugin.ts
117
+ function k(n = {}) {
118
+ if ("createAuthRequest" in n) throw Error("createAuthRequest was removed; use request(config) instead");
119
+ if (n.request !== void 0 && typeof n.request != "function") throw Error("request must be a function: request(config)");
120
+ if (n.formFields !== void 0 && n.request !== void 0) throw Error("Use formFields only with the default request");
121
+ let r = {
122
+ path: n.cookie?.path ?? "/",
123
+ sameSite: n.cookie?.sameSite ?? "lax",
124
+ secure: n.cookie?.secure ?? !1,
125
+ httpOnly: n.cookie?.httpOnly ?? !1
126
+ };
127
+ if (r.sameSite === "none" && !r.secure) throw Error("cookie.sameSite \"none\" requires cookie.secure to be true");
128
+ e({
129
+ ...r,
130
+ name: "validation",
131
+ value: ""
132
+ });
133
+ let i = /* @__PURE__ */ new Set();
134
+ return {
135
+ name: T,
136
+ apply: "serve",
137
+ configureServer(a) {
138
+ let { mode: o, envDir: s, logger: c } = a.config;
139
+ try {
140
+ let l = E(t(o, s, "API_AUTO_LOGIN_"));
141
+ e({
142
+ ...r,
143
+ name: l.cookieName,
144
+ value: ""
145
+ });
146
+ let u = n.request ?? x(l, n.formFields), d = new AbortController(), f = () => {
147
+ d.abort(), a.httpServer?.off("close", f), i.delete(f);
148
+ };
149
+ i.add(f), a.httpServer?.once("close", f);
150
+ let p = C(l, {
151
+ request: u,
152
+ signal: d.signal
153
+ });
154
+ a.middlewares.use(O(l, r, p, c, d.signal));
155
+ } catch (e) {
156
+ c.error(`[${T}] ${e instanceof Error ? e.message : String(e)}`);
157
+ }
158
+ },
159
+ closeBundle() {
160
+ for (let e of i) e();
161
+ }
162
+ };
163
+ }
164
+ export { x as createFormDataRequest, E as loadPluginConfig, k as viteAPIAutoLogin };
package/package.json CHANGED
@@ -1,6 +1,115 @@
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.2.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
+ "cookie": "^2.0.2",
72
+ "jose": "^6.2.15",
73
+ "rxjs": "^7.8.2",
74
+ "undici": "^8.12.3"
75
+ },
76
+ "peerDependencies": {
77
+ "vite": "^7.0.0 || ^8.0.0"
78
+ },
79
+ "publishConfig": {
80
+ "access": "public",
81
+ "registry": "https://registry.npmjs.org/"
82
+ },
83
+ "keywords": [
84
+ "vite-plugin",
85
+ "authentication",
86
+ "auto-login"
87
+ ],
88
+ "compatiblePackages": {
89
+ "schemaVersion": 1,
90
+ "vite": {
91
+ "type": "compatible",
92
+ "versions": "^7.0.0 || ^8.0.0",
93
+ "note": "Development server only; requires Vite configureServer and middleware APIs."
94
+ },
95
+ "rollup": {
96
+ "type": "incompatible",
97
+ "reason": "Requires the Vite development server and its configureServer hook."
98
+ },
99
+ "rolldown": {
100
+ "type": "incompatible",
101
+ "reason": "Requires the Vite development server and its configureServer hook."
102
+ }
103
+ },
104
+ "overrides": {
105
+ "minimatch@^10.0.0": "^11.0.5"
106
+ },
107
+ "repository": {
108
+ "type": "git",
109
+ "url": "git+https://github.com/core-spec-tech/vite-plugin-api-auto-login.git"
110
+ },
111
+ "homepage": "https://github.com/core-spec-tech/vite-plugin-api-auto-login#readme",
112
+ "bugs": {
113
+ "url": "https://github.com/core-spec-tech/vite-plugin-api-auto-login/issues"
114
+ }
115
+ }