@lunch-money/v2-api-spec 2.11.1-preview.1 → 2.11.1-preview.10
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 +21 -66
- package/lunch-money-api-v2.yaml +760 -405
- package/oauth-client-logo/index.d.ts +27 -0
- package/oauth-client-logo/index.js +32 -0
- package/oauth-scopes/catalog.d.ts +9 -0
- package/oauth-scopes/catalog.js +528 -0
- package/oauth-scopes/index.d.ts +2 -0
- package/oauth-scopes/index.js +12 -0
- package/oauth-scopes/types.d.ts +50 -0
- package/oauth-scopes/types.js +2 -0
- package/package.json +34 -9
- package/version-history.md +25 -19
- package/update-urls.sh +0 -135
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared preflight and authoritative-validation policy for OAuth client logos.
|
|
3
|
+
*
|
|
4
|
+
* Client-side validation improves feedback, but API implementations must
|
|
5
|
+
* independently validate uploaded bytes and remain authoritative.
|
|
6
|
+
*/
|
|
7
|
+
export declare const oauthClientLogoPolicy: Readonly<{
|
|
8
|
+
readonly id: "oauth-client-logo-v1";
|
|
9
|
+
readonly acceptedMediaTypes: readonly ["image/png", "image/jpeg", "image/webp"];
|
|
10
|
+
readonly maxBytes: 1000000;
|
|
11
|
+
readonly minWidth: 200;
|
|
12
|
+
readonly minHeight: 200;
|
|
13
|
+
readonly maxWidth: 4096;
|
|
14
|
+
readonly maxHeight: 4096;
|
|
15
|
+
readonly maxPixels: 16000000;
|
|
16
|
+
readonly minAspectRatio: 0.95;
|
|
17
|
+
readonly maxAspectRatio: 1.05;
|
|
18
|
+
readonly allowAnimation: false;
|
|
19
|
+
readonly recommendations: Readonly<{
|
|
20
|
+
width: 512;
|
|
21
|
+
height: 512;
|
|
22
|
+
avoidText: true;
|
|
23
|
+
supportLightAndDarkBackgrounds: true;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
export type OAuthClientLogoMediaType = typeof oauthClientLogoPolicy.acceptedMediaTypes[number];
|
|
27
|
+
export type OAuthClientLogoErrorCode = 'logo_required' | 'logo_file_too_large' | 'unsupported_logo_media_type' | 'invalid_logo_image' | 'invalid_logo_dimensions' | 'invalid_logo_aspect_ratio' | 'animated_logo_not_supported' | 'logo_processing_failed';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.oauthClientLogoPolicy = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Shared preflight and authoritative-validation policy for OAuth client logos.
|
|
6
|
+
*
|
|
7
|
+
* Client-side validation improves feedback, but API implementations must
|
|
8
|
+
* independently validate uploaded bytes and remain authoritative.
|
|
9
|
+
*/
|
|
10
|
+
exports.oauthClientLogoPolicy = Object.freeze({
|
|
11
|
+
id: 'oauth-client-logo-v1',
|
|
12
|
+
acceptedMediaTypes: Object.freeze([
|
|
13
|
+
'image/png',
|
|
14
|
+
'image/jpeg',
|
|
15
|
+
'image/webp',
|
|
16
|
+
]),
|
|
17
|
+
maxBytes: 1_000_000,
|
|
18
|
+
minWidth: 200,
|
|
19
|
+
minHeight: 200,
|
|
20
|
+
maxWidth: 4096,
|
|
21
|
+
maxHeight: 4096,
|
|
22
|
+
maxPixels: 16_000_000,
|
|
23
|
+
minAspectRatio: 0.95,
|
|
24
|
+
maxAspectRatio: 1.05,
|
|
25
|
+
allowAnimation: false,
|
|
26
|
+
recommendations: Object.freeze({
|
|
27
|
+
width: 512,
|
|
28
|
+
height: 512,
|
|
29
|
+
avoidText: true,
|
|
30
|
+
supportLightAndDarkBackgrounds: true,
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
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;
|
|
@@ -0,0 +1,528 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
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';
|
|
@@ -0,0 +1,12 @@
|
|
|
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; } });
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
};
|