{"_id":"@ape-egg/async-await-websockets","_rev":"5-63c95affbb0b269e594ff540322d2e5b","name":"@ape-egg/async-await-websockets","dist-tags":{"latest":"3.4.0"},"versions":{"3.0.7":{"name":"@ape-egg/async-await-websockets","version":"3.0.7","author":{"name":"Kkortes"},"license":"ISC","_id":"@ape-egg/async-await-websockets@3.0.7","maintainers":[{"name":"kortes","email":"me@korte.kim"}],"dist":{"shasum":"269d76c4826deb4e30894ce2af121c8e6f1f1831","tarball":"https://registry.npmjs.org/@ape-egg/async-await-websockets/-/async-await-websockets-3.0.7.tgz","fileCount":5,"integrity":"sha512-2JEm3ioawjcEj9qOS1saJxNGh7nTQdn1uQcuTC9kTEIoVRlN4iBkN/YTimbzYqcSwCO8lGWkW0XJqKgbxpG5CQ==","signatures":[{"sig":"MEQCIFhxgFpbr8h54ib35Ah5Fc7dIdz/HyYs6mn552ckIi/8AiBDVGqHbRaZiqLtyypjPGpcWZdfSNYWTybcSuApVqhgEQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":9412},"main":"server.js","type":"module","types":"index.d.ts","module":"client.js","gitHead":"3b8f11e48f0683f5ccde03ccecb4360281aba05c","scripts":{"start":"node server.js"},"_npmUser":{"name":"kortes","email":"me@korte.kim"},"_npmVersion":"11.6.2","description":"A async/await solution to websockets","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/async-await-websockets_3.0.7_1780001119603_0.009692817863075742","host":"s3://npm-registry-packages-npm-production"}},"3.1.0":{"name":"@ape-egg/async-await-websockets","version":"3.1.0","author":{"name":"Kkortes"},"license":"ISC","_id":"@ape-egg/async-await-websockets@3.1.0","maintainers":[{"name":"kortes","email":"me@korte.kim"}],"dist":{"shasum":"fa41890f56af42b0097799907c50ec41d2abebb9","tarball":"https://registry.npmjs.org/@ape-egg/async-await-websockets/-/async-await-websockets-3.1.0.tgz","fileCount":6,"integrity":"sha512-/ZzOvT/I0rn41MiLUilTjlQ2UtMD0dVg48ut0uNSMhmE/AalVwPJ4BD6BoOAhlcC9dui2Vv2XnUCsLEnO8QKsQ==","signatures":[{"sig":"MEYCIQCkXYHim4cjy7KfZWGQTJAXlPq4xD7+iAIkM+WABHXI1AIhANztd2TOURUTURLEQtCGpQutP1ktVG/6OkMztiHoh2vj","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":13108},"main":"server.js","type":"module","types":"index.d.ts","module":"client.js","gitHead":"9463d47882963abc6ba8e95d8fd2d0bdb9d9489d","scripts":{"start":"node server.js","publish":"bun publish:check && ./scripts/publish.sh","publish:check":"npm pack --dry-run"},"_npmUser":{"name":"kortes","email":"me@korte.kim"},"_npmVersion":"11.6.2","description":"A async/await solution to websockets","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/async-await-websockets_3.1.0_1782814213394_0.5496859462799772","host":"s3://npm-registry-packages-npm-production"}},"3.2.1":{"name":"@ape-egg/async-await-websockets","version":"3.2.1","author":{"name":"Kkortes"},"license":"ISC","_id":"@ape-egg/async-await-websockets@3.2.1","maintainers":[{"name":"kortes","email":"me@korte.kim"}],"dist":{"shasum":"92715ad2b7c71f8b3dad94704a3d211de55c01d7","tarball":"https://registry.npmjs.org/@ape-egg/async-await-websockets/-/async-await-websockets-3.2.1.tgz","fileCount":14,"integrity":"sha512-RhUK9nclHBpE98DWrxgTZGF/UviNyIPBBDnZywtrBvsfIKtvxP42fMAzWW2ty41cKih1z5mLuTgBL8lRqff9bw==","signatures":[{"sig":"MEYCIQDlatlq24iN3rDkf++K+cVm3eyP6D3sGZ86I2k/PQFViAIhAOpwKBiuIs/Ox+twUJEiZ51EhvgsWYt18lBsYeN0YpM9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37904},"main":"server.js","type":"module","types":"index.d.ts","module":"client.js","gitHead":"a52ee1ec5b19efad9a8ef1b16c0dba2a6a110fc0","scripts":{"start":"node server.js","publish":"bun publish:check && ./scripts/publish.sh","publish:check":"npm pack --dry-run"},"_npmUser":{"name":"kortes","email":"me@korte.kim"},"_npmVersion":"11.6.2","description":"A async/await solution to websockets","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/async-await-websockets_3.2.1_1787206571168_0.695327971526881","host":"s3://npm-registry-packages-npm-production"}},"3.3.0":{"name":"@ape-egg/async-await-websockets","version":"3.3.0","author":{"name":"Kkortes"},"license":"ISC","_id":"@ape-egg/async-await-websockets@3.3.0","maintainers":[{"name":"kortes","email":"me@korte.kim"}],"dist":{"shasum":"1efa0e195ae8fcb66b7a16afa92efa8da6b5936b","tarball":"https://registry.npmjs.org/@ape-egg/async-await-websockets/-/async-await-websockets-3.3.0.tgz","fileCount":14,"integrity":"sha512-rM6cTXVS+i7pG7wlIJBbbHL0MDUzRcuLv3/eFW8xibz1s9WYLITRG4qQ9+k5IerWiRSDo2t/PTP26XbyDZG9nA==","signatures":[{"sig":"MEUCIGQAZhsXranhoLHhNAIK8ktFXdQb8FthmMm178mWo1/jAiEAxrg77Oj046LqVOmVmycXqz8hM+xG0pmg7fgPlvKrjPM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":40647},"main":"server.js","type":"module","types":"index.d.ts","module":"client.js","gitHead":"7b8ea9224360a40f1ae5a6376338f8f3fe7a6390","scripts":{"start":"node server.js","release":"bun publish:check && ./scripts/publish.sh","publish:check":"npm pack --dry-run"},"_npmUser":{"name":"kortes","email":"me@korte.kim"},"_npmVersion":"11.6.2","description":"A async/await solution to websockets","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/async-await-websockets_3.3.0_1787339067616_0.4209513972597405","host":"s3://npm-registry-packages-npm-production"}},"3.4.0":{"_id":"@ape-egg/async-await-websockets@3.4.0","dist":{"shasum":"34388b0f5d6abc0915639c449c66f473bf3fc00d","tarball":"https://registry.npmjs.org/@ape-egg/async-await-websockets/-/async-await-websockets-3.4.0.tgz","fileCount":14,"integrity":"sha512-yoZgirhEPMyGK/OH/8YfrV+tIHlV/SqXu8yHmzpkk3QuMJbgTbZkitxHvyd050aiHrTzeDa7CvjLZDrzk2JrlQ==","signatures":[{"sig":"MEUCIHB6aMbpXtfYSlHHRjTL2Wf9Y7qGVbzQZaYkHZ8gCqROAiEA4aF44xSl6UoFLJt5JG9gKyd3XPJcdAF2SqDIjfqfqLU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDCWDBIAyTbgihFEWcabezz37rjTNaUIlP7pipDqsTPRwIgbxxW3U8PKHjWkDUWmL67oK4zyP3wn9813zscobVOMo0="}],"unpackedSize":42438},"main":"server.js","name":"@ape-egg/async-await-websockets","type":"module","types":"index.d.ts","author":{"name":"Kkortes"},"module":"client.js","gitHead":"2bda8af250efe12ff2ea806af6a28df19b784fdd","license":"ISC","scripts":{"start":"node server.js","release":"bun publish:check && ./scripts/publish.sh","publish:check":"npm pack --dry-run"},"version":"3.4.0","_npmUser":{"name":"kortes","email":"me@korte.kim"},"_npmVersion":"11.6.2","description":"A async/await solution to websockets","directories":{},"maintainers":[{"name":"kortes","email":"me@korte.kim"}],"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/async-await-websockets_3.4.0_1788475185925_0.21674122354060543"}}},"time":{"created":"2026-05-28T20:45:19.085Z","modified":"2026-09-03T22:39:46.204Z","3.0.7":"2026-05-28T20:45:19.793Z","3.1.0":"2026-06-30T10:10:13.541Z","3.2.1":"2026-08-20T06:16:11.323Z","3.3.0":"2026-08-21T19:04:27.748Z","3.4.0":"2026-09-03T22:39:46.029Z"},"author":{"name":"Kkortes"},"license":"ISC","description":"A async/await solution to websockets","maintainers":[{"name":"kortes","email":"me@korte.kim"}],"readme":"# aaw\n\n**[aaw.korte.kim](https://aaw.korte.kim)** — documentation\n\n![](https://wallpaperaccess.com/full/374183.jpg)\n\n## Major update since v3.0.0+\n\nAsync-await-websockets is now running on Bun (https://bun.sh/). Until the most popular runtime hosts have support for Bun you'll have to run it on your own custom server _or_ in a docker container.\n\n## async-await-websockets\n\n- ✅ Uses native `websockets`\n  - CLIENT (https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)\n  - SERVER (https://github.com/websockets/ws)\n- ✅ Enables `async/await` messaging from the client\n- ✅ Broadcast messages\n- ✅ Automatic reconnection\n- ✅ Rooms — targeted multicast to named subsets of connections\n- ✅ Client authentication (optional)\n\n## How to create your own server\n\n1. `mkdir my-server`\n2. `cd my-server`\n3. `bun init`\n4. Add to package.json\n\n```\n\"scripts\": {\n  \"dev\": \"bun --watch index.js\"\n},\n```\n\n5. `bun install async-await-websockets`\n6. Create `index.js` with contents:\n\n```\nimport aaw from \"async-await-websockets\";\n\naaw(\"events\");\n```\n\n7. `mkdir events`\n8. `bun dev`\n\nYour server should now be reachable on ws://localhost:1337\n\n## Configuration\n\n`aaw(eventDir, services, port, log, auth, maxPayloadLength)`\n\n### eventDir (string)\n\nName of directory that holds your socket events.\n\nDefault: `events`\n\n### services (object)\n\nThird party services that you need access to in your socket events (e.g. database connection). `ws` and `room` are always exposed and cannot be removed.\n\nDefault: `{ ws: [Websocket Object], room: [Room API] }`\n\n### port (integer)\n\nA port of your liking.\n\nDefault: `1337`\n\n### log (function)\n\nWith the parameter signature `(event, websocketKey, async, error, body, result)` you can create custom server logging for all events called through `root`-directory.\n\nDefault: `undefined`\n\n### auth (object | boolean)\n\nOptional authentication. `false` (the default) leaves aaw a pure transport; `true` enables it\nwith the built-in SQLite store. See [Authentication](#authentication).\n\nDefault: `false`\n\n### maxPayloadLength (integer)\n\nThe largest inbound websocket frame the server accepts, in bytes. A frame over the limit\nnever reaches a handler — Bun closes the connection (1006, *Received too big message*), which\na client experiences as a dead socket and a reconnect loop if it retries the same send. The\nserver names the closed frame and the configured limit on `console.error`, so the failure is\nloud where it can be seen. Raise the limit when your events carry large payloads; remember\nbase64 is a third larger than the bytes it encodes.\n\nDefault: `16777216` (16 MiB, Bun's own default)\n\n## Your server\n\n`aaw` returns an `Bun websocket`-instance (https://bun.sh/docs/api/websockets)\n\nEach `.js` file in `events` is scanned and available with `ws.sendAsync('dir/file')`\n\nThis is the signature for any `.js` file within `events`:\n\n```\nexport default async (body, services) => {\n  const response = await services.mongo.insertSomething(body.id);\n  services.ws.sendEvent('notify-about-insertion', { id: response.id });\n  return response;\n}\n```\n\nOmitting the `async` keyword will treat the event as a regular websocket event.\n\n## Authentication\n\nOff by default — aaw stays the transport it has always been. Switch it on with a fifth\nargument and you get server-minted sessions, a folder convention for who may call what, and\na SQLite user store you never have to configure.\n\n```js\naaw(\"events\", { mongo }, 1337, log, true);\n```\n\n### The folder convention\n\n**Events under `auth/` require a session; everything else is open.** No per-file flag, no\ncentral policy list — where the file sits *is* the rule, the same way its path is already\nits name.\n\n```\nevents/health.js            → \"health\"           anyone\nevents/teamplay/list.js     → \"teamplay/list\"    anyone\nevents/auth/chat.js         → \"auth/chat\"        needs a session\nevents/auth/admin/seed.js   → \"auth/admin/seed\"  needs a session\n```\n\nTurning authentication **off does not open those events** — it makes them unreachable, and\naaw says so at boot:\n\n```\nAuthentication is off — 2 event(s) under auth/ are unreachable: [ \"auth/chat\", \"auth/admin/seed\" ]\n```\n\nA caller that tries anyway is told why, rather than being served:\n\n```\nAuthentication is not enabled — auth/chat is unreachable\n```\n\nSo forgetting to configure auth can never be the thing that exposes a protected event.\n\n### Connecting\n\nThe connection itself is free — anyone may open a socket. What a token buys is the right to\ncall anything under `auth/`.\n\n```js\nimport aaw from \"@ape-egg/async-await-websockets/client.js\";\n\nconst ws = aaw(\"wss://example.com\");\n\nawait ws.sendAsync(\"aaw/login\", { email, password });\nawait ws.sendAsync(\"auth/chat\", { id, text });\nawait ws.sendAsync(\"aaw/logout\");\n```\n\nThere is no separate login API — aaw's own events are called the way every other event\nis.\n\nThe session binds to the connection, so it is established once rather than re-proven on\nevery message. Handlers receive it as `identity`:\n\n```js\nexport default async ({ id, text }, { identity, room }) => {\n  room.emit(`teamplay:${id}`, \"chat\", { from: identity.email, text });\n};\n```\n\nThe client keeps the token from those events and replays it after an automatic reconnect,\nbefore `open` fires — so a first call made inside `open` cannot race a reconnect it never\nsaw. If the session has expired by then the client emits `unauthorized` instead.\n\nStore the token yourself to survive a page reload:\n\n```js\nconst ws = aaw(\"wss://example.com\", { token: localStorage.token });\nws.on(\"unauthorized\", () => delete localStorage.token);\n```\n\n### Built-in events\n\naaw's own events live in the package's own `events/aaw/` folder, one file per event, named by\ntheir path exactly like yours, and registered alongside yours when authentication is on. They cannot sit under\n`auth/` themselves — a caller has to be able to log in before it holds anything to log in\nwith. An event file of your own that collides stops the server at boot rather than being\nsilently shadowed.\n\n```\nevents/aaw/login.js                   → \"aaw/login\"\nevents/aaw/password/request-reset.js  → \"aaw/password/request-reset\"\n```\n\nThey are ordinary event files. Each declares the provider it belongs to, so naming a\ndifferent provider simply leaves it unregistered:\n\n```js\nexport const provider = \"sqlite\";\n\nexport default async ({ email, password }, { authenticate, auth: { store } }) => {\n  const user = await store.verify(email, password);\n\n  if (!user) throw Error(\"Invalid credentials\");\n\n  return authenticate(user);\n};\n```\n\n| Event | Does |\n|---|---|\n| `aaw/register` | Create an account and bind a session |\n| `aaw/login` | Bind a session to this connection |\n| `aaw/resume` | Re-bind an existing token (what reconnects use) |\n| `aaw/logout` | End the session everywhere |\n| `aaw/password/request-reset` | Mint a reset token and hand it to `onPasswordReset` |\n| `aaw/password/set-new` | Consume a reset token and set a new password |\n\nReset tokens are `crypto.randomUUID()` with a real expiry, checked where the password\nactually changes. Delivering one is your app's business, so aaw hands it over and stays out\nof the mail:\n\n```js\nimport { Resend } from \"resend\";\n\nconst resend = new Resend(process.env.RESEND_API_KEY);\n\naaw(\"events\", {}, 1337, undefined, {\n  onPasswordReset: ({ user, token }) =>\n    resend.emails.send({\n      from: \"Acme <no-reply@acme.com>\",\n      to: user.email,\n      subject: \"Reset your password\",\n      html: `<a href=\"https://acme.com/reset#${token}\">Reset your password</a>`,\n    }),\n});\n```\n\nAny sender works the same way — Resend, Postmark, SES, SMTP, or your own queue. Without an\n`onPasswordReset` handler there is no way to deliver a token, so `aaw/password/request-reset`\nanswers with an error rather than a success nobody can act on.\n\n`request-reset` answers `{ ok: true }` for a known and an unknown address alike, so it\ncannot be used to enumerate accounts. Passwords are hashed with `Bun.password` (Argon2id),\nwhich is what makes a guessed password cost the attacker real CPU on every attempt.\n\n### Permissions\n\nAn identity may carry `allowed` — globs matched against the event path. Without it, any\nsession may call any protected event.\n\n```js\n{ allowed: [\"auth/teamplay/*\"] }   // a player\n{ allowed: [\"*\"] }                 // an admin\n```\n\n### Providers\n\n`providers` defaults to `[\"sqlite\"]`, aaw's built-in store. Naming any other provider turns\nthe built-in password login off unless you list it too.\n\n```js\naaw(\"events\", {}, 1337, undefined, {\n  providers: [\"sqlite\", { name: \"google\", clientId, clientSecret, start, callback }],\n});\n```\n\nA social provider redirects a browser, so it arrives over HTTP rather than the socket — the\nonly reason aaw ever answers a plain request. aaw owns the session half (find or create the\nuser, link `provider` + `subject`, mint a token, redirect back with it in the URL fragment);\na provider owns the handshake half, as two functions:\n\n```js\n{\n  name: \"google\",\n  redirect: \"/\",                                  // token arrives as #token=…\n  start: (request) => Response.redirect(authorizeUrl, 302),\n  callback: async (request) => ({ subject, email }),   // verified profile\n}\n```\n\nIt is served at `/auth/google` and `/auth/google/callback` — HTTP paths, unrelated to the\n`auth/` event folder. An address already registered here links to that account rather than\ncolliding with it, which holds only because `callback` returns an address the provider\nverified.\n\n**No OAuth provider ships yet** — the store, the routes and the contract are in place so one\ncan be added as a small module, and so social login does not need a schema change later.\n\n### Bringing your own store\n\nThe SQLite store is a default, not a requirement. Pass `store` and aaw never opens a\ndatabase — useful when users already live in mongo, or when \"users\" are API keys in a\ncommitted file.\n\n```js\naaw(\"events\", { mongo }, 1337, undefined, {\n  store: {\n    findUser: (email) => …,\n    verify: async (email, password) => identity | null,\n    createSession: (user, ttl) => token,\n    readSession: (token) => identity | null,\n    endSession: (token) => …,\n  },\n});\n```\n\nA store only needs what the features you enable actually call.\n\nYour own login event can bind a session directly, for credentials aaw knows nothing about:\n\n```js\nexport default async ({ license }, { authenticate }) =>\n  authenticate(await lookupByLicense(license));\n```\n\n## Rooms\n\nEvery event handler receives a `room` API alongside `ws`. Rooms are named subsets of connections you can multicast to — useful for chat channels, game lobbies, or any group of clients that should receive the same event. Membership is per-connection and clears automatically when a client disconnects.\n\n```\nexport default (body, { ws, room }) => {\n  room.join(body.channel);\n  room.emit(body.channel, 'joined', { id: ws.data }, ws);\n};\n```\n\n### `room` API\n\n- `room.join(name)` — add the current connection to room `name` (created on demand).\n- `room.leave(name)` — remove the current connection from room `name` (room is deleted when empty).\n- `room.emit(name, event, data, except?)` — send `[event, data]` to every member of `name`, optionally skipping one connection (e.g. pass `ws` to exclude the sender). Returns the number of clients sent to.\n- `room.size(name)` — number of connections currently in room `name`.\n\n`emit` is connection-agnostic, so a later callback (e.g. a timer) can multicast to a room after the triggering message has resolved.\n\n### Rooms carry no authentication\n\nA socket receives a room's messages because a handler called `room.join` for it — nothing more. The guard only sees inbound calls, so it never inspects who is in a room. If a room carries data only some connections should see, gate the join: put the event that calls `room.join` under `auth/`, or guard it on `identity`. Otherwise a public event that joins a socket to a protected room lets that socket receive everything emitted to it, even though it could never write to it.\n\n```js\n// events/auth/subscribe.js — only a session can join, so only a session receives\nexport default (body, { room }) => {\n  room.join(`teamplay:${body.id}`);\n};\n```\n\n## Your client\n\n`npm install async-await-websockets`\n\n```\nimport aaw from 'async-await-websockets';\n\nconst ws = aaw('wss://websocket-server.url:1337');\n\nws.on('open', () => {\n  (async () => {\n    try {\n      const result = await ws.sendAsync('example-async', { somedata: \"for the backend\" });\n      console.info(result);\n    } catch ({ error }) {\n      console.error(error);\n    }\n  })();\n});\n```\n\n### `ws.sendAsync` parameters:\n\n- `event name` (string, required)\n- `payload` (any, default `undefined`)\n- `timeout in ms` (integer, default `3000`)\n\n## Error handling\n\nWhen calling `ws.sendAsync('some-event')` there are two possible failures:\n\n1. The call to your socket server timed out (happens on the client).\n2. The server threw an error because something went wrong.\n\nIn both cases `sendAsync` will throw an object that contains an error-message like so:\n\n```\n{\n  error: \"What went wrong\"\n}\n```\n\n## Publishing\n\n- `bun run publish:check` — dry-run `npm pack` to preview the published tarball.\n- `bun run release` — runs the check, prompts for a new version (or keeps the current one), and publishes `@ape-egg/async-await-websockets` to npm.\n\nRequires being logged in to npm (`npm login`). The script is named `release` rather than `publish` because npm runs a script called `publish` again as a lifecycle hook once `npm publish` succeeds, which republishes the same version and fails on the second attempt.\n","readmeFilename":"README.md"}