{"_id":"@altium-developer/a365-auth","_rev":"5-91c2caf66d304397c6c26c7ee2b8ab52","name":"@altium-developer/a365-auth","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@altium-developer/a365-auth","version":"0.1.0","keywords":["altium","altium365","a365","oauth2","oidc","pkce","authentication","sso","token-exchange"],"author":{"url":"https://www.altium.com","name":"Altium Limited"},"license":"MIT","_id":"@altium-developer/a365-auth@0.1.0","maintainers":[{"name":"kolomiets-altium","email":"dmitry.kolomiets@altium.com"}],"homepage":"https://github.com/AltiumDeveloper/a365-auth/tree/main/libs/typescript#readme","bugs":{"url":"https://github.com/AltiumDeveloper/a365-auth/issues"},"dist":{"shasum":"05c2ee5ac4c8d4907a9d616b239ff61697ea4d02","tarball":"https://registry.npmjs.org/@altium-developer/a365-auth/-/a365-auth-0.1.0.tgz","fileCount":12,"integrity":"sha512-RVFubBRfBe3w4GwCj0IeanmGLDVB7/1zjPKAkNcq1xG2Q80KcPe8eQnb0jqm3eQscoxDz6ZxEXy7ScRVoA4dHA==","signatures":[{"sig":"MEQCIBmGFBbjnMX6bsC7WQilMxocmlilI0Ec3g8aPLKJ4vOdAiANgbF9pF6oe3IJPpkWAum31iB24q81gKq9v2h41u5MyA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":57993},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"92518da7235d99715f31203ce9bbaf7d231272a3","scripts":{"lint":"eslint src","test":"vitest run","build":"tsc -p tsconfig.build.json","clean":"rm -rf dist","test:e2e":"tsx scripts/test-signin.ts","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build","test:conformance":"vitest run --config test/conformance/vitest.config.ts"},"_npmUser":{"name":"kolomiets-altium","email":"dmitry.kolomiets@altium.com"},"repository":{"url":"git+https://github.com/AltiumDeveloper/a365-auth.git","type":"git","directory":"libs/typescript"},"_npmVersion":"10.9.2","description":"Altium 365 OAuth2 authentication library with PKCE and ActionWait long-polling for browser-based sign-in.","directories":{},"sideEffects":false,"_nodeVersion":"22.16.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.4","eslint":"^9.0.0","vitest":"^2.0.0","@eslint/js":"^9.0.0","typescript":"^5.4.0","@types/node":"^20.0.0","typescript-eslint":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/a365-auth_0.1.0_1784888567306_0.9203730626996189","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Renamed to @altium-developer/altium-auth"},"0.1.1":{"name":"@altium-developer/a365-auth","version":"0.1.1","keywords":["altium","altium365","a365","oauth2","oidc","pkce","authentication","sso","token-exchange"],"author":{"url":"https://www.altium.com","name":"Altium Limited"},"license":"MIT","_id":"@altium-developer/a365-auth@0.1.1","maintainers":[{"name":"kolomiets-altium","email":"dmitry.kolomiets@altium.com"}],"homepage":"https://github.com/AltiumDeveloper/a365-auth/tree/main/libs/typescript#readme","bugs":{"url":"https://github.com/AltiumDeveloper/a365-auth/issues"},"dist":{"shasum":"5840de12d1bfd25e2978c8742dd29b09f35189d0","tarball":"https://registry.npmjs.org/@altium-developer/a365-auth/-/a365-auth-0.1.1.tgz","fileCount":12,"integrity":"sha512-euwj764JVOSTrKfQShSjlLPHwzS4HJ2jRfkM0wxGusB9xufrXGEcgEDig75Klbqqip0F3/DGa7tFNpZ3eyMzJA==","signatures":[{"sig":"MEYCIQDj16dl3JznsqslxpqFkwk7lrU0jNXbtytYrj3SSTqOWwIhAIxTJahDeVSeBLvXtlorAwGstKa85s89VcdNg+87Nx2s","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@altium-developer%2fa365-auth@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":57993},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"c5b266b1bd67cfede161bdd3182266e8c2c556b5","scripts":{"lint":"eslint src","test":"vitest run","build":"tsc -p tsconfig.build.json","clean":"rm -rf dist","test:e2e":"tsx scripts/test-signin.ts","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build","test:conformance":"vitest run --config test/conformance/vitest.config.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:c83ffcdd-8e8d-4d4e-81b3-3c6e833086ea"}},"repository":{"url":"git+https://github.com/AltiumDeveloper/a365-auth.git","type":"git","directory":"libs/typescript"},"_npmVersion":"12.0.1","description":"Altium 365 OAuth2 authentication library with PKCE and ActionWait long-polling for browser-based sign-in.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.4","eslint":"^9.0.0","vitest":"^2.0.0","@eslint/js":"^9.0.0","typescript":"^5.4.0","@types/node":"^20.0.0","typescript-eslint":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/a365-auth_0.1.1_1784890210695_0.565671355608317","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Renamed to @altium-developer/altium-auth"},"0.1.2":{"name":"@altium-developer/a365-auth","version":"0.1.2","keywords":["altium","altium365","a365","oauth2","oidc","pkce","authentication","sso","token-exchange"],"author":{"url":"https://www.altium.com","name":"Altium Limited"},"license":"MIT","_id":"@altium-developer/a365-auth@0.1.2","maintainers":[{"name":"kolomiets-altium","email":"dmitry.kolomiets@altium.com"}],"homepage":"https://github.com/AltiumDeveloper/a365-auth/tree/main/libs/typescript#readme","bugs":{"url":"https://github.com/AltiumDeveloper/a365-auth/issues"},"dist":{"shasum":"fbc1da234e0503d7f33e693ba8493a3733ce63b1","tarball":"https://registry.npmjs.org/@altium-developer/a365-auth/-/a365-auth-0.1.2.tgz","fileCount":12,"integrity":"sha512-PeYkGwLPWX7lYCV+LFg/gGOCAovQW0ClDAliWAgLhylFocUdoIz8hLtTJwZNdOceQN1iUntzB7CmXxwdRBaorA==","signatures":[{"sig":"MEUCIGv61HTIcScG3unj5P9r6QTsdubLya+D9ubqHqi8BZZ4AiEAlGeNx+biuXhLtkB2lvr8qgkJbIITcp11GK4si2SGBFY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@altium-developer%2fa365-auth@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":59648},"main":"./dist/index.js","type":"commonjs","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"c0d79c3414819e2505769e72bb2444ac138f509f","scripts":{"lint":"eslint src","test":"vitest run","build":"tsc -p tsconfig.build.json","clean":"rm -rf dist","test:e2e":"tsx scripts/test-signin.ts","typecheck":"tsc --noEmit","prepublishOnly":"npm run clean && npm run lint && npm run test && npm run build","test:conformance":"vitest run --config test/conformance/vitest.config.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:c83ffcdd-8e8d-4d4e-81b3-3c6e833086ea"}},"repository":{"url":"git+https://github.com/AltiumDeveloper/a365-auth.git","type":"git","directory":"libs/typescript"},"_npmVersion":"12.0.1","description":"Altium 365 OAuth2 authentication library with PKCE and ActionWait long-polling for browser-based sign-in.","directories":{},"sideEffects":false,"_nodeVersion":"22.23.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.4","eslint":"^9.0.0","vitest":"^2.0.0","@eslint/js":"^9.0.0","typescript":"^5.4.0","@types/node":"^20.0.0","typescript-eslint":"^8.0.0"},"_npmOperationalInternal":{"tmp":"tmp/a365-auth_0.1.2_1784904485647_0.758401387272424","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Renamed to @altium-developer/altium-auth"}},"time":{"created":"2026-07-24T10:22:47.141Z","modified":"2026-09-04T18:38:30.963Z","0.1.0":"2026-07-24T10:22:47.438Z","0.1.1":"2026-07-24T10:50:10.840Z","0.1.2":"2026-07-24T14:48:05.832Z"},"bugs":{"url":"https://github.com/AltiumDeveloper/a365-auth/issues"},"author":{"url":"https://www.altium.com","name":"Altium Limited"},"license":"MIT","homepage":"https://github.com/AltiumDeveloper/a365-auth/tree/main/libs/typescript#readme","keywords":["altium","altium365","a365","oauth2","oidc","pkce","authentication","sso","token-exchange"],"repository":{"url":"git+https://github.com/AltiumDeveloper/a365-auth.git","type":"git","directory":"libs/typescript"},"description":"Altium 365 OAuth2 authentication library with PKCE and ActionWait long-polling for browser-based sign-in.","maintainers":[{"name":"kolomiets-altium","email":"dmitry.kolomiets@altium.com"},{"name":"vmal-altium","email":"vincent.malmedy@altium.com"}],"readme":"# @altium-developer/a365-auth\n\n[![CI](https://github.com/AltiumDeveloper/a365-auth/actions/workflows/typescript-ci.yml/badge.svg)](https://github.com/AltiumDeveloper/a365-auth/actions/workflows/typescript-ci.yml)\n[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/AltiumDeveloper/a365-auth/blob/main/libs/typescript/LICENSE)\n\nAltium 365 OAuth2 / OpenID Connect authentication library. Supports both client types and both clouds:\n\n- **Public clients** (desktop, on-prem, native) — browser sign-in with PKCE over Altium's **ActionWait** long-poll: [`signIn`](#signinconfig-options).\n- **Confidential clients** (web/server backends with a secret) — the standard **authorization-code redirect** flow via composable steps: [`createAuthorizationUrl`](#createauthorizationurlconfig-options) + [`exchangeCode`](#exchangecodeconfig-params).\n- **Workspace tokens**, **token refresh**, and first-class **Gov Cloud** support.\n\n**Zero runtime dependencies.** Runs on Node ≥20, Bun, and Deno (and bundled apps that polyfill Node's `crypto`).\n\n## Documentation\n\nThe library implements the flow described in these guides (protocol-level, independent of this package) — start here if you're new to Altium Identity:\n\n- [Authentication overview](../../docs/guides/overview.md) — endpoints, key terms, and the recommended flow\n- [Register your application](../../docs/guides/register-your-application.md) — client types, redirect URLs, credentials\n- [Web / server apps](../../docs/guides/web-and-server-apps.md) — authorization-code redirect flow (confidential clients)\n- [Desktop / on-prem apps](../../docs/guides/desktop-and-onprem-apps.md) — the ActionWait pattern (public clients)\n- [Gov Cloud](../../docs/guides/gov-cloud.md) — Commercial vs Gov and the `secure=1` two-token model\n- [Access token claims](../../docs/guides/token-claims.md) — what's inside a token (`iss`, `workspaceId`, `secure`, scopes)\n\n## Quick Start\n\nOnly `clientId` and `scopes` are required — the endpoints default to the Altium 365 Commercial Cloud. Pick the flow that matches your app.\n\n### Public apps (desktop / on-prem — ActionWait sign-in)\n\nFor desktop, on-prem, and native clients that **can't host a public redirect**. `signIn` opens the browser, waits for the callback over Altium's ActionWait long-poll, and returns tokens. See [Desktop / on-prem apps](../../docs/guides/desktop-and-onprem-apps.md).\n\n```typescript\nimport { signIn, signIntoWorkspace } from \"@altium-developer/a365-auth\";\n\nconst config = {\n  clientId: \"your-client-id\",\n  scopes: \"openid profile\",\n};\n\n// Opens a browser login page and waits for the callback\nconst tokens = await signIn(config);\n\n// Persist `tokens` yourself — e.g. VS Code SecretStorage, a keychain, or a file.\n\n// Exchange the global token for a workspace-scoped token\nconst workspaceToken = await signIntoWorkspace(config, tokens.access_token, \"workspace-id-here\");\n```\n\nThe library returns tokens and never stores them — persistence, refresh, and\nsign-out are entirely yours to manage.\n\nIf your host runtime needs to open URLs through a host API, keep using `signIn`\nand provide a browser opener. The library still owns PKCE, ActionWait polling,\nstate correlation, CSRF validation, and token exchange:\n\n```typescript\nconst tokens = await signIn(config, {\n  openBrowser: async (url) => {\n    // Example: VS Code extension host, Electron shell, native app bridge, etc.\n    await openExternalUrl(url);\n  },\n});\n```\n\n### Confidential apps (web / server — authorization-code redirect)\n\nFor web/server backends that **host their own redirect endpoint**. Set `clientSecret` on the config to authenticate as a confidential client (HTTP Basic), and drive the flow with two composable steps. See [Web / server apps](../../docs/guides/web-and-server-apps.md).\n\n```typescript\nimport { createAuthorizationUrl, exchangeCode } from \"@altium-developer/a365-auth\";\n\nconst config = {\n  clientId: \"your-client-id\",\n  clientSecret: \"your-client-secret\",   // confidential client → HTTP Basic auth\n  scopes: \"openid profile offline_access\",\n};\nconst redirectUri = \"https://my-service.example.com/oauth/callback\";\n\n// On your login route: build the URL, stash state + verifier, then redirect.\napp.get(\"/login\", (req, res) => {\n  const { url, state, codeVerifier } = createAuthorizationUrl(config, { redirectUri });\n  req.session.oauth = { state, codeVerifier };\n  res.redirect(url);\n});\n\n// On your callback route: verify state, then exchange the code for tokens.\napp.get(\"/oauth/callback\", async (req, res) => {\n  const { state, codeVerifier } = req.session.oauth;\n  if (req.query.state !== state) throw new Error(\"state mismatch\");\n\n  const tokens = await exchangeCode(config, {\n    code: String(req.query.code),\n    codeVerifier,\n    redirectUri,\n  });\n  // Persist `tokens`; then discover/exchange workspaces and refresh as usual.\n});\n```\n\n`createAuthorizationUrl` is synchronous (it generates PKCE + `state` and returns the URL to redirect to); `exchangeCode` performs the token exchange. Both `signIntoWorkspace` and `refreshToken` also send the `clientSecret` automatically when it's set.\n\n### Gov Cloud\n\nAltium Gov Cloud is an isolated environment for ITAR/regulated workspaces. Use the exported `GOV_CLOUD_ENDPOINTS` — that's it. The library detects the Gov token endpoint and adds the required `secure=1` to token requests automatically (the two-token model); no `secure` flag to set.\n\n```typescript\nimport { signIn, GOV_CLOUD_ENDPOINTS } from \"@altium-developer/a365-auth\";\n\nconst tokens = await signIn({\n  clientId: \"your-gov-client-id\",\n  scopes: \"openid profile\",\n  ...GOV_CLOUD_ENDPOINTS,\n});\n```\n\nCommercial and Gov are kept strictly separate: a global token can only be exchanged for a workspace of the matching kind. `secure=1` is driven by which token endpoint you use — Gov endpoint → sent, Commercial endpoint → omitted — so pointing `tokenEndpoint` at the Gov host is all it takes to exchange a Commercial token for a Gov workspace token.\n\n> Gov tokens must never be used against Commercial services, and vice versa. See [docs/gov-cloud.md](../../docs/guides/gov-cloud.md).\n\n### Custom or on-prem installations\n\nFor on-prem or other custom installations, override any of the four\nendpoints. Anything you omit still falls back to the Commercial Cloud:\n\n```typescript\nimport { COMMERCIAL_CLOUD_ENDPOINTS } from \"@altium-developer/a365-auth\";\n\nconst config = {\n  clientId: \"your-client-id\",\n  scopes: \"openid profile\",\n  // Point auth + token at an on-prem host; keep the rest on the Commercial Cloud:\n  authEndpoint: \"https://auth.my-onprem.example/connect/authorize\",\n  tokenEndpoint: \"https://auth.my-onprem.example/connect/token\",\n};\n\n// Endpoint presets are exported to inspect or spread:\n// COMMERCIAL_CLOUD_ENDPOINTS and GOV_CLOUD_ENDPOINTS.\nconsole.log(COMMERCIAL_CLOUD_ENDPOINTS.tokenEndpoint);\n```\n\n## API Reference\n\n### `signIn(config, options?)`\n\nPerforms OAuth2 PKCE sign-in. Opens the browser to the authorization URL, long-polls for the code callback via ActionWait, exchanges it for tokens, and returns the result.\n\n**Does NOT** persist tokens or manage auth state — those are your responsibility. You receive a `TokenSet` back and store it yourself.\n\n**Parameters:**\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| config | `OAuthConfig` | OAuth2 configuration (`clientId` + `scopes` required; endpoints default to the Commercial Cloud) |\n| options | `SignInOptions` | Optional: `timeoutMs` (default 180s), `AbortSignal` for cancellation, `selectWorkspace` for login-into-workspace mode, and `openBrowser` for host-specific URL opening |\n\n**Returns:** `Promise<TokenSet>` — the full token response from the IdP.\n\n### `createAuthorizationUrl(config, options?)`\n\nBuilds an OAuth2 authorization URL with PKCE for the redirect-based (authorization-code) flow. Synchronous — no network I/O.\n\n**Parameters:**\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| config | `OAuthConfig` | Same as signIn |\n| options | `AuthorizationUrlOptions` | Optional: `redirectUri`, `state`, `codeVerifier`, `scopes`, `selectWorkspace` overrides |\n\n**Returns:** `AuthorizationRequest` — `{ url, state, codeVerifier }`. Redirect the user to `url`; persist `state` and `codeVerifier` (e.g. in the session) for the callback.\n\n### `exchangeCode(config, params)`\n\nExchanges an authorization `code` (from your redirect callback) for tokens via the `authorization_code` grant. Sends the `clientSecret` (HTTP Basic) for confidential clients, or `client_id` + PKCE `codeVerifier` for public clients.\n\n**Parameters:**\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| config | `OAuthConfig` | Same as signIn |\n| params | `ExchangeCodeParams` | `{ code, codeVerifier?, redirectUri? }` |\n\n**Returns:** `Promise<TokenSet>`.\n\n**Throws:** `Error` if `code` is empty.\n\n### `signIntoWorkspace(config, baseAccessToken, workspaceAuthId)`\n\nExchanges a base access token (the `access_token` from `signIn`) for a workspace-scoped token using OAuth2's `urn:ietf:params:oauth:grant-type:token-exchange` grant.\n\n**Does NOT** cache tokens internally — call it each time you need a fresh token.\n\n**Parameters:**\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| config | `OAuthConfig` | Same as signIn |\n| baseAccessToken | `string` | The `access_token` returned by `signIn` |\n| workspaceAuthId | `string` | The workspace's auth ID from Altium 365 |\n\n**Returns:** `Promise<TokenSet>` — the workspace-scoped token.\n\n**Throws:** `Error` if `baseAccessToken` is empty.\n\n### `refreshToken(config, refreshToken)`\n\nExchanges a refresh token for a fresh `TokenSet` using the OAuth2 `refresh_token` grant. Use it when an access token has expired (compare `TokenSet.expires_at` against the current epoch seconds) to avoid a full interactive sign-in.\n\nWorks the same for **global and workspace** refresh tokens — no scope is sent, so the refreshed token keeps whatever grant the refresh token was issued for (a global token stays global; a workspace token stays workspace-scoped).\n\n**Parameters:**\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| config | `OAuthConfig` | Same as signIn |\n| refreshToken | `string` | A `refresh_token` from a prior `signIn` or `signIntoWorkspace` TokenSet |\n\n**Returns:** `Promise<TokenSet>` — the refreshed tokens. If the IdP rotates refresh tokens, the response includes a new `refresh_token`; persist it and discard the old one.\n\n**Throws:** `Error` if `refreshToken` is empty, or if the grant is rejected (e.g. the refresh token expired or was revoked).\n\n> **Tip:** request the `offline_access` scope at sign-in to receive a `refresh_token`.\n\n```typescript\nimport { refreshToken } from \"@altium-developer/a365-auth\";\n\nconst nowSeconds = Math.floor(Date.now() / 1000);\nif (tokens.expires_at && tokens.expires_at <= nowSeconds && tokens.refresh_token) {\n  tokens = await refreshToken(config, tokens.refresh_token);\n  // Persist `tokens` again — including a rotated refresh_token if present.\n}\n```\n\n### `revokeRefreshToken(config, refreshToken)`\n\nRevokes a refresh token via the OAuth2 Token Revocation endpoint (RFC 7009) — call it on sign-out to invalidate the grant server-side. The revocation endpoint is derived from the token endpoint (`/connect/token` → `/connect/revocation`); only **refresh** tokens are revoked. Client authentication follows the client type (confidential → HTTP Basic; public → `client_id` in body).\n\n**Parameters:**\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| config | `OAuthConfig` | Same as signIn |\n| refreshToken | `string` | The `refresh_token` to revoke |\n\n**Returns:** `Promise<void>`. Per RFC 7009 the call is idempotent (the endpoint returns `200` for both known and unknown tokens).\n\n**Throws:** `Error` if `refreshToken` is empty, or the endpoint returns a non-2xx status. After a successful revoke, a subsequent `refreshToken` with the same token fails with `invalid_grant`.\n\n```typescript\nimport { revokeRefreshToken } from \"@altium-developer/a365-auth\";\n\n// On sign-out:\nif (tokens.refresh_token) {\n  await revokeRefreshToken(config, tokens.refresh_token);\n}\n// ...then discard your local copy of the tokens.\n```\n\n## Types\n\n### `OAuthConfig`\n\n```typescript\ninterface OAuthConfig {\n  clientId: string;           // OAuth2 client ID registered with Altium\n  scopes: string;             // Space-delimited, must include \"openid profile\"\n\n  clientSecret?: string;      // Confidential clients only → HTTP Basic auth\n  secure?: boolean;           // Override Gov auto-detection (normally unset)\n\n  // Optional — default to COMMERCIAL_CLOUD_ENDPOINTS. Override for Dev/UAT/on-prem,\n  // or spread GOV_CLOUD_ENDPOINTS for Gov Cloud.\n  authEndpoint?: string;      // default: https://auth.altium.com/connect/authorize\n  tokenEndpoint?: string;     // default: https://auth.altium.com/connect/token\n  actionWaitEndpoint?: string; // default: https://actionwait.altium.com/await\n  redirectUri?: string;       // default: https://auth.altium.com/api/AuthComplete\n}\n```\n\n### `TokenSet`\n\n```typescript\ninterface TokenSet {\n  access_token: string;\n  refresh_token?: string;\n  id_token?: string;\n  token_type?: string;\n  expires_in?: number;\n  expires_at?: number;       // Epoch seconds (computed by library)\n  scope?: string;\n}\n```\n\n> `access_token` is a signed JWT — decode it to read `iss`, `workspaceId`, `secure`, and scopes. See [Access token claims](../../docs/guides/token-claims.md).\n\n### `AuthorizationUrlOptions` / `SignInOptions`\n\nBoth `createAuthorizationUrl` and `signIn` accept a `selectWorkspace` option for [login-into-workspace mode](../../docs/guides/overview.md#login-into-workspace-mode):\n\n```typescript\n// In AuthorizationUrlOptions (createAuthorizationUrl) and SignInOptions (signIn):\nselectWorkspace?: \"none\" | \"strict\" | \"optional\";\n// \"strict\"   — workspace selection mandatory; returned token is workspace-scoped.\n// \"optional\" — workspace selection offered; user may skip.\n// \"none\" or omitted (default) — no workspace prompt; issues a global access token.\n\n// SignInOptions only:\nopenBrowser?: (url: string) => void | Promise<void>;\n```\n\nUse `openBrowser` when embedding the library into a host with its own browser API\nsuch as VS Code, Electron, or a native app bridge. ActionWait polling remains\nowned by the library.\n\n## Error Handling\n\nThe library throws descriptive `Error` objects in these scenarios:\n\n| Scenario | Error message pattern |\n|----------|----------------------|\n| Missing `clientId`/`scopes` | `OAuthConfig.{field} is required and must be non-empty.` |\n| Invalid endpoint URL | `OAuthConfig.{field} is not a valid URL:` |\n| ActionWait timeout | `ActionWait poll timed out after {n}ms.` |\n| Sign-in cancelled | `Sign-in cancelled.` |\n| CSRF detected | `State mismatch during sign-in (possible CSRF attack).` |\n| Empty base token for workspace exchange | `baseAccessToken is required — pass the access_token from signIn().` |\n| Empty refresh token | `refreshToken is required — pass the refresh_token from a prior TokenSet.` |\n| Empty authorization code | `code is required — pass the authorization code from the redirect callback.` |\n| Token endpoint returns OAuth error | `Token endpoint {status} {error_code} — {description}` |\n\n## Compatibility\n\n- **Node.js ≥20** — uses the global `fetch`, the global Web `crypto` (`randomUUID`), and the `crypto` module (`randomBytes`, `createHash`). Node 18 is EOL and not supported.\n- **Bun / Deno** — supported (Node-compatible `fetch` + `crypto`).\n- **Browsers / bundlers** — works where your bundler polyfills Node's `crypto` and `Buffer`.\n\n## Development\n\n```bash\nnpm install     # install dev dependencies\nnpm test        # run unit tests\nnpm run lint    # lint src/\nnpm run build   # compile to dist/\n```\n\n### E2E sign-in test\n\nRun the sign-in flow against a live Altium environment. It opens your browser, waits for the ActionWait callback, and prints the resulting tokens (and optionally exercises workspace exchange and refresh).\n\n```bash\n# Commercial Cloud, public client\nnpm run test:e2e -- YOUR_CLIENT_ID\n\n# Gov Cloud (Dev) — verifies the secure=1 two-token model\nnpm run test:e2e -- --env dev-gov YOUR_GOV_CLIENT_ID\n\n# Also exchange a workspace token and exercise refresh\nnpm run test:e2e -- --workspace <authId> --refresh YOUR_CLIENT_ID\n\n# Confidential client (HTTP Basic) — pass the secret via env, never on the CLI\nA365_CLIENT_SECRET=... npm run test:e2e -- YOUR_CLIENT_ID\n```\n\nOptions include `--env prod|dev|gov|dev-gov`, `--workspace-env`, `--secure`/`--no-secure`, `--scopes`, `--workspace`, `--refresh`, `--userinfo`, `--revoke`, plus `--authorize-url`/`--exchange-code`/`--redirect-uri` for confidential (custom-callback) clients — see the header of [`scripts/test-signin.ts`](https://github.com/AltiumDeveloper/a365-auth/blob/main/libs/typescript/scripts/test-signin.ts). The client secret is read from `A365_CLIENT_SECRET` so it never appears in shell history or the process list.\n\nSee [CONTRIBUTING.md](https://github.com/AltiumDeveloper/a365-auth/blob/main/CONTRIBUTING.md) and [AGENTS.md](https://github.com/AltiumDeveloper/a365-auth/blob/main/AGENTS.md) for the full contributor guide.\n\n## Security\n\nPlease report vulnerabilities privately — see [SECURITY.md](https://github.com/AltiumDeveloper/a365-auth/blob/main/SECURITY.md).\n\n## License\n\n[MIT](https://github.com/AltiumDeveloper/a365-auth/blob/main/libs/typescript/LICENSE) © Altium Limited\n","readmeFilename":"README.md"}