{"_id":"@dooraccess/opendoor-web-sdk","_rev":"4-a802af9b46947c09e2f404f67bf1eac1","name":"@dooraccess/opendoor-web-sdk","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@dooraccess/opendoor-web-sdk","version":"1.0.0","keywords":["door","access","sdk","door-code","latch"],"license":"MIT","_id":"@dooraccess/opendoor-web-sdk@1.0.0","maintainers":[{"name":"ryan.salmons.door","email":"ryan.salmons@latch.com"}],"dist":{"shasum":"66877eb9f429f12511e640ab169412c1bab03234","tarball":"https://registry.npmjs.org/@dooraccess/opendoor-web-sdk/-/opendoor-web-sdk-1.0.0.tgz","fileCount":11,"integrity":"sha512-eh0xuxbCvemo5482SVk2ctVPKbi3DsjWJOONRXLM08QO56luXkFMT7PodCPHZ8IU6KERan2bYPGLsiJNJw6Ajw==","signatures":[{"sig":"MEQCIEZEhyPAT70zVSoZ3ps7FaoMsAgWDaOMERDpfWgCUH+pAiAMp/1iJoDVNVwQX9T3FD7Fgsv26OBmKxNN4+z0JFKU3w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":164692},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","unpkg":"./dist/index.umd.js","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"cb7612f9e7427b89d3529129ab1c18079c8ecb6b","scripts":{"dev":"tsup --watch","demo":"npm --prefix demo run start","lint":"eslint src/ tests/","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","prepare":"husky","lint:fix":"eslint src/ tests/ --fix","test:e2e":"playwright test","demo:live":"LATCH_CLIENT_ID=$LATCH_CLIENT_ID LATCH_CLIENT_SECRET=$LATCH_CLIENT_SECRET npm --prefix demo run start","typecheck":"tsc --noEmit","test:watch":"vitest","demo:install":"npm --prefix demo install","format:check":"prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"ryan.salmons.door","email":"ryan.salmons@latch.com"},"jsdelivr":"./dist/index.umd.js","_npmVersion":"10.8.2","description":"JavaScript/TypeScript SDK for embedding door code access into web applications","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"]},"sideEffects":false,"_nodeVersion":"20.20.1","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","husky":"^9.0.0","eslint":"^9.0.0","vitest":"^3.0.0","prettier":"^3.0.0","@eslint/js":"^9.0.0","typescript":"^5.7.0","lint-staged":"^15.0.0","@playwright/test":"^1.50.0","typescript-eslint":"^8.0.0","@vitest/coverage-v8":"^3.0.0","eslint-config-prettier":"^10.0.0"},"_npmOperationalInternal":{"tmp":"tmp/opendoor-web-sdk_1.0.0_1773786598030_0.405193044662034","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@dooraccess/opendoor-web-sdk","version":"1.0.1","keywords":["door","access","sdk","door-code","latch"],"license":"MIT","_id":"@dooraccess/opendoor-web-sdk@1.0.1","maintainers":[{"name":"ryan.salmons.door","email":"ryan.salmons@latch.com"}],"dist":{"shasum":"a74e49dee7d51c158698b580bc8b71db3ca22b82","tarball":"https://registry.npmjs.org/@dooraccess/opendoor-web-sdk/-/opendoor-web-sdk-1.0.1.tgz","fileCount":11,"integrity":"sha512-SNDasIZvAiipO32GN4ralquuC8lpdqSmdb5ihkE8Iq6WmkB2+opVchr2i37kp3nM27xjrCTaVZ3NeDcxB8FKHQ==","signatures":[{"sig":"MEUCIQDWOr9FIMNfyppyx1i15H78dMvteQ0YWRoNQxzFrlofowIgc/ocMMWoI//dqoEHaGRCYmuro29KQyERfZfwrYstp/E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":164018},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","unpkg":"./dist/index.umd.js","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"9ca892c1c7774a339baae9a7f4e76d619b61da5e","scripts":{"dev":"tsup --watch","demo":"npm --prefix demo run start","lint":"eslint src/ tests/","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","prepare":"husky","lint:fix":"eslint src/ tests/ --fix","test:e2e":"playwright test","demo:live":"LATCH_CLIENT_ID=$LATCH_CLIENT_ID LATCH_CLIENT_SECRET=$LATCH_CLIENT_SECRET npm --prefix demo run start","typecheck":"tsc --noEmit","test:watch":"vitest","demo:install":"npm --prefix demo install","format:check":"prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"ryan.salmons.door","email":"ryan.salmons@latch.com"},"jsdelivr":"./dist/index.umd.js","_npmVersion":"11.9.0","description":"JavaScript/TypeScript SDK for embedding door code access into web applications","directories":{},"lint-staged":{"*.ts":["eslint --fix","prettier --write"]},"sideEffects":false,"_nodeVersion":"24.14.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","husky":"^9.0.0","eslint":"^9.0.0","vitest":"^3.0.0","prettier":"^3.0.0","@eslint/js":"^9.0.0","typescript":"^5.7.0","lint-staged":"^15.0.0","@playwright/test":"^1.50.0","typescript-eslint":"^8.0.0","@vitest/coverage-v8":"^3.0.0","eslint-config-prettier":"^10.0.0"},"_npmOperationalInternal":{"tmp":"tmp/opendoor-web-sdk_1.0.1_1773790949011_0.8330839520712834","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@dooraccess/opendoor-web-sdk","version":"1.0.2","description":"JavaScript/TypeScript SDK for embedding door code access into web applications","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"unpkg":"./dist/index.umd.js","jsdelivr":"./dist/index.umd.js","sideEffects":false,"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","test:e2e":"playwright test","lint":"eslint src/ tests/","lint:fix":"eslint src/ tests/ --fix","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","format:check":"prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"","typecheck":"tsc --noEmit","demo":"npm --prefix demo run start","demo:live":"LATCH_CLIENT_ID=$LATCH_CLIENT_ID LATCH_CLIENT_SECRET=$LATCH_CLIENT_SECRET npm --prefix demo run start","demo:install":"npm --prefix demo install","prepare":"husky"},"keywords":["door","access","sdk","door-code","latch"],"license":"MIT","devDependencies":{"@eslint/js":"^9.0.0","@playwright/test":"^1.50.0","@vitest/coverage-v8":"^3.0.0","eslint":"^9.0.0","eslint-config-prettier":"^10.0.0","husky":"^9.0.0","lint-staged":"^15.0.0","prettier":"^3.0.0","tsup":"^8.0.0","typescript":"^5.7.0","typescript-eslint":"^8.0.0","vitest":"^3.0.0"},"lint-staged":{"*.ts":["eslint --fix","prettier --write"]},"gitHead":"175d1daaf5eae4ccd2105fc3433013ce8e460a7c","_id":"@dooraccess/opendoor-web-sdk@1.0.2","_nodeVersion":"24.14.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-8GIgHQos9/Dh1LucCTJJn6BziPElCrNXonlIJ6KrxuhRZIaTOrjZ2BqTurLqr4dYDeyRqrNroRQ4WqTGTeVBxw==","shasum":"371b7711c39c781dfe703dc62d341596b976e168","tarball":"https://registry.npmjs.org/@dooraccess/opendoor-web-sdk/-/opendoor-web-sdk-1.0.2.tgz","fileCount":11,"unpackedSize":173940,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQC3cnWAbEnMp2ee2b0k1oGt8dtbt78wIv4wdo9EvOk6LAIgTfIsfspdRYt41aFnfdmTEWpnyp6HSOnAQHCDxu5Trs4="}]},"_npmUser":{"name":"ryan.salmons.door","email":"ryan.salmons@latch.com"},"directories":{},"maintainers":[{"name":"ryan.salmons.door","email":"ryan.salmons@latch.com"},{"name":"sacramone","email":"dean.sacramone@door.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/opendoor-web-sdk_1.0.2_1776821205373_0.9590628339244345"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-17T22:29:57.942Z","modified":"2026-04-22T01:26:45.705Z","1.0.0":"2026-03-17T22:29:58.181Z","1.0.1":"2026-03-17T23:42:29.154Z","1.0.2":"2026-04-22T01:26:45.530Z"},"license":"MIT","keywords":["door","access","sdk","door-code","latch"],"description":"JavaScript/TypeScript SDK for embedding door code access into web applications","maintainers":[{"name":"ryan.salmons.door","email":"ryan.salmons@latch.com"},{"name":"sacramone","email":"dean.sacramone@door.com"}],"readme":"# OpenDOOR Web SDK\n\nJavaScript/TypeScript SDK for embedding door code access into your web application. Your guests authenticate via email OTP, and the SDK fetches and displays their door codes.\n\n## Installation\n\n```bash\nnpm install @dooraccess/opendoor-web-sdk\n```\n\nOr load via CDN:\n\n```html\n<script src=\"https://unpkg.com/@dooraccess/opendoor-web-sdk\"></script>\n```\n\n## How It Works\n\nThe integration has two parts: your **backend** handles authentication (because it requires secrets), and the **web SDK** runs in the guest's browser and fetches door codes.\n\n```\nGuest's Browser                   Your Backend                      DOOR\n───────────────                   ────────────                      ────\n\n1. Guest enters email       ──>   POST /passwordless/start     ──>  DOOR sends OTP email\n                                  (includes client_id + secret)\n\n2. Guest enters OTP code    ──>   POST /oauth/token            ──>  DOOR returns JWT\n                                  (includes client_id + secret)      + refresh_token\n                            <──   Return JWT to browser         <──\n\n3. Browser initializes SDK\n   with the JWT\n\n4. SDK calls getLocks()     ──>  Returns locks + door codes\n\n5. JWT expires, SDK calls\n   onTokenExpired()         ──>   POST /oauth/token            ──>  DOOR refresh_token grant\n                                  (includes client_id + secret)\n                            <──   Return new JWT                <──\n```\n\nSteps 1, 2, and 5 go through your backend because they require `client_id` and `client_secret`, which must never be exposed in browser code. The SDK's lock/device calls run in the browser and call the DOOR API directly with the JWT.\n\n## Step 1: Your Backend — Authentication\n\nYour backend handles the passwordless OTP flow. The SDK does not manage authentication — it receives a JWT from your backend.\n\n### Obtain credentials\n\nYou'll receive a `client_id` and `client_secret` from DOOR during onboarding. These are scoped to your account.\n\n### Send the OTP email\n\nWhen a guest wants to access their door codes, your backend triggers an OTP email:\n\n```\nPOST https://auth.prod.latch.com/passwordless/start\nContent-Type: application/json\n\n{\n  \"client_id\": \"YOUR_CLIENT_ID\",\n  \"client_secret\": \"YOUR_CLIENT_SECRET\",\n  \"connection\": \"email\",\n  \"send\": \"code\",\n  \"email\": \"guest@example.com\"\n}\n```\n\nThis sends a 6-digit code to the guest's email.\n\n### Exchange the OTP for a JWT\n\nAfter the guest enters the code, your backend exchanges it for tokens:\n\n```\nPOST https://auth.prod.latch.com/oauth/token\nContent-Type: application/json\n\n{\n  \"grant_type\": \"http://auth0.com/oauth/grant-type/passwordless/otp\",\n  \"client_id\": \"YOUR_CLIENT_ID\",\n  \"client_secret\": \"YOUR_CLIENT_SECRET\",\n  \"username\": \"guest@example.com\",\n  \"otp\": \"123456\",\n  \"realm\": \"email\",\n  \"scope\": \"openid profile email offline_access\",\n  \"audience\": \"https://rest.latchaccess.com/access/sdk\"\n}\n```\n\n**The `audience` parameter is required.** Without it, the JWT will not have the correct permissions to call the DOOR API.\n\nResponse:\n\n```json\n{\n  \"access_token\": \"eyJhbGciOi...\",\n  \"refresh_token\": \"v1.MjY0OTFk...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 86400\n}\n```\n\n- **`access_token`**: Pass this to the SDK. Valid for 24 hours.\n- **`refresh_token`**: Store this securely on your backend. Used to get a new `access_token` when the current one expires.\n\n### Return the JWT to the browser\n\nYour backend returns the `access_token` to the browser (e.g., via a JSON response). **Never return the `refresh_token` or `client_secret` to the browser.**\n\n## Step 2: Browser — Initialize the SDK\n\nOnce the browser has the JWT, initialize the SDK:\n\n```typescript\nimport { OpenDOORClient } from '@dooraccess/opendoor-web-sdk';\n\nconst client = new OpenDOORClient({\n  token: jwtFromYourBackend,\n  onTokenExpired: async () => {\n    // Call your backend to refresh the token (see Step 3)\n    const res = await fetch('/api/auth/refresh', { method: 'POST' });\n    const { token } = await res.json();\n    return token;\n  },\n});\n\n// Fetch all locks and door codes\nconst locks = await client.getLocks();\n\nlocks.forEach(lock => {\n  console.log(`${lock.name}: ${lock.doorCode ?? 'No code — use app to unlock'}`);\n});\n\n// Clean up when done\nclient.destroy();\n```\n\nVia CDN / script tag:\n\n```html\n<script src=\"https://unpkg.com/@dooraccess/opendoor-web-sdk\"></script>\n<script>\n  var client = new OpenDOOR.OpenDOORClient({\n    token: jwtFromYourBackend,\n    onTokenExpired: function () {\n      return fetch('/api/auth/refresh', { method: 'POST' })\n        .then(function (res) { return res.json(); })\n        .then(function (data) { return data.token; });\n    },\n  });\n\n  client.getLocks().then(function (locks) {\n    // Render locks in your UI\n  });\n</script>\n```\n\n## Step 3: Your Backend — Token Refresh\n\nJWTs expire after 24 hours. The SDK detects expiry and calls your `onTokenExpired` callback automatically. Your backend should use the stored `refresh_token` to get a new `access_token`:\n\n```\nPOST https://auth.prod.latch.com/oauth/token\nContent-Type: application/json\n\n{\n  \"grant_type\": \"refresh_token\",\n  \"client_id\": \"YOUR_CLIENT_ID\",\n  \"client_secret\": \"YOUR_CLIENT_SECRET\",\n  \"refresh_token\": \"STORED_REFRESH_TOKEN\",\n  \"audience\": \"https://rest.latchaccess.com/access/sdk\"\n}\n```\n\n**The `audience` parameter is required on refresh too.** Without it, the new JWT won't have the correct permissions.\n\nResponse:\n\n```json\n{\n  \"access_token\": \"eyJhbGciOi...\",\n  \"token_type\": \"Bearer\",\n  \"expires_in\": 86400\n}\n```\n\nReturn the new `access_token` to the browser. If a new `refresh_token` is included in the response, update your stored copy.\n\n### How the SDK handles expiry\n\nThe SDK checks the JWT's `exp` claim before every API call. If expired (or within 30 seconds of expiry):\n\n1. **If `onTokenExpired` is provided**: The SDK calls it, waits for the new token, and retries the request automatically. The guest never sees an interruption.\n2. **If `onTokenExpired` is not provided**: The SDK throws an `AuthError` with a message explaining what happened.\n\nIf a request returns `401` (token rejected server-side), the same flow applies — the SDK calls `onTokenExpired` and retries once.\n\n## Avoiding Repeated OTP Verification\n\nThe OTP flow only needs to happen once. After the initial verification, your backend has a `refresh_token` that can mint new JWTs for the duration of the guest's stay — no additional OTP emails needed.\n\nThe recommended approach is to tie the DOOR `refresh_token` to your existing user session:\n\n1. **Guest verifies OTP once** — your backend receives the `access_token` and `refresh_token` from DOOR\n2. **Store the `refresh_token` in your session** — associate it with the guest's session in your app (e.g., in your session store, database, or server-side cache, keyed to your session cookie)\n3. **On subsequent page loads** — the guest is already logged into your app. Your backend checks the session, finds the stored DOOR `refresh_token`, mints a fresh `access_token`, and passes it to the SDK. No OTP required.\n4. **The SDK's `onTokenExpired` callback** — follows the same path. It calls your backend refresh endpoint, which uses the stored `refresh_token` to get a new JWT silently.\n\n```\nFirst Visit                        Subsequent Visits\n───────────                        ─────────────────\nGuest enters email                 Guest loads page (already logged in)\n  ↓                                  ↓\nOTP email sent                     Your backend checks session\n  ↓                                  ↓\nGuest enters code                  Finds stored DOOR refresh_token\n  ↓                                  ↓\nBackend gets JWT + refresh_token   Calls DOOR to mint fresh JWT\n  ↓                                  ↓\nStores refresh_token in session    Returns JWT to browser\n  ↓                                  ↓\nReturns JWT to browser             SDK initialized — door codes load\n  ↓\nSDK initialized — door codes load\n```\n\nThe guest only sees the OTP screen on their very first visit. Every visit after that, door codes load automatically as part of your normal page load.\n\n## API Reference\n\n### `OpenDOORClient`\n\n| Option | Type | Required | Default | Description |\n|--------|------|----------|---------|-------------|\n| `token` | `string` | Yes | — | JWT `access_token` from the auth flow |\n| `onTokenExpired` | `() => Promise<string> \\| string` | No | — | Called when the token expires. Return a fresh JWT. |\n| `timeout` | `number` | No | `15000` | Request timeout in milliseconds |\n| `maxRetries` | `number` | No | `2` | Max retries for 5xx, 429, and network errors |\n| `includeAllDevices` | `boolean` | No | `false` | When `true`, retrieves all credentials for doors the user can unlock. When `false`, retrieves only credentials generated from accesses granted by the Partner that minted the user's JWT. |\n\n#### Methods\n\n| Method | Description |\n|--------|-------------|\n| `getLocks(): Promise<Lock[]>` | Fetch all locks accessible to the authenticated user |\n| `getLock(lockId: string): Promise<Lock>` | Fetch a single lock by its device UUID |\n| `updateToken(newToken: string): void` | Manually replace the current JWT |\n| `isAuthenticated(): boolean` | Returns `true` if the current token has not expired |\n| `destroy(): void` | Clean up the client. All subsequent calls will throw. |\n\n### Types\n\n```typescript\ninterface Lock {\n  id: string;              // Device UUID\n  name: string;            // e.g. \"Building Entrance\", \"Unit 4B\"\n  buildingId: string;      // Building UUID\n  startTime: Date;         // Access window start\n  endTime: Date | null;    // Access window end (null = no end date)\n  doorCode: string | null; // Door code PIN, or null if not available\n}\n```\n\n### Error Types\n\nAll errors extend `SDKError`.\n\n| Error | When | Properties |\n|-------|------|------------|\n| `AuthError` | Token expired with no `onTokenExpired` callback, or refresh returned an invalid token | `statusCode` |\n| `APIError` | DOOR API returned a non-success response (4xx, 5xx) | `statusCode`, `responseBody` |\n| `NotFoundError` | `getLock(lockId)` could not find a matching lock in the returned device list | — |\n| `NetworkError` | Network failure — DNS, timeout, connection refused | `cause` |\n| `ConfigError` | Invalid configuration (e.g., empty token) | — |\n\n### Error Handling Example\n\n```typescript\nimport { OpenDOORClient, AuthError, APIError, NetworkError } from '@dooraccess/opendoor-web-sdk';\n\ntry {\n  const locks = await client.getLocks();\n} catch (error) {\n  if (error instanceof AuthError) {\n    // Token expired and refresh failed — redirect to login\n    redirectToLogin();\n  } else if (error instanceof APIError) {\n    // Server error — show message, maybe retry later\n    console.error(`API error ${error.statusCode}:`, error.responseBody);\n  } else if (error instanceof NetworkError) {\n    // Offline or connectivity issue\n    showOfflineMessage();\n  }\n}\n```\n\n## Security Notes\n\n- **Never expose `client_id` or `client_secret` in browser code.** All auth calls must go through your backend.\n- **Never send `refresh_token` to the browser.** Store it server-side and expose a refresh endpoint that returns a new `access_token`.\n- **The `audience` parameter is required** on both the initial token exchange and refresh calls. Without it, the JWT will lack the necessary permissions.\n\n## Browser Support\n\nThe SDK works in all modern browsers:\n\n- Chrome 60+\n- Firefox 55+\n- Safari 11+\n- Edge 79+\n\n## Support\n\nContact your DOOR account representative for integration assistance.\n\n## License\n\nMIT\n","readmeFilename":"README.md"}