@logi-auth/browser 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 ADDED
@@ -0,0 +1,120 @@
1
+ # @logi-auth/browser
2
+
3
+ Browser SDK for **logi (1pass)** — OAuth 2.0 + OIDC PKCE for SPAs. Zero dependencies, ~3 KB minified.
4
+
5
+ ```bash
6
+ npm install @logi-auth/browser
7
+ ```
8
+
9
+ ## Quickstart
10
+
11
+ ```ts
12
+ import { LogiAuth } from "@logi-auth/browser";
13
+
14
+ const auth = new LogiAuth({
15
+ clientId: "logi_xxx",
16
+ redirectUri: window.location.origin + "/auth/callback",
17
+ // scopes: ["openid", "profile:basic", "email"], // default
18
+ // issuer: "https://api.1pass.dev", // default
19
+ });
20
+ ```
21
+
22
+ ### Sign in (page A — wherever the login button lives)
23
+
24
+ ```ts
25
+ loginButton.addEventListener("click", () => {
26
+ auth.signIn({ returnTo: location.pathname });
27
+ // → redirects to https://api.1pass.dev/oauth/authorize
28
+ });
29
+ ```
30
+
31
+ ### Handle callback (page B — `/auth/callback`)
32
+
33
+ ```ts
34
+ if (auth.hasPendingCallback()) {
35
+ try {
36
+ const tokens = await auth.handleCallback();
37
+ // tokens.accessToken — Bearer token for your API
38
+ // tokens.refreshToken — store securely (preferably HttpOnly cookie via your backend)
39
+ // tokens.idToken — OIDC identity (decode for UI hints only)
40
+ // tokens.returnTo — what you passed to signIn({ returnTo })
41
+ // tokens.expiresAt — ms epoch
42
+ location.replace(tokens.returnTo ?? "/");
43
+ } catch (err) {
44
+ if (err instanceof LogiAuthError) {
45
+ console.error(err.code, err.message, err.details);
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### Refresh
52
+
53
+ ```ts
54
+ const fresh = await auth.refresh(savedRefreshToken);
55
+ // fresh.refreshToken is the rotated token — persist the new value.
56
+ ```
57
+
58
+ ### Read the ID token (UI hints only)
59
+
60
+ ```ts
61
+ const claims = auth.parseIdToken<{ sub: string; email?: string }>(tokens.idToken!);
62
+ // ⚠️ No signature verification. Don't make authorization decisions client-side.
63
+ ```
64
+
65
+ ## Why this SDK
66
+
67
+ Browser PKCE flow is small but easy to get wrong:
68
+ - Generating the `code_verifier` + SHA-256 `code_challenge`
69
+ - Persisting `verifier` + `state` across the IdP redirect
70
+ - Validating returned `state` to defeat CSRF
71
+ - Distinguishing `error=` callbacks from missing-`code` cases
72
+ - Cleaning up `sessionStorage` on every exit path (success or failure)
73
+
74
+ This SDK does all of that in ~250 LOC, zero deps, ESM-only.
75
+
76
+ ## Design
77
+
78
+ - **Zero dependencies.** Uses `crypto.subtle` and `fetch` directly.
79
+ - **Public client.** Never sends `client_secret`. Token endpoint must accept `none` auth (logi PKCE clients do).
80
+ - **No signature verification.** ID token claims are decoded for UI only; your backend is the trust root and re-verifies via `/.well-known/jwks.json`.
81
+ - **sessionStorage by default.** Pending handoff is wiped on tab close. Override via `storage:` option.
82
+ - **TTL on pending handoff.** Stale handoffs (default 10 min) are rejected with `expired_handoff`.
83
+
84
+ ## Requirements
85
+
86
+ - **Secure context.** `crypto.subtle` is undefined on plain `http://` (except `http://localhost`). Serve your SPA over HTTPS.
87
+ - **Modern browsers.** Chromium 92+, Safari 15.4+, Firefox 90+ (anything with `crypto.subtle.digest("SHA-256")` and `fetch`).
88
+ - **Node ≥ 18** if you import this from a server-side test harness or SSR layer.
89
+
90
+ ## Errors
91
+
92
+ `LogiAuthError` with one of:
93
+
94
+ - `storage_unavailable` — `signIn()` couldn't persist the PKCE handoff (Safari ITP, iOS private browsing, corp policy). Thrown **before** redirecting to the IdP so the user doesn't waste a round-trip.
95
+ - `no_pending_handoff` — `handleCallback()` called without a prior `signIn()` in this tab
96
+ - `state_mismatch` — returned `state` ≠ persisted (CSRF attempt or stale callback)
97
+ - `missing_code` — callback URL had no `code` parameter
98
+ - `authorization_server_error` — IdP returned `?error=...`
99
+ - `token_exchange_failed` — `/oauth/token` POST failed (HTTP status + truncated body in `details`)
100
+ - `network_error` — `fetch` rejected (offline, DNS, CORS, TLS)
101
+ - `expired_handoff` — pending older than `pendingTtlMs`
102
+
103
+ > **`details.body` may include server payloads.** We truncate to 2 KB but logging it to Sentry/Datadog without scrubbing could leak tokens that the IdP echoed in a 4xx response.
104
+
105
+ ## Limitations (v0.1.0)
106
+
107
+ - **Multi-tab race.** Concurrent `signIn()` calls in multiple tabs share `sessionStorage` per origin, so only the most-recent handoff completes; the older tab's `handleCallback()` will fail with `state_mismatch`. State-keyed storage is on the v0.2.0 roadmap.
108
+ - **No automatic refresh.** Call `auth.refresh(savedRefreshToken)` yourself before `expiresAt`. A token-manager wrapper (`@logi-auth/react`) is planned.
109
+
110
+ ## Server side
111
+
112
+ This SDK only handles the browser. Your backend should:
113
+ 1. Validate `accessToken` against `/.well-known/jwks.json` on every protected request
114
+ 2. Store `refreshToken` server-side (HttpOnly cookie) — don't keep it in `localStorage`
115
+
116
+ For Node.js servers, use a generic OIDC library pointed at `https://api.1pass.dev/.well-known/openid-configuration`. logi advertises a full discovery document so `oauth4webapi`, `openid-client`, `next-auth`, `auth.js` all auto-configure.
117
+
118
+ ## License
119
+
120
+ MIT © Seunghan Kim
@@ -0,0 +1,82 @@
1
+ import { type StorageBackend } from "./storage.js";
2
+ export interface LogiAuthOptions {
3
+ /** OAuth client_id from logi developer portal. Public PKCE client. */
4
+ clientId: string;
5
+ /** Where the IdP returns the user. Must be one of the registered redirect_uris. */
6
+ redirectUri: string;
7
+ /** Default scopes; override per-call via signIn({ scopes }). */
8
+ scopes?: string[];
9
+ /** Issuer URL. Defaults to https://api.1pass.dev. */
10
+ issuer?: string;
11
+ /** Override storage backend (default: sessionStorage). */
12
+ storage?: StorageBackend;
13
+ /** Maximum age of a pending handoff in ms (default: 10 minutes). */
14
+ pendingTtlMs?: number;
15
+ }
16
+ export interface SignInRequest {
17
+ /** Override default scopes. */
18
+ scopes?: string[];
19
+ /** Caller-supplied passthrough — restored in handleCallback().returnTo. */
20
+ returnTo?: string;
21
+ /** OIDC `prompt` parameter (e.g. "login" or "consent"). */
22
+ prompt?: "none" | "login" | "consent" | "select_account";
23
+ }
24
+ export interface TokenResponse {
25
+ accessToken: string;
26
+ idToken?: string;
27
+ refreshToken?: string;
28
+ tokenType: string;
29
+ expiresAt?: number;
30
+ scope?: string;
31
+ }
32
+ export interface CallbackResult extends TokenResponse {
33
+ /** The `returnTo` value passed to signIn(), if any. */
34
+ returnTo?: string;
35
+ }
36
+ export type LogiAuthErrorCode = "no_pending_handoff" | "state_mismatch" | "missing_code" | "authorization_server_error" | "token_exchange_failed" | "expired_handoff" | "storage_unavailable" | "network_error";
37
+ export declare class LogiAuthError extends Error {
38
+ readonly code: LogiAuthErrorCode;
39
+ readonly details?: unknown | undefined;
40
+ constructor(code: LogiAuthErrorCode, message: string, details?: unknown | undefined);
41
+ }
42
+ export declare class LogiAuth {
43
+ readonly issuer: string;
44
+ readonly clientId: string;
45
+ readonly redirectUri: string;
46
+ readonly defaultScopes: string[];
47
+ private readonly storage;
48
+ private readonly pendingTtlMs;
49
+ constructor(opts: LogiAuthOptions);
50
+ /**
51
+ * Build the authorize URL and navigate the browser to it. Persists the PKCE
52
+ * verifier + state to sessionStorage so handleCallback() can complete the
53
+ * exchange after the IdP redirects back.
54
+ */
55
+ signIn(req?: SignInRequest): Promise<void>;
56
+ /**
57
+ * Read the current page URL for ?code & ?state, validate against the
58
+ * persisted handoff, and exchange the code for tokens. Call this from your
59
+ * `/auth/callback` route.
60
+ *
61
+ * Pass an explicit URL if you've already routed past the callback (rare).
62
+ */
63
+ handleCallback(callbackUrl?: string | URL): Promise<CallbackResult>;
64
+ /**
65
+ * Exchange a refresh_token for a fresh access_token. Public clients should
66
+ * persist the rotated refresh_token returned in `refreshToken`.
67
+ */
68
+ refresh(refreshToken: string): Promise<TokenResponse>;
69
+ /**
70
+ * Decode an ID token's payload (no signature verification). Use only for
71
+ * UI hints (e.g. show user's email). Real authorization decisions must be
72
+ * made server-side after re-verifying via JWKS.
73
+ */
74
+ parseIdToken<T = Record<string, unknown>>(idToken: string): T;
75
+ /**
76
+ * True when sessionStorage has a pending sign-in (i.e. user just returned
77
+ * from the IdP). Use to gate calling handleCallback() in shared components.
78
+ */
79
+ hasPendingCallback(): boolean;
80
+ }
81
+ export type { StorageBackend, PendingHandoff } from "./storage.js";
82
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,KAAK,cAAc,EAKpB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB,CAAC;CAC1D;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GACzB,oBAAoB,GACpB,gBAAgB,GAChB,cAAc,GACd,4BAA4B,GAC5B,uBAAuB,GACvB,iBAAiB,GACjB,qBAAqB,GACrB,eAAe,CAAC;AAEpB,qBAAa,aAAc,SAAQ,KAAK;aAEpB,IAAI,EAAE,iBAAiB;aAEvB,OAAO,CAAC,EAAE,OAAO;gBAFjB,IAAI,EAAE,iBAAiB,EACvC,OAAO,EAAE,MAAM,EACC,OAAO,CAAC,EAAE,OAAO,YAAA;CAKpC;AAED,qBAAa,QAAQ;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,IAAI,EAAE,eAAe;IAWjC;;;;OAIG;IACG,MAAM,CAAC,GAAG,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0CpD;;;;;;OAMG;IACG,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC;IAqGzE;;;OAGG;IACG,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA2C3D;;;;OAIG;IACH,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC;IAa7D;;;OAGG;IACH,kBAAkB,IAAI,OAAO;CAG9B;AAED,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,223 @@
1
+ // @logi-auth/browser — OAuth 2.0 + OIDC PKCE client for SPAs.
2
+ //
3
+ // Usage:
4
+ // const auth = new LogiAuth({
5
+ // clientId: 'logi_xxx',
6
+ // redirectUri: window.location.origin + '/auth/callback',
7
+ // });
8
+ //
9
+ // // Page A — kick off
10
+ // await auth.signIn();
11
+ //
12
+ // // Page B (callback) — finish
13
+ // const tokens = await auth.handleCallback();
14
+ //
15
+ // // Server validates id_token via /.well-known/jwks.json — this SDK does
16
+ // // NOT verify signatures (browsers can't keep the public-key cache safe
17
+ // // and the RP backend should be the trust root anyway).
18
+ import { generateCodeVerifier, deriveCodeChallenge, generateState } from "./pkce.js";
19
+ import { sessionStorageBackend, savePending, loadPending, clearPending, } from "./storage.js";
20
+ export class LogiAuthError extends Error {
21
+ code;
22
+ details;
23
+ constructor(code, message, details) {
24
+ super(message);
25
+ this.code = code;
26
+ this.details = details;
27
+ this.name = "LogiAuthError";
28
+ }
29
+ }
30
+ export class LogiAuth {
31
+ issuer;
32
+ clientId;
33
+ redirectUri;
34
+ defaultScopes;
35
+ storage;
36
+ pendingTtlMs;
37
+ constructor(opts) {
38
+ if (!opts.clientId)
39
+ throw new Error("LogiAuth: clientId is required");
40
+ if (!opts.redirectUri)
41
+ throw new Error("LogiAuth: redirectUri is required");
42
+ this.clientId = opts.clientId;
43
+ this.redirectUri = opts.redirectUri;
44
+ this.issuer = (opts.issuer ?? "https://api.1pass.dev").replace(/\/+$/, "");
45
+ this.defaultScopes = opts.scopes ?? ["openid", "profile:basic", "email"];
46
+ this.storage = opts.storage ?? sessionStorageBackend;
47
+ this.pendingTtlMs = opts.pendingTtlMs ?? 10 * 60 * 1000;
48
+ }
49
+ /**
50
+ * Build the authorize URL and navigate the browser to it. Persists the PKCE
51
+ * verifier + state to sessionStorage so handleCallback() can complete the
52
+ * exchange after the IdP redirects back.
53
+ */
54
+ async signIn(req = {}) {
55
+ const verifier = generateCodeVerifier();
56
+ const challenge = await deriveCodeChallenge(verifier);
57
+ const state = generateState();
58
+ const scopes = (req.scopes ?? this.defaultScopes).join(" ");
59
+ // Persist BEFORE navigating. If sessionStorage is disabled (Safari ITP,
60
+ // iOS private browsing, corp policy), throw a typed error instead of
61
+ // redirecting the user into a flow that can't complete (codex P2
62
+ // 2026-05-15).
63
+ try {
64
+ savePending({
65
+ state,
66
+ verifier,
67
+ redirectUri: this.redirectUri,
68
+ returnTo: req.returnTo,
69
+ startedAt: Date.now(),
70
+ }, this.storage);
71
+ }
72
+ catch (cause) {
73
+ throw new LogiAuthError("storage_unavailable", "Could not persist PKCE handoff to sessionStorage (private browsing, ITP, or corp policy).", cause);
74
+ }
75
+ const url = new URL(`${this.issuer}/oauth/authorize`);
76
+ url.searchParams.set("response_type", "code");
77
+ url.searchParams.set("client_id", this.clientId);
78
+ url.searchParams.set("redirect_uri", this.redirectUri);
79
+ url.searchParams.set("scope", scopes);
80
+ url.searchParams.set("state", state);
81
+ url.searchParams.set("code_challenge", challenge);
82
+ url.searchParams.set("code_challenge_method", "S256");
83
+ if (req.prompt)
84
+ url.searchParams.set("prompt", req.prompt);
85
+ window.location.assign(url.toString());
86
+ }
87
+ /**
88
+ * Read the current page URL for ?code & ?state, validate against the
89
+ * persisted handoff, and exchange the code for tokens. Call this from your
90
+ * `/auth/callback` route.
91
+ *
92
+ * Pass an explicit URL if you've already routed past the callback (rare).
93
+ */
94
+ async handleCallback(callbackUrl) {
95
+ const url = new URL(callbackUrl ?? (typeof window !== "undefined" ? window.location.href : "http://localhost/"));
96
+ const params = url.searchParams;
97
+ const pending = loadPending(this.storage);
98
+ if (!pending) {
99
+ throw new LogiAuthError("no_pending_handoff", "No pending sign-in handoff in sessionStorage. Did you call signIn() in this tab?");
100
+ }
101
+ if (Date.now() - pending.startedAt > this.pendingTtlMs) {
102
+ clearPending(this.storage);
103
+ throw new LogiAuthError("expired_handoff", `Pending handoff older than ${this.pendingTtlMs}ms — the user took too long. Restart sign-in.`);
104
+ }
105
+ const errParam = params.get("error");
106
+ if (errParam) {
107
+ clearPending(this.storage);
108
+ throw new LogiAuthError("authorization_server_error", `Authorization server returned error: ${errParam}`, { error: errParam, errorDescription: params.get("error_description") });
109
+ }
110
+ const returnedState = params.get("state");
111
+ if (returnedState !== pending.state) {
112
+ clearPending(this.storage);
113
+ throw new LogiAuthError("state_mismatch", "state parameter mismatch — possible CSRF attempt or stale callback.");
114
+ }
115
+ const code = params.get("code");
116
+ if (!code) {
117
+ clearPending(this.storage);
118
+ throw new LogiAuthError("missing_code", "Callback URL had no `code` parameter.");
119
+ }
120
+ // PKCE token exchange. Public client → no client_secret.
121
+ let tokenResp;
122
+ try {
123
+ tokenResp = await fetch(`${this.issuer}/oauth/token`, {
124
+ method: "POST",
125
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
126
+ body: new URLSearchParams({
127
+ grant_type: "authorization_code",
128
+ code,
129
+ redirect_uri: pending.redirectUri,
130
+ client_id: this.clientId,
131
+ code_verifier: pending.verifier,
132
+ }).toString(),
133
+ });
134
+ }
135
+ catch (cause) {
136
+ clearPending(this.storage);
137
+ throw new LogiAuthError("network_error", "Network error during token exchange (offline, DNS, CORS, or TLS failure).", cause);
138
+ }
139
+ clearPending(this.storage);
140
+ if (!tokenResp.ok) {
141
+ // Truncate body to 2 KB — IdPs occasionally echo request params on
142
+ // 4xx, and consumers shouldn't blindly log multi-MB payloads to Sentry.
143
+ const rawBody = await tokenResp.text();
144
+ const body = rawBody.length > 2048 ? rawBody.slice(0, 2048) + "…[truncated]" : rawBody;
145
+ throw new LogiAuthError("token_exchange_failed", `Token exchange failed: HTTP ${tokenResp.status}`, { status: tokenResp.status, body });
146
+ }
147
+ const tokens = await tokenResp.json();
148
+ return {
149
+ accessToken: tokens.access_token,
150
+ idToken: tokens.id_token,
151
+ refreshToken: tokens.refresh_token,
152
+ tokenType: tokens.token_type ?? "Bearer",
153
+ expiresAt: tokens.expires_in
154
+ ? Date.now() + Number(tokens.expires_in) * 1000
155
+ : undefined,
156
+ scope: tokens.scope,
157
+ returnTo: pending.returnTo,
158
+ };
159
+ }
160
+ /**
161
+ * Exchange a refresh_token for a fresh access_token. Public clients should
162
+ * persist the rotated refresh_token returned in `refreshToken`.
163
+ */
164
+ async refresh(refreshToken) {
165
+ let resp;
166
+ try {
167
+ resp = await fetch(`${this.issuer}/oauth/token`, {
168
+ method: "POST",
169
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
170
+ body: new URLSearchParams({
171
+ grant_type: "refresh_token",
172
+ refresh_token: refreshToken,
173
+ client_id: this.clientId,
174
+ }).toString(),
175
+ });
176
+ }
177
+ catch (cause) {
178
+ throw new LogiAuthError("network_error", "Network error during refresh (offline, DNS, CORS, or TLS failure).", cause);
179
+ }
180
+ if (!resp.ok) {
181
+ const rawBody = await resp.text();
182
+ const body = rawBody.length > 2048 ? rawBody.slice(0, 2048) + "…[truncated]" : rawBody;
183
+ throw new LogiAuthError("token_exchange_failed", `Refresh failed: HTTP ${resp.status}`, { status: resp.status, body });
184
+ }
185
+ const tokens = await resp.json();
186
+ return {
187
+ accessToken: tokens.access_token,
188
+ idToken: tokens.id_token,
189
+ refreshToken: tokens.refresh_token,
190
+ tokenType: tokens.token_type ?? "Bearer",
191
+ expiresAt: tokens.expires_in
192
+ ? Date.now() + Number(tokens.expires_in) * 1000
193
+ : undefined,
194
+ scope: tokens.scope,
195
+ };
196
+ }
197
+ /**
198
+ * Decode an ID token's payload (no signature verification). Use only for
199
+ * UI hints (e.g. show user's email). Real authorization decisions must be
200
+ * made server-side after re-verifying via JWKS.
201
+ */
202
+ parseIdToken(idToken) {
203
+ const [, payload] = idToken.split(".");
204
+ if (!payload)
205
+ throw new Error("Invalid id_token: missing payload");
206
+ const b64 = payload.replace(/-/g, "+").replace(/_/g, "/");
207
+ const padded = b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), "=");
208
+ // Decode as UTF-8, not Latin-1 — Korean / Japanese / emoji claim values
209
+ // round-trip correctly. atob() returns a binary string interpreted as
210
+ // UTF-16 by JSON.parse, which mojibakes any non-ASCII (codex P2 fix).
211
+ const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
212
+ const json = new TextDecoder("utf-8").decode(bytes);
213
+ return JSON.parse(json);
214
+ }
215
+ /**
216
+ * True when sessionStorage has a pending sign-in (i.e. user just returned
217
+ * from the IdP). Use to gate calling handleCallback() in shared components.
218
+ */
219
+ hasPendingCallback() {
220
+ return loadPending(this.storage) !== null;
221
+ }
222
+ }
223
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/pkce.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function generateCodeVerifier(byteLength?: number): string;
2
+ export declare function deriveCodeChallenge(verifier: string): Promise<string>;
3
+ export declare function generateState(): string;
4
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAWA,wBAAgB,oBAAoB,CAAC,UAAU,SAAK,GAAG,MAAM,CAI5D;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI3E;AAED,wBAAgB,aAAa,IAAI,MAAM,CAKtC"}
package/dist/pkce.js ADDED
@@ -0,0 +1,27 @@
1
+ // PKCE helpers (RFC 7636) — browser-only, uses crypto.subtle.
2
+ //
3
+ // `code_verifier`: 43–128 char URL-safe random string.
4
+ // `code_challenge`: BASE64URL(SHA256(verifier)), 43 chars unpadded.
5
+ function base64url(bytes) {
6
+ let bin = "";
7
+ for (const b of bytes)
8
+ bin += String.fromCharCode(b);
9
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
10
+ }
11
+ export function generateCodeVerifier(byteLength = 48) {
12
+ const bytes = new Uint8Array(byteLength);
13
+ crypto.getRandomValues(bytes);
14
+ return base64url(bytes);
15
+ }
16
+ export async function deriveCodeChallenge(verifier) {
17
+ const data = new TextEncoder().encode(verifier);
18
+ const digest = await crypto.subtle.digest("SHA-256", data);
19
+ return base64url(new Uint8Array(digest));
20
+ }
21
+ export function generateState() {
22
+ // Random opaque value to defeat CSRF on the callback. 16 bytes → 22 chars.
23
+ const bytes = new Uint8Array(16);
24
+ crypto.getRandomValues(bytes);
25
+ return base64url(bytes);
26
+ }
27
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,uDAAuD;AACvD,oEAAoE;AAEpE,SAAS,SAAS,CAAC,KAAiB;IAClC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,UAAU,GAAG,EAAE;IAClD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACxD,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3D,OAAO,SAAS,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,2EAA2E;IAC3E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,19 @@
1
+ export interface PendingHandoff {
2
+ state: string;
3
+ verifier: string;
4
+ redirectUri: string;
5
+ /** Optional caller-supplied passthrough (e.g. UI route to restore). */
6
+ returnTo?: string;
7
+ /** ms epoch — used to expire stale handoffs. */
8
+ startedAt: number;
9
+ }
10
+ export interface StorageBackend {
11
+ get(key: string): string | null;
12
+ set(key: string, value: string): void;
13
+ remove(key: string): void;
14
+ }
15
+ export declare const sessionStorageBackend: StorageBackend;
16
+ export declare function savePending(p: PendingHandoff, backend: StorageBackend): void;
17
+ export declare function loadPending(backend: StorageBackend): PendingHandoff | null;
18
+ export declare function clearPending(backend: StorageBackend): void;
19
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,eAAO,MAAM,qBAAqB,EAAE,cAuBnC,CAAC;AAEF,wBAAgB,WAAW,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAE5E;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,GAAG,IAAI,CAQ1E;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAE1D"}
@@ -0,0 +1,48 @@
1
+ // Per-handoff storage for the PKCE verifier + CSRF state. Kept in
2
+ // sessionStorage so it survives the IdP redirect round-trip but is wiped on
3
+ // tab close.
4
+ const KEY = "logi-auth.pending";
5
+ export const sessionStorageBackend = {
6
+ get(key) {
7
+ try {
8
+ return sessionStorage.getItem(key);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ },
14
+ set(key, value) {
15
+ // Re-throw on quota / disabled storage so signIn() can refuse to
16
+ // navigate to the IdP. Silent failure (codex P2 2026-05-15) lets the
17
+ // user complete the IdP round-trip and only fail at handleCallback()
18
+ // with a misleading no_pending_handoff. Real-world hits: Safari ITP,
19
+ // iOS private browsing, corporate policies disabling sessionStorage.
20
+ sessionStorage.setItem(key, value);
21
+ },
22
+ remove(key) {
23
+ try {
24
+ sessionStorage.removeItem(key);
25
+ }
26
+ catch {
27
+ // ignore
28
+ }
29
+ },
30
+ };
31
+ export function savePending(p, backend) {
32
+ backend.set(KEY, JSON.stringify(p));
33
+ }
34
+ export function loadPending(backend) {
35
+ const raw = backend.get(KEY);
36
+ if (!raw)
37
+ return null;
38
+ try {
39
+ return JSON.parse(raw);
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export function clearPending(backend) {
46
+ backend.remove(KEY);
47
+ }
48
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,4EAA4E;AAC5E,aAAa;AAEb,MAAM,GAAG,GAAG,mBAAmB,CAAC;AAkBhC,MAAM,CAAC,MAAM,qBAAqB,GAAmB;IACnD,GAAG,CAAC,GAAG;QACL,IAAI,CAAC;YACH,OAAO,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,GAAG,CAAC,GAAG,EAAE,KAAK;QACZ,iEAAiE;QACjE,qEAAqE;QACrE,qEAAqE;QACrE,qEAAqE;QACrE,qEAAqE;QACrE,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,CAAC,GAAG;QACR,IAAI,CAAC;YACH,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,CAAiB,EAAE,OAAuB;IACpE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAuB;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAuB;IAClD,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AACtB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@logi-auth/browser",
3
+ "version": "0.1.0",
4
+ "description": "Browser SDK for logi (1pass) — OAuth 2.0 + OIDC PKCE for SPAs. Zero dependencies.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "prepublishOnly": "npm run build && npm run test"
24
+ },
25
+ "keywords": [
26
+ "logi",
27
+ "1pass",
28
+ "oauth",
29
+ "oidc",
30
+ "pkce",
31
+ "spa",
32
+ "auth",
33
+ "openid"
34
+ ],
35
+ "author": "Seunghan Kim (https://github.com/seunghan91)",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/seunghan91/logi.git",
40
+ "directory": "Packages/npm/browser"
41
+ },
42
+ "homepage": "https://docs.1pass.dev",
43
+ "bugs": {
44
+ "url": "https://github.com/seunghan91/logi/issues"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "devDependencies": {
53
+ "typescript": "^5.6.0",
54
+ "vitest": "^2.1.0"
55
+ },
56
+ "sideEffects": false
57
+ }