diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2c5d683..a18e4da 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,7 +2,7 @@ version: 2 updates: # Each sample app needs its own entry, pointing at the folder that holds its package.json. - package-ecosystem: npm - directory: /app-client-starter-app + directory: /samples/app-client-starter-app schedule: interval: weekly groups: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d2208c..0883ec4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,14 +13,14 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: app-client-starter-app + working-directory: samples/app-client-starter-app steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - cache-dependency-path: app-client-starter-app/package-lock.json + cache-dependency-path: samples/app-client-starter-app/package-lock.json - run: npm ci - run: npm test - run: npm run lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c38e9bb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +Thanks for helping improve the Buffer sample apps. + +## What we accept + +- **Pull requests for fixes and improvements** to the existing samples are welcome. +- **New samples** aren't accepted at the moment. If you have an idea for one, please [open an issue](../../issues) instead. +- **Bugs and suggestions** can be reported by [opening an issue](../../issues). + +## Guidelines + +- Keep each sample small and copyable. A change should teach one clear Buffer API practice. +- Include a test when you change behavior. +- Avoid adding frameworks or dependencies unless they make the example clearer. +- Before opening a pull request, run the sample's checks. For the Node.js samples, that's `npm test` and `npm run lint`. +- Never commit `.env` files, certificates, client secrets, sessions, or real tokens. diff --git a/README.md b/README.md index 41873f1..c89b93d 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,69 @@ # Buffer Sample Apps + A collection of sample applications that show how to build on the Buffer API. Each sample is a small, self-contained project you can clone, run, and use as a starting point for your own build. > **Note:** These samples are meant for learning and prototyping. Make sure to review and adapt them to your own security, error-handling, and scaling needs before using them in production. -### Prerequisites - +## Samples + +| Sample | What it shows | Stack | +| --- | --- | --- | +| [App client starter app](samples/app-client-starter-app) | A third-party app that connects a Buffer account over OAuth 2.0 with PKCE, lists its channels, and adds a draft post to a channel's queue. | Node.js, Express | + +## Prerequisites + - A [Buffer](https://buffer.com) account -- API access credentials - see the [Buffer developer docs](https://developers.buffer.com/guides/authentication.html) for how to get them +- API access credentials. See the [Buffer developer docs](https://developers.buffer.com/guides/authentication.html) for how to get them. - Any language-specific tooling listed in the sample's own README ## Repository structure - -Each sample app lives in its own folder with everything it needs to run: - + +All sample apps live in the `samples/` directory. Each one has its own folder with everything it needs to run: + ``` . -├── sample-app-name/ -│ ├── README.md # Setup and usage instructions for this sample -│ ├── .env.example # Environment variables the sample needs -│ └── ... # Source code -├── another-sample/ -│ └── ... +├── samples/ +│ ├── sample-app-name/ +│ │ ├── README.md # Setup and usage instructions for this sample +│ │ ├── .env.example # Environment variables the sample needs +│ │ └── ... # Source code +│ └── another-sample/ +│ └── ... +├── CONTRIBUTING.md ├── LICENSE └── README.md ``` -### Run a sample app - +## Run a sample app + 1. Clone the repository: -```bash + + ```bash git clone https://github.com/bufferapp/buffer-sample-apps.git cd buffer-sample-apps -``` - + ``` + 2. Move into the sample you want to try: -```bash - cd sample-app-name -``` - + + ```bash + cd samples/sample-app-name + ``` + 3. Follow the setup instructions in that sample's `README.md`. -Never commit your API credentials. Each sample reads them from environment variables - copy `.env.example` to `.env` and fill in your own values. +Never commit your API credentials. Each sample reads them from environment variables: copy `.env.example` to `.env` and fill in your own values. ## Resources - + - [Buffer developer docs](https://developers.buffer.com) - [Buffer Help Center](https://support.buffer.com) ## Contributing - -These sample apps are provided as-is for educational purposes. They're maintained on a best-effort basis and aren't covered by Buffer's product support or service-level agreements. -We welcome issues and suggestions for improvements. If you spot a bug or have an idea for a new sample, please [open an issue](../../issues). At the moment we don't accept direct contributions of new samples. +These sample apps are provided as-is for educational purposes. They're maintained on a best-effort basis and aren't covered by Buffer's product support or service-level agreements. + +See [CONTRIBUTING.md](CONTRIBUTING.md) for what we accept and how to contribute. ## License -This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details. +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. diff --git a/app-client-starter-app/CONTRIBUTING.md b/app-client-starter-app/CONTRIBUTING.md deleted file mode 100644 index 8dfeb9d..0000000 --- a/app-client-starter-app/CONTRIBUTING.md +++ /dev/null @@ -1,5 +0,0 @@ -# Contributing - -Keep this example small and copyable. New code should teach one clear Buffer App Client practice, include a test when it changes behavior, and avoid adding frameworks or dependencies unless they clarify the OAuth example. - -Before opening a pull request, run `npm test` and `npm run lint`. Never commit `.env`, certificates, client secrets, sessions, or real tokens. diff --git a/app-client-starter-app/LICENSE b/app-client-starter-app/LICENSE deleted file mode 100644 index 841d138..0000000 --- a/app-client-starter-app/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -MIT License - -Copyright (c) 2026 Draft Queue contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/app-client-starter-app/.env.example b/samples/app-client-starter-app/.env.example similarity index 97% rename from app-client-starter-app/.env.example rename to samples/app-client-starter-app/.env.example index 772a2d3..7cd3ec1 100644 --- a/app-client-starter-app/.env.example +++ b/samples/app-client-starter-app/.env.example @@ -9,5 +9,3 @@ REDIRECT_URI=https://localhost:3000/callback # the app generates one into .session-key on first boot. Required in production: # openssl rand -hex 32 SESSION_SECRET= - -PORT=3000 diff --git a/app-client-starter-app/.gitignore b/samples/app-client-starter-app/.gitignore similarity index 100% rename from app-client-starter-app/.gitignore rename to samples/app-client-starter-app/.gitignore diff --git a/app-client-starter-app/.vercelignore b/samples/app-client-starter-app/.vercelignore similarity index 100% rename from app-client-starter-app/.vercelignore rename to samples/app-client-starter-app/.vercelignore diff --git a/app-client-starter-app/README.md b/samples/app-client-starter-app/README.md similarity index 80% rename from app-client-starter-app/README.md rename to samples/app-client-starter-app/README.md index 028cdba..4411b21 100644 --- a/app-client-starter-app/README.md +++ b/samples/app-client-starter-app/README.md @@ -1,9 +1,9 @@ # Draft Queue: a Buffer app client example -A small, runnable example of a third-party app built on the [Buffer API](https://developers.buffer.com). - +A small, runnable example of a third-party app built on the [Buffer API](https://developers.buffer.com). ## What it does + It's a small starter app called Draft Queue which covers key components of an app client: 1. A user connects their own Buffer account over OAuth 2.0 (Authorization Code + PKCE). @@ -14,7 +14,6 @@ It's a small starter app called Draft Queue which covers key components of an ap Use it as a starting point for your own integration. Clone it, configure it with your own app client, build new features on top, or share it with your AI coding agent as a reference. - ## Prerequisites - Node.js 22 @@ -27,7 +26,7 @@ Use it as a starting point for your own integration. Clone it, configure it with 1. From the root of the repository, move into this sample: ```bash - cd app-client-starter-app + cd samples/app-client-starter-app ``` 2. Install dependencies: @@ -61,11 +60,11 @@ Use it as a starting point for your own integration. Clone it, configure it with 5. Run the app locally: -```bash -npm start -``` + ```bash + npm start + ``` -Open `https://localhost:3000` and accept the self-signed development certificate in your browser. + Open `https://localhost:3000` and accept the self-signed development certificate in your browser. ## Project structure @@ -76,27 +75,21 @@ Open `https://localhost:3000` and accept the self-signed development certificate | `server.js` | Local HTTPS dev server. | | `api/index.js`, `vercel.json` | Serverless entry point and routing, using Vercel as an example host. | | `test/` | Regression tests for the protocol and security-critical helpers. | -| `docs/` | Architecture notes and the production OAuth guide. | +| `docs/` | The demo GIF used in this README. | | `scripts/` | Generates the self-signed certificate for local HTTPS. | ## What this example covers - + - **OAuth 2.0 with PKCE:** the full Authorization Code flow with PKCE. It sends the user to Buffer to authorize the app, handles the redirect back, and exchanges the authorization code for tokens. See `lib/buffer.js`. - **GraphQL requests:** authenticated calls to the Buffer GraphQL API. The app reads the user's organization and channels, then runs a mutation that creates a draft post in a channel's queue. - **Refresh tokens:** the app requests the `offline_access` scope, so Buffer returns a refresh token alongside the access token. When the access token expires, the app renews it without asking the user to sign in again. Both tokens are sealed inside an HttpOnly cookie, so browser JavaScript can never read them. -- **Staying within rate limits:** the app reads the rate limit headers on each response and uses them to stay within the limits applied to the client. -- **Deployment:** a working setup for Vercel (`api/index.js` and `vercel.json`) shows how to run the same app as a serverless deployment. You can adapt it to any Node.js host. - -## Further reading - -- [Buffer developer docs](https://developers.buffer.com) -- [Architecture notes](docs/architecture.md) -- [Production OAuth guide](docs/production-oauth.md) +- **Handling rate limits:** rate limits apply per app client, so every user of the app shares one quota. When Buffer responds with HTTP 429, the app reads the `Retry-After` header. If the wait is 5 seconds or less, it waits and retries the request once. Otherwise it returns the error to the user with the wait time. See `graphql()` in `lib/buffer.js`. +- **Deployment:** a working setup for Vercel (`api/index.js` and `vercel.json`) shows how to run the same app as a serverless deployment. If you deploy from this repository, set the project's root directory to `samples/app-client-starter-app`. You can adapt it to any Node.js host. ## Support and contributing -This sample is provided as-is for educational purposes and isn't covered by Buffer's product support or service-level agreements. If you find a bug or something out of date, please [open an issue](../../../issues). See the [repository README](../README.md#contributing) for how we handle contributions. +This sample is provided as-is for educational purposes and isn't covered by Buffer's product support or service-level agreements. If you find a bug or something out of date, please [open an issue](../../../../issues). See [CONTRIBUTING.md](../../CONTRIBUTING.md) for how we handle contributions. ## License -This sample is licensed under the MIT License. See the [LICENSE](../LICENSE) file at the root of the repository. \ No newline at end of file +This sample is licensed under the MIT License. See the [LICENSE](../../LICENSE) file at the root of the repository. diff --git a/app-client-starter-app/SECURITY.md b/samples/app-client-starter-app/SECURITY.md similarity index 100% rename from app-client-starter-app/SECURITY.md rename to samples/app-client-starter-app/SECURITY.md diff --git a/app-client-starter-app/api/index.js b/samples/app-client-starter-app/api/index.js similarity index 100% rename from app-client-starter-app/api/index.js rename to samples/app-client-starter-app/api/index.js diff --git a/app-client-starter-app/docs/draft-queue-demo.gif b/samples/app-client-starter-app/docs/draft-queue-demo.gif similarity index 100% rename from app-client-starter-app/docs/draft-queue-demo.gif rename to samples/app-client-starter-app/docs/draft-queue-demo.gif diff --git a/app-client-starter-app/lib/app.js b/samples/app-client-starter-app/lib/app.js similarity index 100% rename from app-client-starter-app/lib/app.js rename to samples/app-client-starter-app/lib/app.js diff --git a/app-client-starter-app/lib/buffer.js b/samples/app-client-starter-app/lib/buffer.js similarity index 100% rename from app-client-starter-app/lib/buffer.js rename to samples/app-client-starter-app/lib/buffer.js diff --git a/app-client-starter-app/lib/config.js b/samples/app-client-starter-app/lib/config.js similarity index 98% rename from app-client-starter-app/lib/config.js rename to samples/app-client-starter-app/lib/config.js index 8573e81..c4878dc 100644 --- a/app-client-starter-app/lib/config.js +++ b/samples/app-client-starter-app/lib/config.js @@ -129,7 +129,7 @@ export const API_URL = 'https://api.buffer.com' // Ask for exactly what the app uses: every extra permission on the consent // screen is another reason not to click Approve. `offline_access` lets this // starter renew an expired access token without placing either token in page -// JavaScript. See docs/production-oauth.md before using this pattern at scale. +// JavaScript. export const SCOPES = 'account:read posts:write offline_access' // Someone building against the API needs Buffer's own words to debug; someone diff --git a/app-client-starter-app/lib/session.js b/samples/app-client-starter-app/lib/session.js similarity index 92% rename from app-client-starter-app/lib/session.js rename to samples/app-client-starter-app/lib/session.js index 2d9a7ae..2b5747b 100644 --- a/app-client-starter-app/lib/session.js +++ b/samples/app-client-starter-app/lib/session.js @@ -23,8 +23,7 @@ const options = { // // This is deliberately the simplest viable pattern. It prevents page // JavaScript from reading tokens, but cannot coordinate refreshes from two -// concurrent requests. For a production shared-store design, see -// docs/production-oauth.md. +// concurrent requests. export function readSession(req, res) { return getIronSession(req, res, options) } diff --git a/app-client-starter-app/package-lock.json b/samples/app-client-starter-app/package-lock.json similarity index 99% rename from app-client-starter-app/package-lock.json rename to samples/app-client-starter-app/package-lock.json index 33fbd9a..6ea7a71 100644 --- a/app-client-starter-app/package-lock.json +++ b/samples/app-client-starter-app/package-lock.json @@ -1,11 +1,11 @@ { - "name": "buffer-app", + "name": "app-client-starter-app", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "buffer-app", + "name": "app-client-starter-app", "version": "1.0.0", "dependencies": { "express": "^4.21.2", diff --git a/app-client-starter-app/package.json b/samples/app-client-starter-app/package.json similarity index 94% rename from app-client-starter-app/package.json rename to samples/app-client-starter-app/package.json index 3a6a76c..834270c 100644 --- a/app-client-starter-app/package.json +++ b/samples/app-client-starter-app/package.json @@ -1,5 +1,5 @@ { - "name": "buffer-app", + "name": "app-client-starter-app", "version": "1.0.0", "private": true, "type": "module", diff --git a/app-client-starter-app/public/app.js b/samples/app-client-starter-app/public/app.js similarity index 100% rename from app-client-starter-app/public/app.js rename to samples/app-client-starter-app/public/app.js diff --git a/app-client-starter-app/public/index.html b/samples/app-client-starter-app/public/index.html similarity index 100% rename from app-client-starter-app/public/index.html rename to samples/app-client-starter-app/public/index.html diff --git a/app-client-starter-app/public/logo.svg b/samples/app-client-starter-app/public/logo.svg similarity index 100% rename from app-client-starter-app/public/logo.svg rename to samples/app-client-starter-app/public/logo.svg diff --git a/app-client-starter-app/public/privacy.html b/samples/app-client-starter-app/public/privacy.html similarity index 100% rename from app-client-starter-app/public/privacy.html rename to samples/app-client-starter-app/public/privacy.html diff --git a/app-client-starter-app/public/styles.css b/samples/app-client-starter-app/public/styles.css similarity index 100% rename from app-client-starter-app/public/styles.css rename to samples/app-client-starter-app/public/styles.css diff --git a/app-client-starter-app/scripts/generate-cert.js b/samples/app-client-starter-app/scripts/generate-cert.js similarity index 100% rename from app-client-starter-app/scripts/generate-cert.js rename to samples/app-client-starter-app/scripts/generate-cert.js diff --git a/app-client-starter-app/server.js b/samples/app-client-starter-app/server.js similarity index 100% rename from app-client-starter-app/server.js rename to samples/app-client-starter-app/server.js diff --git a/app-client-starter-app/test/buffer.test.js b/samples/app-client-starter-app/test/buffer.test.js similarity index 95% rename from app-client-starter-app/test/buffer.test.js rename to samples/app-client-starter-app/test/buffer.test.js index 77b85b8..4902d2a 100644 --- a/app-client-starter-app/test/buffer.test.js +++ b/samples/app-client-starter-app/test/buffer.test.js @@ -36,15 +36,15 @@ test('post input is always draft-only and adds service metadata', () => { assert.deepEqual(input.assets, [{ image: { url: 'https://cdn.example.test/photo.jpg' } }]) }) -test('token exchange keeps the refresh token in the sealed HttpOnly cookie session', async () => { +test('token responses map to an access token and a refresh token', async () => { const originalFetch = globalThis.fetch globalThis.fetch = async () => new Response(JSON.stringify({ - access_token: 'access-token', refresh_token: 'do-not-store-me', expires_in: 3600, scope: 'account:read', + access_token: 'access-token', refresh_token: 'refresh-token', expires_in: 3600, scope: 'account:read', }), { status: 200, headers: { 'Content-Type': 'application/json' } }) try { const tokens = await requestTokens({ grant_type: 'authorization_code', code: 'code' }) assert.equal(tokens.accessToken, 'access-token') - assert.equal(tokens.refreshToken, 'do-not-store-me') + assert.equal(tokens.refreshToken, 'refresh-token') } finally { globalThis.fetch = originalFetch } diff --git a/app-client-starter-app/vercel.json b/samples/app-client-starter-app/vercel.json similarity index 100% rename from app-client-starter-app/vercel.json rename to samples/app-client-starter-app/vercel.json