diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f8565b8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Reviewers requested automatically on every pull request. +* @JustinaPetr diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a18e4da --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + # Each sample app needs its own entry, pointing at the folder that holds its package.json. + - package-ecosystem: npm + directory: /samples/app-client-starter-app + schedule: + interval: weekly + groups: + minor-and-patch: + update-types: [minor, patch] + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..c664167 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## What does this change? + + + +## How was it tested? + + + +## Checklist + +- [ ] `npm test` and `npm run lint` pass for any app I changed +- [ ] No secrets, tokens or `.env` files are committed +- [ ] README updated if setup or behavior changed 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 66% rename from app-client-starter-app/.env.example rename to samples/app-client-starter-app/.env.example index 772a2d3..e337ac4 100644 --- a/app-client-starter-app/.env.example +++ b/samples/app-client-starter-app/.env.example @@ -2,12 +2,13 @@ BUFFER_CLIENT_ID=your_client_id BUFFER_CLIENT_SECRET=your_client_secret -# Must match what you registered, character for character +# Can be changed to your own REDIRECT_URI. Must match what you registered, character for character REDIRECT_URI=https://localhost:3000/callback +#PORT your app is running on locally. Can be changed to match the details in your custom REDIRECT_URI. +PORT=3000 + # Encrypts the session cookie; 32 characters minimum. Leave it empty locally and # 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 74% rename from app-client-starter-app/README.md rename to samples/app-client-starter-app/README.md index a22b1bd..de455ba 100644 --- a/app-client-starter-app/README.md +++ b/samples/app-client-starter-app/README.md @@ -1,17 +1,18 @@ # 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). 2. The app lists the channels in their Buffer organization. 3. The user picks a channel, and the app creates a **draft** post in that channel's queue. -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. + ![Draft Queue demo: connecting a Buffer account, picking a channel, and adding a draft post to its queue](docs/draft-queue-demo.gif) +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 @@ -25,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: @@ -48,8 +49,9 @@ Use it as a starting point for your own integration. Clone it, configure it with | `BUFFER_CLIENT_SECRET` | Required for a confidential client. Never expose it to the browser or commit it. | | `SESSION_SECRET` | At least 32 characters. Generate one with `openssl rand -hex 32`. Optional locally, required in production. | | `REDIRECT_URI` | It must exactly match the redirect URI registered for your app client. | + | `PORT` | Port your app is running on locally. Should match the port specified in the `REDIRECT_URI`. -4. Generate a self-signed certificate for local HTTPS: +5. Generate a self-signed certificate for local HTTPS: ```bash npm run certs @@ -57,13 +59,13 @@ Use it as a starting point for your own integration. Clone it, configure it with Buffer doesn't accept `http://localhost` redirect URIs, so the dev server runs over HTTPS. If you skip this step, `npm start` generates the certificate on first run. -5. Run the app locally: +6. 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 @@ -74,27 +76,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/samples/app-client-starter-app/docs/draft-queue-demo.gif b/samples/app-client-starter-app/docs/draft-queue-demo.gif new file mode 100644 index 0000000..c7e7421 Binary files /dev/null and b/samples/app-client-starter-app/docs/draft-queue-demo.gif differ 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