@lunch-money/developer-docs 2.11.2-preview.11 → 2.11.2-preview.12

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.
@@ -17,6 +17,8 @@ For Node.js and TypeScript, [`openid-client`](https://github.com/panva/openid-cl
17
17
 
18
18
  The [confidential Node.js sample application](/oauth/sample-applications#understand-the-confidential-teaching-code) shows this library in a complete Lunch Money flow. Begin with its [`src/oauth` guide](https://github.com/lunch-money/lm-oauth-confidential-node-example/blob/main/src/oauth/README.md), then follow the listed reading order from protocol types and discovery through authorization, callback validation, credential storage, API access, optional refresh, and revocation.
19
19
 
20
+ Lunch Money requires PKCE with the `S256` challenge method for every authorization-code flow, including confidential clients that also authenticate with a client secret.
21
+
20
22
  ## Configure Lunch Money with discovery
21
23
 
22
24
  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:
@@ -65,6 +67,18 @@ Send a form-encoded request to the discovered token endpoint with `grant_type=au
65
67
 
66
68
  Keep the returned credentials in temporary server-side state while you identify the authorization. Do not make an unidentified credential set available to the application or commit it to the normal connection store yet. If a `refresh_token` is returned, treat it as a high-value credential even during this temporary step.
67
69
 
70
+ The token endpoint returns a JSON object with these fields:
71
+
72
+ | Field | Type | When present |
73
+ | --- | --- | --- |
74
+ | `access_token` | string | Always. Treat it as an opaque credential. |
75
+ | `token_type` | string | Always. Lunch Money returns `Bearer`. |
76
+ | `expires_in` | integer | Always. Number of seconds until the access token is expected to expire. |
77
+ | `scope` | string | Always. Space-delimited scopes granted to the token. |
78
+ | `refresh_token` | string | When the registered client includes `offline_access`. |
79
+
80
+ Lunch Money does not return an `id_token`. A successful refresh response has the same shape and includes a replacement `refresh_token`; persist the complete replacement set together.
81
+
68
82
  ## 4. Identify and store the connection
69
83
 
70
84
  Send the access token in the header:
@@ -7,6 +7,8 @@ The goal now is to test that complete workflow—including its failure and recov
7
7
  > [!NOTE] Development access is owner-only
8
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 budget accounts. Other team members cannot authorize the application until the client has been reviewed and approved.
9
9
 
10
+ Owner-only testing is enough to submit a client for review. You do not need beta-tester coverage before submission; test the complete flow and recovery behavior with the owner's Lunch Money user and test budget accounts.
11
+
10
12
  ## Set up a safe test environment
11
13
 
12
14
  Use a separate Lunch Money test budget account when exercising create, update, or delete operations. The [Getting Started guide](/getting-started#create-a-test-budget-account) walks through creating one and adding sample data.
@@ -66,6 +68,6 @@ Your application should discard the unusable token for that connection and start
66
68
 
67
69
  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).
68
70
 
69
- Finally, add a production HTTPS redirect and complete the [review checklist](/oauth/review-and-approval).
71
+ Finally, complete the [review checklist](/oauth/review-and-approval). A confidential web client needs a production HTTPS redirect before review. A native client may be submitted with an eligible native redirect and an accessible pre-release build.
70
72
 
71
73
  Next: [Operate securely](/oauth/security).
@@ -2,6 +2,8 @@
2
2
 
3
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
4
 
5
+ Lunch Money requires PKCE with the `S256` challenge method for every client, public or confidential. Native clients send no client secret.
6
+
5
7
  ## Recommended libraries
6
8
 
7
9
  - iOS and macOS: [`AppAuth-iOS`](https://github.com/openid/AppAuth-iOS), presented through `ASWebAuthenticationSession`.
@@ -13,13 +15,22 @@ Use Lunch Money's authorization-server discovery document and configure `token_e
13
15
 
14
16
  ## Redirects
15
17
 
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. A private-use scheme must contain a dot, such as `app.example.demo:/oauth/callback`, and must be protected against interception.
18
+ Use a redirect mechanism that returns control to your app and that your platform can bind to it. Claimed HTTPS redirects or Android App Links provide stronger app ownership than a custom URI scheme when correctly configured. A private-use scheme must contain a dot, such as `app.example.demo:/oauth/callback`, and must be protected against interception.
19
+
20
+ Lunch Money accepts a well-formed non-loopback HTTPS redirect URI but does not fetch or validate an Apple `apple-app-site-association` file or Android `assetlinks.json` file. Platform association remains your responsibility:
21
+
22
+ - For AppAuth-iOS HTTPS redirects, use iOS 17.4 or later, add an Associated Domains entitlement for the redirect host with the `webcredentials` service, and publish an `apple-app-site-association` file that lists the app under `webcredentials`. An `applinks` association by itself is not sufficient for AppAuth-iOS.
23
+ - For Android App Links, add an HTTPS intent filter with `android:autoVerify="true"` for the redirect host and publish `https://HOST/.well-known/assetlinks.json` with the app package name and signing-certificate fingerprint.
24
+
25
+ Test the installed application with the production-signed association before release. Successful client registration proves only that the redirect URI is syntactically eligible; it does not prove that the operating system will return the authorization response to your app.
17
26
 
18
27
  Native clients may use an HTTP loopback redirect with `localhost`, `127.0.0.1`, or `[::1]`. The scheme, host, path, and query string must exactly match the registered URI, but the port may differ so the application can bind an ephemeral port at runtime. The exception applies only to loopback redirects for native clients; native HTTPS and private-use scheme redirects must match every component of the registered URI exactly. The loopback hosts are not interchangeable: a client registered with `http://127.0.0.1/cb` cannot authorize with `http://localhost:4000/cb` or `http://[::1]:4000/cb`, so register the host your app actually binds to.
19
28
 
20
29
  ## Store and renew tokens
21
30
 
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.
31
+ Store tokens in Keychain on Apple platforms. If an app and its extensions both refresh, place the complete credential set in a Keychain access group they can share. On Android, use current Keystore-backed storage guidance from your maintained library; do not start new work with deprecated `EncryptedSharedPreferences` APIs.
32
+
33
+ Serialize refreshes with a cross-process lock for each connection. After acquiring the lock, re-read the stored credential set because another process may already have refreshed it. Persist the complete replacement set before using the new access token or releasing the lock. If a successful refresh response is lost before it is saved, do not retry with the consumed refresh token: replay revokes the token family, so the user must authorize again.
23
34
 
24
35
  After every code exchange, call and strictly validate `GET /v2/me` before adding the credentials to the app's normal connection store. For an app without its own user accounts, identify each local connection by the returned Lunch Money user `id` and budget-account `account_id` pair. Atomically create a record for a new pair or replace the credential set for an existing pair. Two Lunch Money users may authorize the same shared `account_id`; their credentials must remain in separate records. If identification or persistence fails, discard the temporary credentials instead of saving a partial or unidentified connection. Retain `budget_name` for display, but never use it as a key because different budgets can share a name.
25
36
 
@@ -45,6 +56,34 @@ Sending tokens to an application backend introduces server-side credential stora
45
56
 
46
57
  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.
47
58
 
48
- Lunch Money does not currently provide complete Swift or Kotlin samples; the upstream AppAuth projects provide maintained platform examples. Expo developers can use the [native Expo sample](/oauth/sample-applications#native-expo-sample) for a complete Lunch Money walkthrough and reference implementation.
59
+ ## AppAuth-iOS outline
60
+
61
+ Use AppAuth-iOS to discover Lunch Money's authorization-server metadata from `https://api.lunchmoney.dev`, then create an `OIDAuthorizationRequest` with the discovered configuration, your client ID, no client secret, the registered redirect URI, and the authorization-code response type. AppAuth-iOS presents the request with `ASWebAuthenticationSession`, generates an `S256` PKCE challenge, handles the callback, and exchanges the code when you use its authorization-state convenience flow.
62
+
63
+ The following outline is illustrative; adapt ownership, error handling, and persistence to your application:
64
+
65
+ ```swift
66
+ let issuer = URL(string: "https://api.lunchmoney.dev")!
67
+
68
+ OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in
69
+ guard let configuration else { return handle(error) }
70
+ let request = OIDAuthorizationRequest(
71
+ configuration: configuration,
72
+ clientId: clientID,
73
+ clientSecret: nil,
74
+ scopes: nil,
75
+ redirectURL: redirectURL,
76
+ responseType: OIDResponseTypeCode,
77
+ additionalParameters: nil
78
+ )
79
+ authorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: viewController) { state, error in
80
+ // Temporarily hold state, then validate GET /v2/me before saving it.
81
+ }
82
+ }
83
+ ```
84
+
85
+ After exchange, call `GET /v2/me` with the access token and strictly validate the Lunch Money user and budget-account identity before saving the credential set. For refresh, acquire the connection's cross-process lock, re-read the Keychain access-group record, ask `OIDAuthState` for a fresh access token only if the stored state still needs refresh, save the complete replacement state, and then make the API request. Treat a terminal refresh failure as a request for interactive authorization.
86
+
87
+ This is integration guidance, not a complete Swift sample. The upstream AppAuth-iOS project remains the source for current library APIs and platform setup. Expo developers can use the [native Expo sample](/oauth/sample-applications#native-expo-sample) for a complete Lunch Money walkthrough and reference implementation.
49
88
 
50
89
  Next: [Review OAuth security guidance](/oauth/security).
@@ -30,7 +30,7 @@ The client type cannot be changed after registration. See [OAuth concepts](/oaut
30
30
 
31
31
  A redirect URI tells Lunch Money where to return the user after authorization. Register every callback address your application will use. Confidential web clients must match every component of a registered URI, including its scheme, host, port, path, and query string. Native-client loopback redirects use the same exact matching except that the ephemeral port is ignored.
32
32
 
33
- Web applications normally use an HTTPS callback. If you need a callback for local development, review the [loopback guidance](/oauth/development#local-loopback-callbacks) before registering one. Mobile and desktop applications may use a private-use scheme containing a dot, such as `app.example.demo:/oauth/callback`, or a verified HTTPS link accepted by the Developer Portal.
33
+ Web applications normally use an HTTPS callback. If you need a callback for local development, review the [loopback guidance](/oauth/development#local-loopback-callbacks) before registering one. Mobile and desktop applications may use a private-use scheme containing a dot, such as `app.example.demo:/oauth/callback`, or a non-loopback HTTPS link. Lunch Money checks that the HTTPS URI is well formed but does not verify an Apple or Android association file when you register it. Configure the URI as a claimed HTTPS redirect on the target platform before relying on it; see the [native redirect guidance](/oauth/native-apps#redirects).
34
34
 
35
35
  After the client is approved, you can add or replace redirect URIs in the Developer Portal without submitting another initial review. An approved confidential web client must keep a production HTTPS callback. See [after approval](/oauth/review-and-approval#after-approval).
36
36
 
@@ -32,7 +32,7 @@ Before requesting review, make sure the client has:
32
32
 
33
33
  The privacy policy must be available at a direct, stable public URL and explain how the application collects, uses, stores, shares, and deletes user data. A published privacy-policy page in a public source-code repository can satisfy this requirement. An in-app page that requires installation or sign-in, or a link to source code without a clear privacy policy, does not.
34
34
 
35
- Fully test the client before submission. A confidential web client also needs at least one active, non-expired secret; a native or public client does not.
35
+ Fully test the client before submission. Testing by the client owner is sufficient; submission does not require beta-tester coverage. A confidential web client also needs at least one active, non-expired secret; a native or public client does not.
36
36
 
37
37
  Explain which features use each requested scope, how that access benefits users, and why narrower scopes are insufficient. If the client includes `offline_access`, explain why the integration must operate while the user is absent. You may also provide documentation, source-code, and demonstration URLs or context about changes made after earlier feedback.
38
38
 
@@ -44,7 +44,10 @@ A confidential web client may retain owner-only loopback callbacks alongside its
44
44
 
45
45
  You do not need to create a standalone marketing website for a native application. Its public Apple App Store or Google Play listing can serve as the homepage if it clearly explains what the application does and identifies the developer. A public pre-release listing, project page, or README in a public source-code repository can also work.
46
46
 
47
- If you provide a pre-release listing or beta-testing link, use the review request's additional context to explain how to access it. Include any required opt-in steps, supported platforms or devices, and other information the reviewer needs to evaluate the application. Do not include passwords, client secrets, tokens, or other credentials.
47
+ Lunch Money can approve a native client before its public App Store or Google Play release when the reviewer can access a TestFlight, other beta, or pre-release build. Use the review request's additional context to explain how to access it. Include any required opt-in steps, supported platforms or devices, and other information the reviewer needs to evaluate the application. Do not include passwords, client secrets, tokens, or other credentials.
48
+
49
+ > [!NOTE] Prepare for Apple App Review
50
+ > If the iOS app requires Lunch Money authorization, provide Apple with a dedicated test Lunch Money account in the App Store Connect review information rather than a personal account. Make sure the account and its test budget remain available throughout review. Two-factor authentication can prevent the reviewer from signing in unless you also provide a usable authentication code or another review-ready access path in the review notes.
48
51
 
49
52
  Native clients may be reviewed with loopback or private-use scheme redirects.
50
53
 
@@ -20,7 +20,9 @@ The [confidential Node.js sample](/oauth/sample-applications#try-oauth-with-your
20
20
 
21
21
  Send a form-encoded `grant_type=refresh_token` request to the discovered token endpoint. Confidential clients authenticate with `client_secret_basic`; public clients send their client ID without a secret. A refresh never expands or switches the grant's registered scopes, Lunch Money user, or budget-account identity.
22
22
 
23
- Atomically persist the complete replacement token set before discarding the previous value, and serialize refreshes for each active application-user and Lunch Money user/account connection. Lunch Money rotates refresh tokens; replay of a consumed token can invalidate the token family. Treat transient failures according to a bounded retry policy, but treat `invalid_grant` as terminal: delete unusable credentials for that connection and start a fresh authorization.
23
+ Atomically persist the complete replacement token set before discarding the previous value, and serialize refreshes for each active application-user and Lunch Money user/account connection. When more than one process can refresh the same connection, use a shared credential store and a cross-process lock. After acquiring the lock, re-read the stored refresh token because another process may already have replaced it. Persist the complete replacement set before using the new access token or releasing the lock.
24
+
25
+ Lunch Money rotates refresh tokens on every successful refresh. A consumed token cannot be retried: replay revokes the token family. If the response to a successful refresh is lost before the replacement set is saved, require the user to authorize again rather than retrying the previous token. Treat other transient failures according to a bounded retry policy, but treat `invalid_grant` as terminal: delete unusable credentials for that connection and start a fresh authorization.
24
26
 
25
27
  ## Revoke access
26
28
 
@@ -12,6 +12,12 @@ If you exceed this limit, you will receive a `429 Too Many Requests` response.
12
12
  > [!NOTE] Rate limit scope
13
13
  > Rate limiting is applied per IP address. Requests from the same IP address share the same rate limit quotas.
14
14
 
15
+ ## OAuth token requests
16
+
17
+ Requests to `/oauth/token`, including refresh requests, use a separate per-IP OAuth limit shared with `/oauth/introspect`. The default is 300 requests per window, and the default window is one minute. These requests do not count against the API limit applied to `/v1/` and `/v2/` resource requests.
18
+
19
+ Because this limit is keyed by IP address, unrelated users behind the same office, mobile-carrier, or other network address translation (NAT) gateway can share it. If the token endpoint returns `429 Too Many Requests`, respect `Retry-After` and back off with jitter instead of repeatedly sending the token request.
20
+
15
21
  ## Rate limit response
16
22
 
17
23
  When you exceed a rate limit, the API returns a `429 Too Many Requests` response:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunch-money/developer-docs",
3
- "version": "2.11.2-preview.11",
3
+ "version": "2.11.2-preview.12",
4
4
  "description": "Developer documentation content for Lunch Money APIs",
5
5
  "exports": {
6
6
  ".": "./package.json",
@@ -400,7 +400,7 @@ components:
400
400
  children:
401
401
  type: array
402
402
  nullable: false
403
- description: List of categories to include in the new category group. This field should only be set if `is_group` is also set to true.<br> You may provide existing category objects, existing category IDs, or names for new categories to add to the group. Categories or IDs must already exist and cannot be category groups. Categories that already belong to another group will be moved. If strings are provided, they will be used as names for new categories added to the group. The request will fail if any provided name already exists.<br> You may mix category objects, IDs, and new category names in the same request.
403
+ description: Categories to include in the new category group. Set only when `is_group` is `true`.<br> You may provide existing category objects, existing category IDs, or names for new categories. IDs and objects must refer to existing categories and cannot refer to category groups. Existing categories are added to the new group; categories that already belong to another group are moved. Each string is used as the name of a new category, and the request fails if that name already exists.<br> You may mix category objects, IDs, and new category names in the same request. Adding or moving an existing category updates it, so OAuth clients that include at least one existing category ID or object in `children` must have both `categories:create` and `categories:update`; without both scopes, the request returns a `403` `insufficient_scope` error. If every child is specified by name, `categories:create` alone is sufficient.
404
404
  items:
405
405
  oneOf:
406
406
  - type: integer
@@ -5456,12 +5456,15 @@ paths:
5456
5456
  tags:
5457
5457
  - categories
5458
5458
  summary: Create a new category or category group
5459
- description: Creates a new category with the given name.<br> If the `is_group` attribute is set to true, a category group is created. In this case, the `children` attribute may be set to an array of category IDs to add to the newly created category group.
5459
+ description: Creates a new category with the given name.<br> If `is_group` is `true`, creates a category group; use `children` to add categories to it. OAuth clients that set `children` may also need `categories:update`; see the `children` property for the exact requirement.
5460
5460
  operationId: createCategory
5461
5461
  security:
5462
5462
  - bearerSecurity: []
5463
5463
  - oauth2Security:
5464
5464
  - categories:create
5465
+ - oauth2Security:
5466
+ - categories:create
5467
+ - categories:update
5465
5468
  requestBody:
5466
5469
  required: true
5467
5470
  content:
@@ -5554,6 +5557,8 @@ paths:
5554
5557
  - errMsg: Cannot specify a 'group_id' in request body if 'is_group' is also true
5555
5558
  "401":
5556
5559
  $ref: "#/components/responses/unauthorizedToken"
5560
+ "403":
5561
+ $ref: "#/components/responses/insufficientScope"
5557
5562
  "429":
5558
5563
  $ref: "#/components/responses/rateLimited"
5559
5564
  "500":