@lunch-money/developer-docs 2.11.1-preview.9 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -13
- package/docs/branding-your-app.md +1 -1
- package/manifest.json +0 -101
- package/package.json +3 -3
- package/v2/docs/version-history.md +1 -3
- package/v2/spec/lunch-money-api-v2.yaml +1 -310
- package/docs/oauth/authorization-code.md +0 -80
- package/docs/oauth/concepts.md +0 -46
- package/docs/oauth/development.md +0 -57
- package/docs/oauth/index.md +0 -28
- package/docs/oauth/native-apps.md +0 -26
- package/docs/oauth/oauth-scope-catalog-design.md +0 -370
- package/docs/oauth/register-client.md +0 -51
- package/docs/oauth/review-and-approval.md +0 -57
- package/docs/oauth/scopes.md +0 -104
- package/docs/oauth/security.md +0 -44
- package/docs/oauth/tokens.md +0 -66
- package/docs/oauth/troubleshooting.md +0 -132
- package/v2/images/oauth-authorization-flow.svg +0 -32
- package/v2/images/oauth-token-lifecycle.svg +0 -14
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# Implement the authorization-code flow
|
|
2
|
-
|
|
3
|
-
After you [register an OAuth client](/oauth/register-client), your application uses its settings—including its client ID, client secret, and registered redirect URI—to implement the authorization flow. As shown in the [OAuth overview](/oauth), your application sends the user to Lunch Money, Lunch Money returns an authorization code to your callback, and your application exchanges that code for tokens.
|
|
4
|
-
|
|
5
|
-
This guide walks through that process for a **Confidential web client**. Your server handles the callback and token exchange and keeps the client secret and refresh tokens secure. If you are building a standalone mobile or desktop application, read the [native application guidance](/oauth/native-apps) instead.
|
|
6
|
-
|
|
7
|
-
## Choose an OAuth library
|
|
8
|
-
|
|
9
|
-
OAuth libraries handle much of the protocol work for you, including generating PKCE values, building authorization requests, validating callback state, and exchanging authorization codes. Choose a well-maintained OAuth 2.0 client library for your language and application framework. Look for support for:
|
|
10
|
-
|
|
11
|
-
- the authorization-code flow with Proof Key for Code Exchange (PKCE);
|
|
12
|
-
- OAuth authorization-server metadata discovery;
|
|
13
|
-
- secure `state` generation and validation; and
|
|
14
|
-
- confidential-client authentication with HTTP Basic.
|
|
15
|
-
|
|
16
|
-
For Node.js and TypeScript, [`openid-client`](https://github.com/panva/openid-client) is one maintained option. Despite its name, using it does not mean that Lunch Money supports OpenID Connect identity scopes, ID tokens, or UserInfo.
|
|
17
|
-
|
|
18
|
-
## Configure Lunch Money with discovery
|
|
19
|
-
|
|
20
|
-
Your OAuth library needs to know where to send the user for authorization, where to exchange codes for tokens, and which protocol features Lunch Money supports. Instead of configuring each value separately, give the library the Lunch Money issuer:
|
|
21
|
-
|
|
22
|
-
`https://api.lunchmoney.dev`
|
|
23
|
-
|
|
24
|
-
Then have it load the OAuth authorization-server metadata document:
|
|
25
|
-
|
|
26
|
-
`https://api.lunchmoney.dev/.well-known/oauth-authorization-server`
|
|
27
|
-
|
|
28
|
-
The metadata document is a machine-readable description of Lunch Money's authorization, token, and revocation endpoints, supported client-authentication methods, and PKCE method. Using it allows your library to obtain the current OAuth configuration without your application assembling endpoint URLs itself.
|
|
29
|
-
|
|
30
|
-
> [!TIP]
|
|
31
|
-
> The resolved endpoint paths are `/oauth/authorize`, `/oauth/token`, and `/oauth/revoke`. These are useful when debugging, but use metadata discovery when your library supports it.
|
|
32
|
-
|
|
33
|
-
## 1. Start authorization
|
|
34
|
-
|
|
35
|
-
On your server, generate:
|
|
36
|
-
|
|
37
|
-
- a cryptographically random `state` value bound to the user's session;
|
|
38
|
-
- a PKCE `code_verifier`; and
|
|
39
|
-
- its `S256` `code_challenge`.
|
|
40
|
-
|
|
41
|
-
Redirect the browser to the discovered authorization endpoint with `response_type=code`, your `client_id`, exact `redirect_uri`, `state`, `code_challenge`, and `code_challenge_method=S256`.
|
|
42
|
-
|
|
43
|
-
> [!NOTE] Scopes come from the client registration
|
|
44
|
-
> You do not need to send `scope`. If your OAuth library includes it, Lunch Money ignores the requested value and uses the client's complete registered scope set. An authorization request cannot narrow or expand that set; changing scopes requires a replacement client.
|
|
45
|
-
|
|
46
|
-
## 2. Handle the callback
|
|
47
|
-
|
|
48
|
-
The callback receives either `code` and `state`, or an OAuth `error` and the original `state`. Before exchanging a code:
|
|
49
|
-
|
|
50
|
-
1. compare `state` with the one-time value stored in the initiating session;
|
|
51
|
-
2. reject missing, mismatched, or reused state;
|
|
52
|
-
3. reject unexpected issuer or callback parameters;
|
|
53
|
-
4. consume the stored state and PKCE verifier once; and
|
|
54
|
-
5. remove authorization parameters from the visible browser URL before rendering a page.
|
|
55
|
-
|
|
56
|
-
Treat `access_denied` as a normal user decision. Do not log the callback query string.
|
|
57
|
-
|
|
58
|
-
## 3. Exchange the code
|
|
59
|
-
|
|
60
|
-
Send a form-encoded request to the discovered token endpoint with `grant_type=authorization_code`, the code, exact `redirect_uri`, and original `code_verifier`. A confidential client authenticates with HTTP Basic using its client ID and secret. Do this server-side.
|
|
61
|
-
|
|
62
|
-
Store `access_token`, `token_type`, granted `scope`, and expiration metadata securely. If a `refresh_token` is returned, keep it server-side as a high-value credential.
|
|
63
|
-
|
|
64
|
-
## 4. Call the API
|
|
65
|
-
|
|
66
|
-
Send the access token in the header:
|
|
67
|
-
|
|
68
|
-
```http
|
|
69
|
-
GET /v2/me HTTP/1.1
|
|
70
|
-
Host: api.lunchmoney.dev
|
|
71
|
-
Authorization: Bearer YOUR_ACCESS_TOKEN
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The operation must be allowed by the token's scope and by the user's access to the selected budgeting account. See the [scope catalog](/oauth/scopes).
|
|
75
|
-
|
|
76
|
-
## 5. Continue or recover
|
|
77
|
-
|
|
78
|
-
Use the response's `expires_in` value to schedule renewal slightly before expiry. Read [token lifecycle and recovery](/oauth/tokens) before implementing refresh, logout, or revocation.
|
|
79
|
-
|
|
80
|
-
Next: [Develop and test your OAuth application](/oauth/development).
|
package/docs/oauth/concepts.md
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# OAuth concepts
|
|
2
|
-
|
|
3
|
-
These terms describe who participates in Lunch Money OAuth and what each credential can do.
|
|
4
|
-
|
|
5
|
-
## Parties in the flow
|
|
6
|
-
|
|
7
|
-
- **User (resource owner):** the Lunch Money user who authorizes a third-party application to make API requests on their behalf for one budgeting account.
|
|
8
|
-
- **Application:** the complete product or service the developer builds. It may contain many features unrelated to Lunch Money; only its Lunch Money OAuth client is managed in the Developer Portal.
|
|
9
|
-
- **Client:** the application's OAuth registration with Lunch Money: its `client_id`, type, redirect URIs, selected scopes, and—when applicable—client secret. The developer must register this client in the Developer Portal before the application can ask Lunch Money users to authorize API access.
|
|
10
|
-
- **Authorization server:** the Lunch Money-owned service that presents the Lunch Money sign-in and consent screens, records the user's decision, and issues authorization codes and tokens. The user signs in directly with Lunch Money; the third-party application never receives or sees their Lunch Money login credentials.
|
|
11
|
-
- **Lunch Money API service:** the Lunch Money-owned V2 API, which receives the application's API requests, accepts access tokens, and checks that each request is permitted by the client's scopes.
|
|
12
|
-
|
|
13
|
-
## Client types and credentials
|
|
14
|
-
|
|
15
|
-
When a developer registers a client, they must choose a client type based on whether the application code can keep a client secret secure. This choice cannot be changed after the client is registered. The developer selects either **Confidential web client** or **Native or public client**.
|
|
16
|
-
|
|
17
|
-
- **Confidential web client:** Runs on a server and can securely store client secrets. In OAuth terminology, this is called a *confidential client*. Choose this type for a web application—or any other application architecture with a centralized server. Its client secret helps Lunch Money verify that a token request came from the application that registered the client. The client secret and refresh tokens must stay on the server. The application must protect access tokens wherever its architecture uses them, and associate each authorization with the correct application user and authorized Lunch Money budgeting account.
|
|
18
|
-
- **Native or public client:** Runs on a user-controlled device and uses PKCE without a client secret because installed application code cannot keep a secret secure. In OAuth terminology, this is called a *native public client*. Choose this type for a standalone mobile or desktop application. The app stores each user's tokens securely on that user's device—for example, in Keychain on Apple platforms or appropriate Keystore-backed storage on Android—and must avoid exposing them through backups, logs, or application data shared with other apps.
|
|
19
|
-
|
|
20
|
-
Every client receives a client ID that identifies its Lunch Money registration. The client ID is not secret. Both client types also use Proof Key for Code Exchange (PKCE) to bind the authorization response to the application that started the flow.
|
|
21
|
-
|
|
22
|
-
## Redirect URIs
|
|
23
|
-
|
|
24
|
-
A redirect URI is the callback address where the Lunch Money authorization server returns control to the application after the user approves or denies access. Lunch Money redirects the user's browser to that address with either a temporary authorization code or information about why authorization did not complete.
|
|
25
|
-
|
|
26
|
-
The developer registers each permitted redirect URI in advance. For a confidential web client, the URI sent at the start of authorization must exactly match one of those registered values, including its scheme, host, port, path, and query string. A native client's loopback redirect follows the same rule except that its ephemeral port is ignored. Fragments are not allowed. These checks prevent an authorization result from being redirected to an unexpected destination. The application's callback must also avoid acting as an open redirect to another location.
|
|
27
|
-
|
|
28
|
-
## Scopes and least privilege
|
|
29
|
-
|
|
30
|
-
Resource scopes such as `transactions:read` authorize specific ways of working with Lunch Money data. A scope generally applies to a particular kind of action—such as reading or updating a resource—and only to the relevant API endpoints. Permissions are independent: for example, `transactions:update` does not also grant `transactions:read`.
|
|
31
|
-
|
|
32
|
-
The [V2 API reference](/v2/docs) identifies the scope required by each API endpoint. Use it together with the [scope catalog](/oauth/scopes) to select the permissions your application's features require.
|
|
33
|
-
|
|
34
|
-
Scopes are selected when the client is created and cannot be edited later. Every user who authorizes that client sees and grants all—and only—the scopes the developer selected. To change them, create and test a replacement client, complete review when applicable, update your integration, and ask existing users to authorize again. Existing authorizations and tokens do not move to the replacement client.
|
|
35
|
-
|
|
36
|
-
Select `offline_access` when your application needs to continue making API requests on a user's behalf for longer than the brief period covered by the initial access token. It allows the application to receive a refresh token, which it can use to maintain access without repeatedly asking the user to sign in and authorize the application. It does not add permission to read or change any Lunch Money data; the client still needs the relevant resource scopes.
|
|
37
|
-
|
|
38
|
-
## Codes and tokens
|
|
39
|
-
|
|
40
|
-
- An **authorization code** is the short-lived, single-use value Lunch Money sends to the application's callback after the user approves access. The code cannot make API requests by itself. Every client exchanges it at Lunch Money's token endpoint using its PKCE verifier. A confidential client must also authenticate that request with its client ID and client secret. Lunch Money then returns an access token and, when applicable, a refresh token.
|
|
41
|
-
- An **access token** is a credential the application sends as part of each V2 API request. It is relatively short-lived. The token response includes `expires_in`, the number of seconds for which the access token is expected to remain valid, so the application can record when it will expire and obtain a replacement when needed.
|
|
42
|
-
- A **refresh token** is a longer-lived but non-permanent credential available only to clients that selected `offline_access`. The application sends it to Lunch Money's token endpoint to request a new access token and replacement refresh token without asking the user to authorize again.
|
|
43
|
-
|
|
44
|
-
An application can refresh its access token shortly before it expires or wait until an API response indicates that the access token is no longer valid. If the application does not have a refresh token—or if that token expires, is revoked, or otherwise stops working—it must ask the user to authorize again.
|
|
45
|
-
|
|
46
|
-
Next: [Register an OAuth client](/oauth/register-client).
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# Develop and test an OAuth application
|
|
2
|
-
|
|
3
|
-
Use this guide after you have [registered a client](/oauth/register-client) and implemented the [authorization flow](/oauth/authorization-code). At this stage, your application should be able to send its owner to Lunch Money, receive an authorization code at its callback, exchange the code for tokens, and make an API request.
|
|
4
|
-
|
|
5
|
-
The goal now is to test that complete workflow—including its failure and recovery paths—before asking Lunch Money to approve the application for other users.
|
|
6
|
-
|
|
7
|
-
> [!NOTE] Development access is owner-only
|
|
8
|
-
> While an OAuth client is in development, only the Lunch Money user who owns it can authorize the application. Your team should have the developer doing most of the initial Lunch Money work create and own the client so they can test the complete flow with their budgeting accounts. Other team members cannot authorize the application until the client has been reviewed and approved.
|
|
9
|
-
|
|
10
|
-
## Set up a safe test environment
|
|
11
|
-
|
|
12
|
-
Use a separate Lunch Money test budgeting account when exercising create, update, or delete operations. The [Getting Started guide](/getting-started#create-a-test-budgeting-account) walks through creating one and adding sample data.
|
|
13
|
-
|
|
14
|
-
Keep production client secrets, tokens, and user data out of test fixtures, logs, screenshots, and bug reports.
|
|
15
|
-
|
|
16
|
-
## Local loopback callbacks
|
|
17
|
-
|
|
18
|
-
When you run a confidential web application on your own computer, its callback handler may not have a public HTTPS address yet. A loopback redirect lets the browser return the authorization result directly to the local process you are developing.
|
|
19
|
-
|
|
20
|
-
For a client in development, register an HTTP redirect using a supported loopback host and the port and path where your application listens:
|
|
21
|
-
|
|
22
|
-
- Hostname: `http://localhost:43821/callback`
|
|
23
|
-
- IPv4: `http://127.0.0.1:43821/callback`
|
|
24
|
-
- IPv6: `http://[::1]:43821/callback`
|
|
25
|
-
|
|
26
|
-
You may choose any available port, but the complete redirect URI sent during authorization must match the registered value exactly. `localhost` and `127.0.0.1` are different hosts, so register the form your application sends. Non-loopback HTTP hosts, embedded credentials, and URI fragments are not supported.
|
|
27
|
-
|
|
28
|
-
The user's browser—not Lunch Money's server—connects to the loopback listener. Keep PKCE and `state` protections in place just as you would for a hosted callback. Before requesting review, also register the HTTPS callback used by the deployed application; the loopback URI can remain available for local testing.
|
|
29
|
-
|
|
30
|
-
> [!NOTE] Test an approved client with your development team
|
|
31
|
-
> After a client is approved, other team members can continue developing and testing the application's Lunch Money functionality. If the approved client has a loopback redirect registered, each developer who can run the application locally may authorize access to their own Lunch Money budgeting account. The client ID is public, but developers working on a confidential web client also need access to its client secret through your team's secure credential-management process. Do not share access tokens or refresh tokens between developers.
|
|
32
|
-
|
|
33
|
-
## Repeat or reset authorization
|
|
34
|
-
|
|
35
|
-
During development, the client owner can run authorization again whenever they want to repeat the sign-in, budgeting-account selection, consent, and callback flow. They do not need to revoke the current token first; a new successful authorization replaces their previous active authorization for that client.
|
|
36
|
-
|
|
37
|
-
To test how the application responds after access is revoked, revoke the current token and make another API request with it. Lunch Money should reject the request, and the application should discard its saved credentials and begin authorization again. The [token revocation guide](/oauth/tokens#revoke-access) includes copyable `curl` requests for development testing.
|
|
38
|
-
|
|
39
|
-
## Test the complete client
|
|
40
|
-
|
|
41
|
-
1. Complete authorization. For each attempt, your application should create a random `state` value, save it with the application user's session, and verify that Lunch Money returns the same value to the callback. Rejecting a missing, changed, or reused value prevents a callback started elsewhere from being attached to the wrong session.
|
|
42
|
-
2. Keep the PKCE verifier with that same authorization attempt and use it when exchanging the returned code. Confirm that an exchange with a missing or changed verifier fails; this prevents someone who intercepts the code from using it.
|
|
43
|
-
3. Read the token response's `scope` field and confirm that it contains the complete scope set registered for the client. Do not inspect or parse the access token itself.
|
|
44
|
-
4. Call every scope-dependent feature, including negative permission cases.
|
|
45
|
-
5. Test user denial, missing or changed `state`, callback errors, and interrupted authorization attempts.
|
|
46
|
-
|
|
47
|
-
## Test reauthorization before refresh
|
|
48
|
-
|
|
49
|
-
First make sure your application can recover without a refresh token. Record when the token response arrives and use `expires_in` to determine when the access token is expected to expire. After it expires, make a harmless API request and confirm that Lunch Money returns an authentication failure identifying the token as invalid.
|
|
50
|
-
|
|
51
|
-
Your application should discard the unusable token and start authorization again. Confirm that the same application user can complete the flow, select a budgeting account, and continue using the application with the replacement token. This recovery path is required when a client does not have `offline_access`, and it remains the fallback when a refresh token expires, is revoked, or otherwise stops working.
|
|
52
|
-
|
|
53
|
-
After reauthorization works, clients registered with `offline_access` can add and test refresh-token handling. Verify replacement-token storage, concurrent-refresh protection, revocation, and terminal refresh recovery before relying on unattended operation. See [token lifecycle and recovery](/oauth/tokens).
|
|
54
|
-
|
|
55
|
-
Finally, add a production HTTPS redirect and complete the [review checklist](/oauth/review-and-approval).
|
|
56
|
-
|
|
57
|
-
Next: [Operate securely](/oauth/security).
|
package/docs/oauth/index.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# OAuth for Lunch Money integrations
|
|
2
|
-
|
|
3
|
-
OAuth lets your application access Lunch Money on behalf of a user without asking that person to copy and share a personal access token. **If you are building an application for anyone other than yourself, use OAuth.** It gives each user a familiar Lunch Money sign-in and consent experience while keeping their personal access token out of your application.
|
|
4
|
-
|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
## Choose OAuth or a personal access token
|
|
8
|
-
|
|
9
|
-
| Question | Personal access token | OAuth application and client |
|
|
10
|
-
| --- | --- | --- |
|
|
11
|
-
| Whose data can it access? | One budgeting account belonging to the user who created the token | One budgeting account selected by each user who authorizes the application |
|
|
12
|
-
| Best fit | API exploration, one-off scripts, and short-lived work on your own account | Persistent integrations and applications intended for other users |
|
|
13
|
-
| How is access granted? | The user creates a token on the [Developers page](https://my.lunchmoney.app/developers) in the Lunch Money web app | The user signs in and approves the permissions requested by the application |
|
|
14
|
-
| Credential handling | The user keeps the token and sends it with their own API requests; third-party applications should use OAuth instead of asking users to share one | The authorization flow delivers application-specific tokens without asking for a personal token |
|
|
15
|
-
| Permissions | Full access to the Lunch Money APIs available for that budgeting account, with no fine-grained permission controls | Only the permissions selected when the application was created |
|
|
16
|
-
| Lifecycle | Created and revoked by the user | Short-lived access tokens and optional finite refresh access; the user can revoke the application's access |
|
|
17
|
-
|
|
18
|
-
Personal access tokens remain useful for quick work on your own account. OAuth is the recommended model for applications and integrations that other Lunch Money users will connect to, as well as automations that need to run over time.
|
|
19
|
-
|
|
20
|
-
## What limits access
|
|
21
|
-
|
|
22
|
-
When a developer creates an OAuth application, they select the scopes—the specific permissions—the application needs. Later, when a Lunch Money user authorizes the application, Lunch Money shows them that complete permission set before they approve access. Choose only the permissions the application's features require. A focused permission request is easier for users to understand and trust, and reduces the risk of the application reading or changing data it does not need.
|
|
23
|
-
|
|
24
|
-
## Before you start
|
|
25
|
-
|
|
26
|
-
You need an active Lunch Money account to [register and manage an OAuth application](/oauth/applications). During development, only the application owner can authorize it. Approval is required before other Lunch Money users can authorize it.
|
|
27
|
-
|
|
28
|
-
Start with [OAuth concepts](/oauth/concepts), [register a client](/oauth/register-client), and then [implement the authorization-code flow](/oauth/authorization-code). Use the [scope catalog](/oauth/scopes) to plan permissions and [troubleshooting](/oauth/troubleshooting) when a flow fails.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# OAuth for native iOS and Android apps
|
|
2
|
-
|
|
3
|
-
Native apps are public clients: they cannot keep a client secret. Use authorization code with PKCE and launch Lunch Money sign-in and consent in a trusted platform authentication browser—not an embedded WebView.
|
|
4
|
-
|
|
5
|
-
## Recommended libraries
|
|
6
|
-
|
|
7
|
-
- iOS and macOS: [`AppAuth-iOS`](https://github.com/openid/AppAuth-iOS), presented through `ASWebAuthenticationSession`.
|
|
8
|
-
- Android: [`AppAuth-Android`](https://github.com/openid/AppAuth-Android), which uses a browser Custom Tab.
|
|
9
|
-
- Bare or non-Expo React Native: [`react-native-app-auth`](https://github.com/FormidableLabs/react-native-app-auth).
|
|
10
|
-
- Expo: Use [`expo-auth-session`](https://docs.expo.dev/versions/latest/sdk/auth-session/) for browser-based OAuth flows.
|
|
11
|
-
|
|
12
|
-
Use Lunch Money's authorization-server discovery document and configure `token_endpoint_auth_method=none`. Send the client ID at the token and revocation endpoints, but never invent or embed a client secret. You do not need to configure authorization-request scopes because the registered client receives its complete fixed set. If your OAuth library sends `scope`, Lunch Money ignores that value and uses the registered set.
|
|
13
|
-
|
|
14
|
-
## Redirects
|
|
15
|
-
|
|
16
|
-
Use a redirect mechanism that returns control to your app and that your platform can bind to it. Claimed Universal Links or Android App Links provide stronger app ownership than a custom URI scheme when correctly configured. Custom schemes must be distinctive and protected against interception.
|
|
17
|
-
|
|
18
|
-
The loopback callbacks supported for confidential web development do not define native or desktop redirect support. Follow the native redirect choices accepted during client registration; do not assume the confidential web-client loopback rules apply.
|
|
19
|
-
|
|
20
|
-
## Store and renew tokens
|
|
21
|
-
|
|
22
|
-
Store tokens in Keychain on Apple platforms. On Android, use current Keystore-backed storage guidance from your maintained library; do not start new work with deprecated `EncryptedSharedPreferences` APIs. Coordinate refresh-token use, persist replacements, and fall back to interactive authorization after terminal failure.
|
|
23
|
-
|
|
24
|
-
Platform code should delegate protocol validation to the maintained library while your application supplies safe configuration, lifecycle storage, API calls, revocation, and user-facing recovery. Full Lunch Money Swift and Kotlin sample apps are not part of this documentation phase; the upstream AppAuth projects provide maintained platform examples.
|
|
25
|
-
|
|
26
|
-
Next: [Review OAuth security guidance](/oauth/security).
|
|
@@ -1,370 +0,0 @@
|
|
|
1
|
-
# OAuth scope catalog design proposal
|
|
2
|
-
|
|
3
|
-
Status: first-cut ENG-616 implementation and inventory, pending consumer review.
|
|
4
|
-
|
|
5
|
-
This document proposes a published, shared source of truth for OAuth scope
|
|
6
|
-
definitions and scope-group presets published with the canonical V2 OpenAPI
|
|
7
|
-
specification. It must still be reconciled with the server OAuth management API
|
|
8
|
-
and database implementation before the catalog is finalized.
|
|
9
|
-
|
|
10
|
-
## Context and decisions
|
|
11
|
-
|
|
12
|
-
The catalog is application code, not persistence. OAuth clients, grants, and
|
|
13
|
-
tokens store scope-name strings. Server, Developer Portal, and authorization UI
|
|
14
|
-
reconcile those strings with their installed catalog version.
|
|
15
|
-
|
|
16
|
-
The detailed OAuth technical specification currently contains an exploratory
|
|
17
|
-
read/write scope list and says write implies read. Those points are superseded
|
|
18
|
-
for this proposal: the final inventory will use independent `:read`, `:create`,
|
|
19
|
-
`:update`, and `:delete` authorities where CRUD applies, and no authority implies
|
|
20
|
-
another. Presets express common combinations without changing authorization
|
|
21
|
-
semantics. Names such as `resource:read` in this document are generic examples,
|
|
22
|
-
not production decisions.
|
|
23
|
-
|
|
24
|
-
This proposal makes these additional decisions:
|
|
25
|
-
|
|
26
|
-
- Keep `OAuthScopeCategory` as `identity | offline | api`. The category
|
|
27
|
-
distinguishes protocol scopes from API permissions. The published inventory
|
|
28
|
-
includes `offline_access` for requesting continued access through refresh
|
|
29
|
-
tokens, but does not publish OpenID Connect identity scopes (`openid`,
|
|
30
|
-
`email`, or `profile`).
|
|
31
|
-
- Add optional `lifecycleNotice` copy for deprecation and retirement warnings.
|
|
32
|
-
It is display metadata, not runtime authorization policy.
|
|
33
|
-
- Reject groups with identical member sets. This is simpler than display
|
|
34
|
-
priorities and makes exact matching unambiguous.
|
|
35
|
-
- Allow unmapped public operations during rollout, but require callers to make
|
|
36
|
-
the choice explicit through an option such as
|
|
37
|
-
`operationIdCoverage: 'allow-unmapped'`. Validation should switch to
|
|
38
|
-
`require-all-mapped` before OAuth scope enforcement is declared complete.
|
|
39
|
-
- Do not encode scope implication, expansion, grant rules, client approval,
|
|
40
|
-
refresh-token policy, or consent policy in the catalog.
|
|
41
|
-
- Use catalog `operationIds` as the authority for V2 endpoint permissions.
|
|
42
|
-
Server decorators must be generated from or validated against this mapping so
|
|
43
|
-
they cannot become a second source of truth.
|
|
44
|
-
- Scope V1 of this catalog to the V2 API only.
|
|
45
|
-
- Map non-CRUD mutations by effect: Plaid and synced-crypto refresh operations
|
|
46
|
-
use `:update`; transaction split/group and their reversals use
|
|
47
|
-
`transactions:update`; balance-history and budget upserts use `:update`.
|
|
48
|
-
- Let `transactions:read` expose attachment IDs and metadata, while requiring
|
|
49
|
-
explicit `transaction_attachments:*` scopes to download, attach, or delete
|
|
50
|
-
files.
|
|
51
|
-
|
|
52
|
-
## Package direction
|
|
53
|
-
|
|
54
|
-
The catalog ships as a browser-safe subpath of the existing
|
|
55
|
-
`@lunch-money/v2-api-spec` package so the catalog and V2 `operationId` values are
|
|
56
|
-
versioned atomically:
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
src/oauth-scopes/ authored TypeScript catalog and helpers
|
|
60
|
-
oauth-scopes/ compiled published subpath
|
|
61
|
-
test/oauth-scopes/ catalog and V2 operationId validation
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
The runtime entry point has no Node-only dependencies and performs no filesystem
|
|
65
|
-
or YAML work. OpenAPI parsing remains in build/test tooling and is not exported
|
|
66
|
-
from the browser-safe `@lunch-money/v2-api-spec/oauth-scopes` subpath.
|
|
67
|
-
|
|
68
|
-
## Public contract
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
type OAuthScopeName = string
|
|
72
|
-
type OAuthScopeCategory = 'identity' | 'offline' | 'api'
|
|
73
|
-
type OAuthScopeLifecycle = 'active' | 'deprecated' | 'retired'
|
|
74
|
-
type KnownOAuthScopeName = 'me:read' | /* ... */ 'budgets:delete'
|
|
75
|
-
|
|
76
|
-
interface OAuthScopeDefinition {
|
|
77
|
-
readonly name: KnownOAuthScopeName
|
|
78
|
-
readonly description: string
|
|
79
|
-
readonly category: OAuthScopeCategory
|
|
80
|
-
readonly operationIds: readonly string[]
|
|
81
|
-
readonly status: OAuthScopeLifecycle
|
|
82
|
-
readonly lifecycleNotice?: string
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface OAuthScopeGroupDefinition {
|
|
86
|
-
readonly id: string
|
|
87
|
-
readonly name: string
|
|
88
|
-
readonly description: string
|
|
89
|
-
readonly scopes: readonly KnownOAuthScopeName[]
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface OAuthCatalogDefinition {
|
|
93
|
-
readonly scopes: readonly OAuthScopeDefinition[]
|
|
94
|
-
readonly groups: readonly OAuthScopeGroupDefinition[]
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
The `oauth-scopes` subpath exports:
|
|
99
|
-
|
|
100
|
-
- The interfaces and aliases above, plus result and issue types.
|
|
101
|
-
- `validateOAuthCatalog(definition, options)` returns all structured issues.
|
|
102
|
-
- `getOAuthScopeDefinition(name)` returns a definition or `undefined`.
|
|
103
|
-
- `getOAuthScopeForOperationId(operationId)` returns the authoritative scope
|
|
104
|
-
definition for a V2 operation or `undefined`.
|
|
105
|
-
- `getOAuthScopeGroup(id)` returns a group or `undefined`.
|
|
106
|
-
- `matchOAuthScopeGroup(names)` returns a discriminated matched/no-match
|
|
107
|
-
result.
|
|
108
|
-
- Immutable `oauthCatalog`, `oauthScopes`, and `oauthScopeGroups` exports.
|
|
109
|
-
|
|
110
|
-
`OAuthScopeName` remains `string` for persisted and API-facing input so consumers
|
|
111
|
-
can preserve names newer than their installed library. `KnownOAuthScopeName`
|
|
112
|
-
provides autocomplete and compile-time checking for this catalog version.
|
|
113
|
-
|
|
114
|
-
## Runtime validation and immutability
|
|
115
|
-
|
|
116
|
-
The published definitions and nested arrays are frozen. Private lookup maps
|
|
117
|
-
support explicit failed lookups, and `validateOAuthCatalog` returns structured
|
|
118
|
-
issues for build/test tooling without importing Node-only code into the runtime.
|
|
119
|
-
|
|
120
|
-
Validation checks:
|
|
121
|
-
|
|
122
|
-
- stable lowercase colon-delimited scope names and lowercase underscore group IDs;
|
|
123
|
-
- required descriptions and valid category/lifecycle values;
|
|
124
|
-
- unique scope names and group IDs;
|
|
125
|
-
- non-empty API `operationIds`, except retired tombstones;
|
|
126
|
-
- no operation IDs on identity or offline scopes;
|
|
127
|
-
- duplicate operation IDs within a scope;
|
|
128
|
-
- conflicting operation ownership unless an ID is deliberately allowlisted;
|
|
129
|
-
- non-empty groups with unique members;
|
|
130
|
-
- group members exist and are not retired;
|
|
131
|
-
- offline scopes are excluded from groups;
|
|
132
|
-
- duplicate group member sets are rejected.
|
|
133
|
-
|
|
134
|
-
The runtime has no mutable singleton. Consumers import the frozen catalog or
|
|
135
|
-
construct catalog fixtures in tests. Public helpers return
|
|
136
|
-
`undefined` or discriminated results rather than using non-null assertions.
|
|
137
|
-
|
|
138
|
-
## Protocol scopes
|
|
139
|
-
|
|
140
|
-
`OAuthScopeCategory` includes `identity` and `offline` so validation can
|
|
141
|
-
distinguish protocol scopes from API permissions. The published catalog ships
|
|
142
|
-
`offline_access` alongside its API scopes.
|
|
143
|
-
|
|
144
|
-
Lunch Money's authorization server exposes OAuth 2.1 authorization-server
|
|
145
|
-
metadata, API scopes, and the `offline_access` protocol scope:
|
|
146
|
-
|
|
147
|
-
- no `openid`, `email`, or `profile` identity scopes;
|
|
148
|
-
- active `offline_access` with no V2 operation IDs;
|
|
149
|
-
- no ID tokens, UserInfo, or public signing keys.
|
|
150
|
-
|
|
151
|
-
`offline_access` represents a request for continued access when the user is not
|
|
152
|
-
actively using the app. The authorization server still owns the decision to
|
|
153
|
-
issue a refresh token based on the requested scope, client capability, consent,
|
|
154
|
-
and server policy.
|
|
155
|
-
|
|
156
|
-
No resource group may contain an offline-category scope. Identity scopes are
|
|
157
|
-
not categorically banned from a group; any future identity-oriented preset
|
|
158
|
-
should be an explicit catalog decision.
|
|
159
|
-
|
|
160
|
-
## Lifecycle and consumer behavior
|
|
161
|
-
|
|
162
|
-
Lifecycle is code-only metadata:
|
|
163
|
-
|
|
164
|
-
| Status | New assignment/request | Existing full replacement | Token issuance | Recognition/display |
|
|
165
|
-
| --- | --- | --- | --- | --- |
|
|
166
|
-
| `active` | allowed | allowed | allowed | yes |
|
|
167
|
-
| `deprecated` | rejected if newly introduced | preserved if already assigned | allowed for existing grants per server rollout | yes, with warning |
|
|
168
|
-
| `retired` | rejected | rejected | rejected | yes, as tombstone |
|
|
169
|
-
|
|
170
|
-
The library provides state and copy; consumers enforce transitions in context.
|
|
171
|
-
For example, only the server can know whether a deprecated name is newly added
|
|
172
|
-
or preserved during a full replacement. Lifecycle must never be copied into a
|
|
173
|
-
client row. A stored name absent from the installed catalog is `unknown`, not
|
|
174
|
-
implicitly retired.
|
|
175
|
-
|
|
176
|
-
Recommended reconciliation status for management APIs:
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
type ScopeConfigurationStatus = 'valid' | 'needs_attention'
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
`needs_attention` applies when stored configuration contains an unknown or
|
|
183
|
-
retired scope. A deprecated-only configuration remains valid but returns warning
|
|
184
|
-
metadata. The exact API response shape is a server-design coordination point.
|
|
185
|
-
|
|
186
|
-
## Compatibility baseline and change classes
|
|
187
|
-
|
|
188
|
-
After the first production catalog is approved, check in a canonical JSON
|
|
189
|
-
snapshot at `compatibility/catalog-baseline.json`. Future CI compares source
|
|
190
|
-
against this file and does not fetch npm. The baseline updates only as part of a
|
|
191
|
-
reviewed release and must represent the most recently published authority map.
|
|
192
|
-
Canonical ordering makes diffs stable.
|
|
193
|
-
|
|
194
|
-
Compatibility checks detect deletion, category changes/name reuse, authority
|
|
195
|
-
broadening, operation remapping, lifecycle changes, and group membership/removal.
|
|
196
|
-
The regular catalog validator separately detects broken references and duplicate
|
|
197
|
-
ownership.
|
|
198
|
-
|
|
199
|
-
Change classification:
|
|
200
|
-
|
|
201
|
-
- Patch-compatible: descriptions, names shown to users, and lifecycle notice
|
|
202
|
-
copy change without changing authority.
|
|
203
|
-
- Minor-compatible: a new scope or group is added. New scopes must not be added
|
|
204
|
-
to an existing group in the same classification; that is a membership change.
|
|
205
|
-
- Migration-requiring: deprecation, retirement, removal of authority from a
|
|
206
|
-
scope, any group membership change, or group removal.
|
|
207
|
-
- Breaking/forbidden without explicit migration: deleting a known scope rather
|
|
208
|
-
than retaining a tombstone, changing its category, reusing its name for a
|
|
209
|
-
different authority, or adding operation IDs to an existing scope.
|
|
210
|
-
|
|
211
|
-
Name reuse cannot be detected from names alone. Preserving every old definition
|
|
212
|
-
as a retired tombstone plus comparing category and operation ownership provides
|
|
213
|
-
the deterministic safeguard. Review policy must treat meaning changes not
|
|
214
|
-
visible in structured data as forbidden. A future `authorityFingerprint` could
|
|
215
|
-
make that stronger if V1 endpoints or non-operation authorities join the model.
|
|
216
|
-
|
|
217
|
-
A future comparison helper should treat a retired tombstone with removed
|
|
218
|
-
operation IDs as migration-requiring rather than breaking. Tombstones should
|
|
219
|
-
remain small while retaining enough description to diagnose old persisted names.
|
|
220
|
-
|
|
221
|
-
## Exact-set group matching
|
|
222
|
-
|
|
223
|
-
Groups are UI presets only. A selected group becomes its concrete member names;
|
|
224
|
-
the group ID is neither sent as authority nor persisted in client, grant, code,
|
|
225
|
-
or token configuration.
|
|
226
|
-
|
|
227
|
-
Matching performs these steps:
|
|
228
|
-
|
|
229
|
-
1. Reject a candidate containing an unknown or duplicate scope name with a
|
|
230
|
-
distinct no-match reason.
|
|
231
|
-
2. Canonically sort the complete candidate set.
|
|
232
|
-
3. Compare it with each complete canonical group member set.
|
|
233
|
-
4. Return exactly one matched group or `no_match`.
|
|
234
|
-
|
|
235
|
-
An additional recognized scope prevents a match. The helper never searches for
|
|
236
|
-
subsets, decomposes a set into overlapping groups, or infers historical preset
|
|
237
|
-
selection. A changed preset causes old clients to fall back to individual scope
|
|
238
|
-
display. Duplicate member sets are invalid, so ambiguity cannot enter a valid
|
|
239
|
-
catalog.
|
|
240
|
-
|
|
241
|
-
Product analytics may separately record that a preset was selected, but that
|
|
242
|
-
event is historical telemetry and has no authorization meaning.
|
|
243
|
-
|
|
244
|
-
## OpenAPI operationId validation
|
|
245
|
-
|
|
246
|
-
The V2 YAML is authoritative for public operation IDs. Node-only test tooling
|
|
247
|
-
parses `v2/spec/lunch-money-api-v2.yaml`, traverses HTTP operations, and returns a set
|
|
248
|
-
while rejecting missing or duplicate IDs. Catalog validation then:
|
|
249
|
-
|
|
250
|
-
- verifies every API-category operation ID exists in the spec;
|
|
251
|
-
- rejects conflicting ownership unless a deliberate `sharedOperationIds`
|
|
252
|
-
allowlist contains the ID;
|
|
253
|
-
- permits identity/offline scopes to have no operation IDs;
|
|
254
|
-
- reports stale and unknown IDs with catalog paths;
|
|
255
|
-
- optionally reports every unmapped public operation.
|
|
256
|
-
|
|
257
|
-
The validation modes are:
|
|
258
|
-
|
|
259
|
-
- `allow-unmapped`: rollout mode; catalog entries must be correct, but endpoints
|
|
260
|
-
can remain unmapped while enforcement is staged.
|
|
261
|
-
- `require-all-mapped`: completion mode; every public operation must have an
|
|
262
|
-
owner or be removed from the public spec.
|
|
263
|
-
|
|
264
|
-
If some public operations are intentionally unscoped (for example a protocol or
|
|
265
|
-
health endpoint represented in the same document), add a named exclusion list
|
|
266
|
-
with a justification per entry rather than silently omitting them. Shared
|
|
267
|
-
operation ownership should likewise be rare and explicit. The tooling never
|
|
268
|
-
modifies the OpenAPI document.
|
|
269
|
-
|
|
270
|
-
V1 endpoints are deliberately outside this catalog. Do not overload V2
|
|
271
|
-
operation IDs with invented V1 values.
|
|
272
|
-
|
|
273
|
-
## Consumer integration
|
|
274
|
-
|
|
275
|
-
### Server
|
|
276
|
-
|
|
277
|
-
- Pin an exact catalog package version in deploys.
|
|
278
|
-
- Persist only scope-name strings.
|
|
279
|
-
- Validate requested/client/grant scopes against the installed catalog.
|
|
280
|
-
- Reject newly introduced deprecated scopes and all retired/unknown scopes.
|
|
281
|
-
- Preserve deprecated scopes on an existing client's full replacement only
|
|
282
|
-
when they were already present.
|
|
283
|
-
- Enforce operation authority from the catalog without implication.
|
|
284
|
-
- Treat `offline_access` as a request for refresh-token-backed access while
|
|
285
|
-
retaining the server-owned issuance decision based on client capability and
|
|
286
|
-
consent policy.
|
|
287
|
-
- Return unknown names so operators and UIs can diagnose drift.
|
|
288
|
-
|
|
289
|
-
### Developer Portal
|
|
290
|
-
|
|
291
|
-
- Render active definitions and groups from the package.
|
|
292
|
-
- Render deprecated existing selections with `lifecycleNotice`; do not offer
|
|
293
|
-
them for new selection.
|
|
294
|
-
- Render retired/unknown stored names as needing attention.
|
|
295
|
-
- Submit concrete scope names, never group IDs.
|
|
296
|
-
- Use exact matching only as a display convenience.
|
|
297
|
-
|
|
298
|
-
### Authorization/consent UI
|
|
299
|
-
|
|
300
|
-
- Resolve the server-approved requested names using the same pinned catalog
|
|
301
|
-
compatibility range.
|
|
302
|
-
- Display groups only on exact match; otherwise display individual definitions.
|
|
303
|
-
- Call out API mutation scopes from structured category and scope data, while
|
|
304
|
-
keeping the actual consent/issuance decision server-owned.
|
|
305
|
-
- Fail closed on unknown or retired requested names.
|
|
306
|
-
|
|
307
|
-
Consumers must not independently copy definitions or group membership. During a
|
|
308
|
-
rolling deploy, the server is authoritative: it must not send a scope newly
|
|
309
|
-
introduced by a catalog version that the UI deployment cannot understand. Pinning
|
|
310
|
-
one exact version across a coordinated release is preferred.
|
|
311
|
-
|
|
312
|
-
## Versioning and publishing
|
|
313
|
-
|
|
314
|
-
Publish the catalog with `@lunch-money/v2-api-spec`. A spec-package version pins
|
|
315
|
-
both the YAML and its catalog, preventing operation mappings from drifting.
|
|
316
|
-
Use the change classes above when selecting the package version:
|
|
317
|
-
|
|
318
|
-
- patch for copy-only and implementation fixes;
|
|
319
|
-
- minor for additive catalog entries or backward-compatible API additions;
|
|
320
|
-
- major only with an approved migration for a public contract break;
|
|
321
|
-
- migration-requiring catalog changes may still use minor versions before 1.0,
|
|
322
|
-
but release notes and server coordination are mandatory.
|
|
323
|
-
|
|
324
|
-
Start with prereleases until all three consumers validate the contract. Existing
|
|
325
|
-
spec-package release tooling builds the subpath and verifies package contents
|
|
326
|
-
before publishing.
|
|
327
|
-
|
|
328
|
-
Each server release should pin an exact package version. Portal and consent UI
|
|
329
|
-
should use the same version or a declared compatibility window validated in CI.
|
|
330
|
-
Never depend on an npm `latest` lookup at application startup.
|
|
331
|
-
|
|
332
|
-
## Reconciliation required with the server design
|
|
333
|
-
|
|
334
|
-
Before this proposal becomes final, compare these exact artifacts:
|
|
335
|
-
|
|
336
|
-
1. Scope string grammar, especially resource separators and full CRUD verbs.
|
|
337
|
-
2. `identity | offline | api` categories and `active | deprecated | retired`
|
|
338
|
-
lifecycle values.
|
|
339
|
-
3. Group schema and the decision to reject duplicate member sets.
|
|
340
|
-
4. The discriminated exact-match result and unknown-scope handling.
|
|
341
|
-
5. `scope_configuration_status` semantics, warning/error details, and whether
|
|
342
|
-
deprecated-only configuration remains `valid`.
|
|
343
|
-
6. Management API request/response shapes: concrete scope names only, with no
|
|
344
|
-
persisted group authority.
|
|
345
|
-
7. Full-replacement rules for preserving deprecated scopes.
|
|
346
|
-
8. `offline_access` request and refresh-token issuance semantics, and
|
|
347
|
-
confirmation that OIDC identity scopes are rejected.
|
|
348
|
-
9. How server `tsoa` operation requirements consume catalog mappings without
|
|
349
|
-
creating a second source of truth.
|
|
350
|
-
10. Exact package versions supported by server, Portal, and consent deployments,
|
|
351
|
-
including rolling-deploy behavior.
|
|
352
|
-
11. The server database's representation of unknown names and its mapping to
|
|
353
|
-
`valid | needs_attention`.
|
|
354
|
-
|
|
355
|
-
The server OpenAPI management schemas should remain string-based so adding a
|
|
356
|
-
catalog name does not require an API schema revision.
|
|
357
|
-
|
|
358
|
-
## Recommended ENG-616 follow-up
|
|
359
|
-
|
|
360
|
-
1. Reconcile this proposal with the server agent's OpenAPI and database artifacts.
|
|
361
|
-
2. Confirm the V2 operation coverage/exclusion policy.
|
|
362
|
-
3. Review and approve the first-cut full-CRUD inventory and descriptions; do not
|
|
363
|
-
derive authority from the older read/write draft.
|
|
364
|
-
4. Check in the first canonical compatibility baseline with that inventory.
|
|
365
|
-
5. Add CI that builds/tests the spec package and validates the production catalog
|
|
366
|
-
against the local V2 YAML in the chosen coverage mode.
|
|
367
|
-
6. Select a prerelease version, verify the tarball, and test it through local
|
|
368
|
-
`file:` or packed dependencies in all consumers.
|
|
369
|
-
7. Publish only after server, Portal, and consent UI agree on the contract and
|
|
370
|
-
rolling-version policy.
|