@lunch-money/v2-api-spec 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 CHANGED
@@ -14,7 +14,6 @@ After installation, the package provides:
14
14
 
15
15
  - **OpenAPI Specification**: `node_modules/@lunch-money/v2-api-spec/lunch-money-api-v2.yaml`
16
16
  - **Version History**: `node_modules/@lunch-money/v2-api-spec/version-history.md`
17
- - **OAuth scope catalog**: `@lunch-money/v2-api-spec/oauth-scopes`
18
17
 
19
18
  ## Usage
20
19
 
@@ -27,22 +26,6 @@ const specPath = require.resolve('@lunch-money/v2-api-spec/lunch-money-api-v2.ya
27
26
  const spec = fs.readFileSync(specPath, 'utf8');
28
27
  ```
29
28
 
30
- ### Using the OAuth scope catalog
31
-
32
- The package also provides immutable OAuth scope and scope-group definitions.
33
- API scopes identify the V2 operations they authorize by `operationId`.
34
-
35
- ```javascript
36
- const {
37
- getOAuthScopeDefinition,
38
- matchOAuthScopeGroup,
39
- oauthScopeGroups,
40
- oauthScopes,
41
- } = require('@lunch-money/v2-api-spec/oauth-scopes');
42
-
43
- const transactionRead = getOAuthScopeDefinition('transactions:read');
44
- ```
45
-
46
29
  ### Using with OpenAPI Tools
47
30
 
48
31
  ```bash
package/package.json CHANGED
@@ -1,31 +1,16 @@
1
1
  {
2
2
  "name": "@lunch-money/v2-api-spec",
3
- "version": "2.11.1-preview.9",
3
+ "version": "2.11.1",
4
4
  "description": "OpenAPI specification and version history for the Lunch Money v2 API",
5
5
  "main": "lunch-money-api-v2.yaml",
6
- "exports": {
7
- ".": "./lunch-money-api-v2.yaml",
8
- "./lunch-money-api-v2.yaml": "./lunch-money-api-v2.yaml",
9
- "./version-history.md": "./version-history.md",
10
- "./oauth-scopes": {
11
- "types": "./oauth-scopes/index.d.ts",
12
- "require": "./oauth-scopes/index.js",
13
- "default": "./oauth-scopes/index.js"
14
- },
15
- "./package.json": "./package.json",
16
- "./*": "./*"
17
- },
18
6
  "files": [
19
7
  "lunch-money-api-v2.yaml",
20
8
  "version-history.md",
21
- "oauth-scopes",
22
9
  "README.md"
23
10
  ],
24
11
  "scripts": {
25
12
  "prepare": "husky install",
26
- "build:oauth-scopes": "tsc -p tsconfig.oauth-scopes.json",
27
- "test:oauth-scopes": "npm run build:oauth-scopes && node --test test/oauth-scopes/*.test.js",
28
- "prepublishOnly": "npm run build:oauth-scopes && node scripts/prep-for-publish.js",
13
+ "prepublishOnly": "node scripts/prep-for-publish.js",
29
14
  "postpublish": "node scripts/restore-readme.js",
30
15
  "publish:docs": "node scripts/publish-docs.js --tag preview",
31
16
  "publish:spec": "node scripts/publish-spec.js --tag preview",
@@ -45,8 +30,6 @@
45
30
  "author": "",
46
31
  "license": "ISC",
47
32
  "devDependencies": {
48
- "husky": "^8.0.3",
49
- "typescript": "^5.9.0",
50
- "yaml": "^2.8.0"
33
+ "husky": "^8.0.3"
51
34
  }
52
35
  }
@@ -5,12 +5,10 @@ The Lunch Money API spec uses a modified version of SEMVER for its versioning me
5
5
  - The minor version represents the number of main endpoints the current version of the spec supports. For example, a version of the API that supports the /me, /categories, and /transactions endpoints would have a minor version of 3.
6
6
  - The revision number represents the number of updates since the last endpoint was added. For example, each time changes are made to one of the existing three APIs as described above, the revision number will be bumped.
7
7
 
8
- ## v2.11.1 - TBD
9
- - Add the OAuth scope and scope-group catalog mapped to V2 `operationId` values
8
+ ## v2.11.1 - Sep 28, 2026
10
9
  - Add `GET /me/account/settings` and `PUT /me/account/settings` for account-level settings
11
10
  - Add `GET /me/user/settings` and `PUT /me/user/settings` for user-level display and formatting preferences
12
11
  - Add `GET /me/user/account/settings` and `PUT /me/user/account/settings` for settings specific to a user and budgeting account
13
- from `GET /me/account/settings` and `PUT /me/account/settings` to `GET /me/user/account/settings` and `PUT /me/user/account/settings`
14
12
  - Add `include_pending_in_totals` to `GET /me/account/settings` and `PUT /me/account/settings`
15
13
  - Add `default_manual_account_id` to `GET /me/user/account/settings` and `PUT /me/user/account/settings`
16
14
  - Document `GET /budgets/settings` response schema publicly; change `budget_period_quantity` to integer
@@ -1,9 +0,0 @@
1
- import type { OAuthCatalogDefinition, OAuthCatalogValidationOptions, OAuthCatalogValidationResult, OAuthScopeDefinition, OAuthScopeGroupDefinition, OAuthScopeGroupMatch, OAuthScopeName } from './types';
2
- export declare const oauthScopes: readonly OAuthScopeDefinition[];
3
- export declare const oauthScopeGroups: readonly OAuthScopeGroupDefinition[];
4
- export declare const oauthCatalog: OAuthCatalogDefinition;
5
- export declare function getOAuthScopeDefinition(name: OAuthScopeName): OAuthScopeDefinition | undefined;
6
- export declare function getOAuthScopeGroup(id: string): OAuthScopeGroupDefinition | undefined;
7
- export declare function getOAuthScopeForOperationId(operationId: string): OAuthScopeDefinition | undefined;
8
- export declare function matchOAuthScopeGroup(scopeNames: readonly OAuthScopeName[]): OAuthScopeGroupMatch;
9
- export declare function validateOAuthCatalog(definition: OAuthCatalogDefinition, options?: OAuthCatalogValidationOptions): OAuthCatalogValidationResult;
@@ -1,528 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.oauthCatalog = exports.oauthScopeGroups = exports.oauthScopes = void 0;
4
- exports.getOAuthScopeDefinition = getOAuthScopeDefinition;
5
- exports.getOAuthScopeGroup = getOAuthScopeGroup;
6
- exports.getOAuthScopeForOperationId = getOAuthScopeForOperationId;
7
- exports.matchOAuthScopeGroup = matchOAuthScopeGroup;
8
- exports.validateOAuthCatalog = validateOAuthCatalog;
9
- const freezeScope = (scope) => Object.freeze({ ...scope, operationIds: Object.freeze([...scope.operationIds]) });
10
- const freezeGroup = (group) => Object.freeze({ ...group, scopes: Object.freeze([...group.scopes]) });
11
- const scopeDefinitions = [
12
- {
13
- name: 'openid',
14
- description: 'Verify your identity with Lunch Money',
15
- category: 'identity',
16
- operationIds: [],
17
- status: 'active',
18
- },
19
- {
20
- name: 'email',
21
- description: 'View your email address and whether it has been verified',
22
- category: 'identity',
23
- operationIds: [],
24
- status: 'active',
25
- },
26
- {
27
- name: 'profile',
28
- description: 'View your Lunch Money profile information',
29
- category: 'identity',
30
- operationIds: [],
31
- status: 'active',
32
- },
33
- {
34
- name: 'offline_access',
35
- description: 'Continue accessing Lunch Money when you are not actively using the app',
36
- category: 'offline',
37
- operationIds: [],
38
- status: 'active',
39
- },
40
- {
41
- name: 'me:read',
42
- description: 'View your Lunch Money identity and account, user, and budget settings',
43
- category: 'api',
44
- operationIds: ['getMe', 'getAccountSettings', 'getUserSettings', 'getUserAccountSettings'],
45
- status: 'active',
46
- },
47
- {
48
- name: 'me:update',
49
- description: 'Update your Lunch Money account, user, and budget settings',
50
- category: 'api',
51
- operationIds: ['updateAccountSettings', 'updateUserSettings', 'updateUserAccountSettings'],
52
- status: 'active',
53
- },
54
- {
55
- name: 'summary:read',
56
- description: 'View your budget summary',
57
- category: 'api',
58
- operationIds: ['getBudgetSummary'],
59
- status: 'active',
60
- },
61
- {
62
- name: 'categories:read',
63
- description: 'View categories and category groups',
64
- category: 'api',
65
- operationIds: ['getAllCategories', 'getCategoryById'],
66
- status: 'active',
67
- },
68
- {
69
- name: 'categories:create',
70
- description: 'Create categories and category groups',
71
- category: 'api',
72
- operationIds: ['createCategory'],
73
- status: 'active',
74
- },
75
- {
76
- name: 'categories:update',
77
- description: 'Update categories and category groups',
78
- category: 'api',
79
- operationIds: ['updateCategory'],
80
- status: 'active',
81
- },
82
- {
83
- name: 'categories:delete',
84
- description: 'Delete categories and category groups',
85
- category: 'api',
86
- operationIds: ['deleteCategory'],
87
- status: 'active',
88
- },
89
- {
90
- name: 'crypto_manual:read',
91
- description: 'View supported cryptocurrencies and manually managed cryptocurrency balances',
92
- category: 'api',
93
- operationIds: ['getAllCryptocurrencies', 'getAllCryptoManual', 'getCryptoManualById'],
94
- status: 'active',
95
- },
96
- {
97
- name: 'crypto_manual:create',
98
- description: 'Add supported cryptocurrencies and create manually managed cryptocurrency balances',
99
- category: 'api',
100
- operationIds: ['createCryptocurrency', 'createCryptoManual'],
101
- status: 'active',
102
- },
103
- {
104
- name: 'crypto_manual:update',
105
- description: 'Update manually managed cryptocurrency balances',
106
- category: 'api',
107
- operationIds: ['updateCryptoManual'],
108
- status: 'active',
109
- },
110
- {
111
- name: 'crypto_manual:delete',
112
- description: 'Delete manually managed cryptocurrency balances',
113
- category: 'api',
114
- operationIds: ['deleteCryptoManual'],
115
- status: 'active',
116
- },
117
- {
118
- name: 'crypto_synced:read',
119
- description: 'View synced cryptocurrency accounts and balances',
120
- category: 'api',
121
- operationIds: ['getAllCryptoSynced', 'getCryptoSyncedById', 'getCryptoSyncedBalanceBySymbol'],
122
- status: 'active',
123
- },
124
- {
125
- name: 'crypto_synced:update',
126
- description: 'Refresh balances for synced cryptocurrency accounts',
127
- category: 'api',
128
- operationIds: ['refreshCryptoSynced'],
129
- status: 'active',
130
- },
131
- {
132
- name: 'balance_history:read',
133
- description: 'View balance history for accounts and synced cryptocurrency balances',
134
- category: 'api',
135
- operationIds: ['getBalanceHistory', 'getBalanceHistoryForAccount', 'getBalanceHistoryForCryptoSynced'],
136
- status: 'active',
137
- },
138
- {
139
- name: 'balance_history:update',
140
- description: 'Create or update balance history and details for deleted accounts',
141
- category: 'api',
142
- operationIds: ['upsertBalanceHistoryForAccount', 'upsertBalanceHistoryForCryptoSynced', 'updateBalanceHistoryDetails'],
143
- status: 'active',
144
- },
145
- {
146
- name: 'balance_history:delete',
147
- description: 'Delete balance history for accounts and synced cryptocurrency balances',
148
- category: 'api',
149
- operationIds: ['deleteBalanceHistoryForAccount', 'deleteBalanceHistoryForCryptoSynced', 'deleteBalanceHistoryEntry'],
150
- status: 'active',
151
- },
152
- {
153
- name: 'manual_accounts:read',
154
- description: 'View manually managed accounts',
155
- category: 'api',
156
- operationIds: ['getAllManualAccounts', 'getManualAccountById'],
157
- status: 'active',
158
- },
159
- {
160
- name: 'manual_accounts:create',
161
- description: 'Create manually managed accounts',
162
- category: 'api',
163
- operationIds: ['createManualAccount'],
164
- status: 'active',
165
- },
166
- {
167
- name: 'manual_accounts:update',
168
- description: 'Update manually managed accounts',
169
- category: 'api',
170
- operationIds: ['updateManualAccount'],
171
- status: 'active',
172
- },
173
- {
174
- name: 'manual_accounts:delete',
175
- description: 'Delete manually managed accounts',
176
- category: 'api',
177
- operationIds: ['deleteManualAccount'],
178
- status: 'active',
179
- },
180
- {
181
- name: 'plaid_accounts:read',
182
- description: 'View accounts connected through Plaid',
183
- category: 'api',
184
- operationIds: ['getAllPlaidAccounts', 'getPlaidAccountById'],
185
- status: 'active',
186
- },
187
- {
188
- name: 'plaid_accounts:update',
189
- description: 'Request a Plaid account refresh, which may import new transactions',
190
- category: 'api',
191
- operationIds: ['triggerPlaidAccountFetch'],
192
- status: 'active',
193
- },
194
- {
195
- name: 'transactions:read',
196
- description: 'View transactions, including split and group information and attachment metadata',
197
- category: 'api',
198
- operationIds: ['getAllTransactions', 'getTransactionById'],
199
- status: 'active',
200
- },
201
- {
202
- name: 'transactions:create',
203
- description: 'Create transactions',
204
- category: 'api',
205
- operationIds: ['createNewTransactions'],
206
- status: 'active',
207
- },
208
- {
209
- name: 'transactions:update',
210
- description: 'Update, split, unsplit, group, and ungroup transactions',
211
- category: 'api',
212
- operationIds: ['updateTransactions', 'updateTransaction', 'groupTransactions', 'ungroupTransactions', 'splitTransaction', 'unsplitTransaction'],
213
- status: 'active',
214
- },
215
- {
216
- name: 'transactions:delete',
217
- description: 'Delete transactions',
218
- category: 'api',
219
- operationIds: ['deleteTransactions', 'deleteTransactionById'],
220
- status: 'active',
221
- },
222
- {
223
- name: 'transaction_attachments:read',
224
- description: 'Get download access to transaction attachments',
225
- category: 'api',
226
- operationIds: ['getTransactionAttachmentUrl'],
227
- status: 'active',
228
- },
229
- {
230
- name: 'transaction_attachments:create',
231
- description: 'Attach files to transactions',
232
- category: 'api',
233
- operationIds: ['attachFileToTransaction'],
234
- status: 'active',
235
- },
236
- {
237
- name: 'transaction_attachments:delete',
238
- description: 'Delete transaction attachments',
239
- category: 'api',
240
- operationIds: ['deleteTransactionAttachment'],
241
- status: 'active',
242
- },
243
- {
244
- name: 'tags:read',
245
- description: 'View tags',
246
- category: 'api',
247
- operationIds: ['getAllTags', 'getTagById'],
248
- status: 'active',
249
- },
250
- {
251
- name: 'tags:create',
252
- description: 'Create tags',
253
- category: 'api',
254
- operationIds: ['createTag'],
255
- status: 'active',
256
- },
257
- {
258
- name: 'tags:update',
259
- description: 'Update tags',
260
- category: 'api',
261
- operationIds: ['updateTag'],
262
- status: 'active',
263
- },
264
- {
265
- name: 'tags:delete',
266
- description: 'Delete tags',
267
- category: 'api',
268
- operationIds: ['deleteTag'],
269
- status: 'active',
270
- },
271
- {
272
- name: 'recurring_items:read',
273
- description: 'View recurring items',
274
- category: 'api',
275
- operationIds: ['getAllRecurring', 'getRecurringById'],
276
- status: 'active',
277
- },
278
- {
279
- name: 'budgets:read',
280
- description: 'View budget period settings',
281
- category: 'api',
282
- operationIds: ['getBudgetSettings'],
283
- status: 'active',
284
- },
285
- {
286
- name: 'budgets:update',
287
- description: 'Create or update budget amounts',
288
- category: 'api',
289
- operationIds: ['upsertBudget'],
290
- status: 'active',
291
- },
292
- {
293
- name: 'budgets:delete',
294
- description: 'Delete budget amounts',
295
- category: 'api',
296
- operationIds: ['deleteBudget'],
297
- status: 'active',
298
- },
299
- ];
300
- exports.oauthScopes = Object.freeze(scopeDefinitions.map(freezeScope));
301
- const allApiReadScopes = exports.oauthScopes
302
- .filter((scope) => scope.category === 'api' && scope.name.endsWith(':read'))
303
- .map((scope) => scope.name);
304
- const allApiScopes = exports.oauthScopes
305
- .filter((scope) => scope.category === 'api')
306
- .map((scope) => scope.name);
307
- const groupDefinitions = [
308
- {
309
- id: 'read_only',
310
- name: 'Read only',
311
- description: 'View all supported Lunch Money data without changing it',
312
- scopes: allApiReadScopes,
313
- },
314
- {
315
- id: 'transactions',
316
- name: 'Transactions',
317
- description: 'Import, export, organize, and manage transactions and their attachments',
318
- scopes: [
319
- 'transactions:read',
320
- 'transactions:create',
321
- 'transactions:update',
322
- 'transactions:delete',
323
- 'transaction_attachments:read',
324
- 'transaction_attachments:create',
325
- 'transaction_attachments:delete',
326
- 'categories:read',
327
- 'tags:read',
328
- 'manual_accounts:read',
329
- 'plaid_accounts:read',
330
- 'recurring_items:read',
331
- ],
332
- },
333
- {
334
- id: 'budgeting',
335
- name: 'Budgeting',
336
- description: 'Analyze and manage budgets, categories, and tags',
337
- scopes: [
338
- 'summary:read',
339
- 'budgets:read',
340
- 'budgets:update',
341
- 'budgets:delete',
342
- 'categories:read',
343
- 'categories:create',
344
- 'categories:update',
345
- 'categories:delete',
346
- 'tags:read',
347
- 'tags:create',
348
- 'tags:update',
349
- 'tags:delete',
350
- 'recurring_items:read',
351
- ],
352
- },
353
- {
354
- id: 'accounts',
355
- name: 'Accounts',
356
- description: 'Manage accounts, balances, and balance history',
357
- scopes: [
358
- 'manual_accounts:read',
359
- 'manual_accounts:create',
360
- 'manual_accounts:update',
361
- 'manual_accounts:delete',
362
- 'plaid_accounts:read',
363
- 'plaid_accounts:update',
364
- 'crypto_manual:read',
365
- 'crypto_manual:create',
366
- 'crypto_manual:update',
367
- 'crypto_manual:delete',
368
- 'crypto_synced:read',
369
- 'crypto_synced:update',
370
- 'balance_history:read',
371
- 'balance_history:update',
372
- 'balance_history:delete',
373
- ],
374
- },
375
- {
376
- id: 'organize',
377
- name: 'Organize',
378
- description: 'Manage categories and tags used to organize transactions',
379
- scopes: [
380
- 'categories:read',
381
- 'categories:create',
382
- 'categories:update',
383
- 'categories:delete',
384
- 'tags:read',
385
- 'tags:create',
386
- 'tags:update',
387
- 'tags:delete',
388
- 'recurring_items:read',
389
- ],
390
- },
391
- {
392
- id: 'full_access',
393
- name: 'Full access',
394
- description: 'View and manage all supported Lunch Money data',
395
- scopes: allApiScopes,
396
- },
397
- ];
398
- exports.oauthScopeGroups = Object.freeze(groupDefinitions.map(freezeGroup));
399
- const scopeByName = new Map(exports.oauthScopes.map((scope) => [scope.name, scope]));
400
- const groupById = new Map(exports.oauthScopeGroups.map((group) => [group.id, group]));
401
- const scopeByOperationId = new Map(exports.oauthScopes.flatMap((scope) => scope.operationIds.map((operationId) => [operationId, scope])));
402
- exports.oauthCatalog = Object.freeze({
403
- scopes: exports.oauthScopes,
404
- groups: exports.oauthScopeGroups,
405
- });
406
- function getOAuthScopeDefinition(name) {
407
- return scopeByName.get(name);
408
- }
409
- function getOAuthScopeGroup(id) {
410
- return groupById.get(id);
411
- }
412
- function getOAuthScopeForOperationId(operationId) {
413
- return scopeByOperationId.get(operationId);
414
- }
415
- const canonicalSet = (values) => [...values].sort().join('\u0000');
416
- const SCOPE_NAME_PATTERN = /^(?:openid|email|profile|offline_access|[a-z][a-z0-9_]*:(?:read|create|update|delete))$/;
417
- const GROUP_ID_PATTERN = /^[a-z][a-z0-9_]*$/;
418
- function matchOAuthScopeGroup(scopeNames) {
419
- const names = new Set(scopeNames);
420
- if (names.size !== scopeNames.length)
421
- return { status: 'no_match', reason: 'duplicate_scope' };
422
- if (scopeNames.some((name) => !getOAuthScopeDefinition(name))) {
423
- return { status: 'no_match', reason: 'unknown_scope' };
424
- }
425
- const candidate = canonicalSet(scopeNames);
426
- const group = exports.oauthScopeGroups.find((definition) => canonicalSet(definition.scopes) === candidate);
427
- return group ? { status: 'matched', group } : { status: 'no_match', reason: 'no_exact_match' };
428
- }
429
- function validateOAuthCatalog(definition, options = {}) {
430
- const issues = [];
431
- const scopeNames = new Set();
432
- const groupIds = new Set();
433
- const groupSets = new Map();
434
- const operationOwners = new Map();
435
- definition.scopes.forEach((scope, index) => {
436
- const path = `scopes[${index}]`;
437
- if (!SCOPE_NAME_PATTERN.test(scope.name)) {
438
- issues.push({ code: 'invalid_scope_name', path: `${path}.name`, message: `Invalid scope name: ${scope.name}` });
439
- }
440
- if (!scope.description.trim()) {
441
- issues.push({ code: 'missing_description', path: `${path}.description`, message: `Scope ${scope.name} requires a description` });
442
- }
443
- if (scopeNames.has(scope.name)) {
444
- issues.push({ code: 'duplicate_scope_name', path: `${path}.name`, message: `Duplicate scope name: ${scope.name}` });
445
- }
446
- scopeNames.add(scope.name);
447
- if (!['identity', 'offline', 'api'].includes(scope.category)) {
448
- issues.push({ code: 'invalid_category', path: `${path}.category`, message: `Invalid category: ${scope.category}` });
449
- }
450
- if (!['active', 'deprecated', 'retired'].includes(scope.status)) {
451
- issues.push({ code: 'invalid_lifecycle', path: `${path}.status`, message: `Invalid lifecycle: ${scope.status}` });
452
- }
453
- if (scope.category === 'api' && scope.status !== 'retired' && scope.operationIds.length === 0) {
454
- issues.push({ code: 'missing_operation_ids', path: `${path}.operationIds`, message: `API scope ${scope.name} has no operationIds` });
455
- }
456
- if (scope.category !== 'api' && scope.operationIds.length > 0) {
457
- issues.push({ code: 'unexpected_operation_ids', path: `${path}.operationIds`, message: `Protocol scope ${scope.name} cannot own V2 operationIds` });
458
- }
459
- const localIds = new Set();
460
- scope.operationIds.forEach((operationId, operationIndex) => {
461
- const operationPath = `${path}.operationIds[${operationIndex}]`;
462
- if (localIds.has(operationId)) {
463
- issues.push({ code: 'duplicate_operation_id', path: operationPath, message: `Duplicate operationId: ${operationId}` });
464
- }
465
- localIds.add(operationId);
466
- const owner = operationOwners.get(operationId);
467
- if (owner && owner !== scope.name) {
468
- issues.push({ code: 'conflicting_operation_id', path: operationPath, message: `${operationId} is assigned to both ${owner} and ${scope.name}` });
469
- }
470
- else {
471
- operationOwners.set(operationId, scope.name);
472
- }
473
- if (options.knownOperationIds && !options.knownOperationIds.has(operationId)) {
474
- issues.push({ code: 'unknown_operation_id', path: operationPath, message: `Unknown V2 operationId: ${operationId}` });
475
- }
476
- });
477
- });
478
- definition.groups.forEach((group, index) => {
479
- const path = `groups[${index}]`;
480
- if (!GROUP_ID_PATTERN.test(group.id)) {
481
- issues.push({ code: 'invalid_group_id', path: `${path}.id`, message: `Invalid group ID: ${group.id}` });
482
- }
483
- if (!group.description.trim()) {
484
- issues.push({ code: 'missing_description', path: `${path}.description`, message: `Group ${group.id} requires a description` });
485
- }
486
- if (groupIds.has(group.id)) {
487
- issues.push({ code: 'duplicate_group_id', path: `${path}.id`, message: `Duplicate group ID: ${group.id}` });
488
- }
489
- groupIds.add(group.id);
490
- if (group.scopes.length === 0) {
491
- issues.push({ code: 'empty_group', path: `${path}.scopes`, message: `Group ${group.id} cannot be empty` });
492
- }
493
- const members = new Set();
494
- group.scopes.forEach((scopeName, scopeIndex) => {
495
- const memberPath = `${path}.scopes[${scopeIndex}]`;
496
- if (members.has(scopeName)) {
497
- issues.push({ code: 'duplicate_group_scope', path: memberPath, message: `Duplicate group scope: ${scopeName}` });
498
- }
499
- members.add(scopeName);
500
- const scope = definition.scopes.find((candidate) => candidate.name === scopeName);
501
- if (!scope) {
502
- issues.push({ code: 'unknown_group_scope', path: memberPath, message: `Unknown group scope: ${scopeName}` });
503
- }
504
- else if (scope.status === 'retired') {
505
- issues.push({ code: 'retired_group_scope', path: memberPath, message: `Retired scope cannot be grouped: ${scopeName}` });
506
- }
507
- else if (scope.category === 'offline') {
508
- issues.push({ code: 'offline_scope_in_group', path: memberPath, message: `Offline scope cannot be grouped: ${scopeName}` });
509
- }
510
- });
511
- const setKey = canonicalSet(group.scopes);
512
- const existingGroup = groupSets.get(setKey);
513
- if (existingGroup) {
514
- issues.push({ code: 'duplicate_group_members', path: `${path}.scopes`, message: `${group.id} has the same members as ${existingGroup}` });
515
- }
516
- else {
517
- groupSets.set(setKey, group.id);
518
- }
519
- });
520
- if (options.operationIdCoverage === 'require-all-mapped' && options.knownOperationIds) {
521
- for (const operationId of options.knownOperationIds) {
522
- if (!operationOwners.has(operationId)) {
523
- issues.push({ code: 'unmapped_operation_id', path: 'scopes', message: `Unmapped V2 operationId: ${operationId}` });
524
- }
525
- }
526
- }
527
- return Object.freeze({ valid: issues.length === 0, issues: Object.freeze(issues) });
528
- }
@@ -1,2 +0,0 @@
1
- export { getOAuthScopeDefinition, getOAuthScopeForOperationId, getOAuthScopeGroup, matchOAuthScopeGroup, oauthCatalog, oauthScopeGroups, oauthScopes, validateOAuthCatalog, } from './catalog';
2
- export type { KnownOAuthScopeName, OAuthApiScopeAction, OAuthCatalogDefinition, OAuthCatalogIssue, OAuthCatalogIssueCode, OAuthCatalogValidationOptions, OAuthCatalogValidationResult, OAuthScopeCategory, OAuthScopeDefinition, OAuthScopeGroupDefinition, OAuthScopeGroupMatch, OAuthScopeLifecycle, OAuthScopeName, OAuthScopeResource, } from './types';
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateOAuthCatalog = exports.oauthScopes = exports.oauthScopeGroups = exports.oauthCatalog = exports.matchOAuthScopeGroup = exports.getOAuthScopeGroup = exports.getOAuthScopeForOperationId = exports.getOAuthScopeDefinition = void 0;
4
- var catalog_1 = require("./catalog");
5
- Object.defineProperty(exports, "getOAuthScopeDefinition", { enumerable: true, get: function () { return catalog_1.getOAuthScopeDefinition; } });
6
- Object.defineProperty(exports, "getOAuthScopeForOperationId", { enumerable: true, get: function () { return catalog_1.getOAuthScopeForOperationId; } });
7
- Object.defineProperty(exports, "getOAuthScopeGroup", { enumerable: true, get: function () { return catalog_1.getOAuthScopeGroup; } });
8
- Object.defineProperty(exports, "matchOAuthScopeGroup", { enumerable: true, get: function () { return catalog_1.matchOAuthScopeGroup; } });
9
- Object.defineProperty(exports, "oauthCatalog", { enumerable: true, get: function () { return catalog_1.oauthCatalog; } });
10
- Object.defineProperty(exports, "oauthScopeGroups", { enumerable: true, get: function () { return catalog_1.oauthScopeGroups; } });
11
- Object.defineProperty(exports, "oauthScopes", { enumerable: true, get: function () { return catalog_1.oauthScopes; } });
12
- Object.defineProperty(exports, "validateOAuthCatalog", { enumerable: true, get: function () { return catalog_1.validateOAuthCatalog; } });
@@ -1,50 +0,0 @@
1
- export type OAuthScopeCategory = 'identity' | 'offline' | 'api';
2
- export type OAuthScopeLifecycle = 'active' | 'deprecated' | 'retired';
3
- export type OAuthApiScopeAction = 'read' | 'create' | 'update' | 'delete';
4
- export type OAuthScopeResource = 'me' | 'summary' | 'categories' | 'crypto_manual' | 'crypto_synced' | 'balance_history' | 'manual_accounts' | 'plaid_accounts' | 'transactions' | 'transaction_attachments' | 'tags' | 'recurring_items' | 'budgets';
5
- /**
6
- * Durable scope names currently recognized by this catalog version.
7
- * Persisted and API-facing scope values remain strings so newer names can be
8
- * preserved and reported as unknown by consumers with an older catalog.
9
- */
10
- export type KnownOAuthScopeName = 'openid' | 'email' | 'profile' | 'offline_access' | 'me:read' | 'me:update' | 'summary:read' | 'categories:read' | 'categories:create' | 'categories:update' | 'categories:delete' | 'crypto_manual:read' | 'crypto_manual:create' | 'crypto_manual:update' | 'crypto_manual:delete' | 'crypto_synced:read' | 'crypto_synced:update' | 'balance_history:read' | 'balance_history:update' | 'balance_history:delete' | 'manual_accounts:read' | 'manual_accounts:create' | 'manual_accounts:update' | 'manual_accounts:delete' | 'plaid_accounts:read' | 'plaid_accounts:update' | 'transactions:read' | 'transactions:create' | 'transactions:update' | 'transactions:delete' | 'transaction_attachments:read' | 'transaction_attachments:create' | 'transaction_attachments:delete' | 'tags:read' | 'tags:create' | 'tags:update' | 'tags:delete' | 'recurring_items:read' | 'budgets:read' | 'budgets:update' | 'budgets:delete';
11
- export type OAuthScopeName = string;
12
- export interface OAuthScopeDefinition {
13
- readonly name: KnownOAuthScopeName;
14
- readonly description: string;
15
- readonly category: OAuthScopeCategory;
16
- readonly operationIds: readonly string[];
17
- readonly status: OAuthScopeLifecycle;
18
- readonly lifecycleNotice?: string;
19
- }
20
- export interface OAuthScopeGroupDefinition {
21
- readonly id: string;
22
- readonly name: string;
23
- readonly description: string;
24
- readonly scopes: readonly KnownOAuthScopeName[];
25
- }
26
- export interface OAuthCatalogDefinition {
27
- readonly scopes: readonly OAuthScopeDefinition[];
28
- readonly groups: readonly OAuthScopeGroupDefinition[];
29
- }
30
- export type OAuthCatalogIssueCode = 'invalid_scope_name' | 'invalid_group_id' | 'missing_description' | 'duplicate_scope_name' | 'duplicate_group_id' | 'duplicate_group_members' | 'duplicate_group_scope' | 'empty_group' | 'invalid_category' | 'invalid_lifecycle' | 'missing_operation_ids' | 'unexpected_operation_ids' | 'duplicate_operation_id' | 'conflicting_operation_id' | 'unknown_group_scope' | 'retired_group_scope' | 'offline_scope_in_group' | 'unknown_operation_id' | 'unmapped_operation_id';
31
- export interface OAuthCatalogIssue {
32
- readonly code: OAuthCatalogIssueCode;
33
- readonly path: string;
34
- readonly message: string;
35
- }
36
- export interface OAuthCatalogValidationOptions {
37
- readonly knownOperationIds?: ReadonlySet<string>;
38
- readonly operationIdCoverage?: 'allow-unmapped' | 'require-all-mapped';
39
- }
40
- export interface OAuthCatalogValidationResult {
41
- readonly valid: boolean;
42
- readonly issues: readonly OAuthCatalogIssue[];
43
- }
44
- export type OAuthScopeGroupMatch = {
45
- readonly status: 'matched';
46
- readonly group: OAuthScopeGroupDefinition;
47
- } | {
48
- readonly status: 'no_match';
49
- readonly reason: 'duplicate_scope' | 'unknown_scope' | 'no_exact_match';
50
- };
@@ -1,2 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });