a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm
101
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(lexicon-doc): raise error for incorrect permission resources

Mary 30fcaaf0 bab10f4c

+16 -279
+5
.changeset/odd-shrimps-crash.md
··· 1 + --- 2 + '@atcute/lexicon-doc': patch 3 + --- 4 + 5 + remove permissions that can't be represented in a permission set
+5
.changeset/whole-meals-smash.md
··· 1 + --- 2 + '@atcute/lexicon-doc': patch 3 + --- 4 + 5 + issue refinement error for incorrect permission resource
-117
packages/lexicons/lexicon-doc/lib/builder.test.ts
··· 1 1 import { describe, expect, test } from 'vitest'; 2 2 3 3 import { 4 - accountPermission, 5 4 array, 6 5 blob, 7 - blobPermission, 8 6 boolean, 9 7 build, 10 8 bytes, 11 9 document, 12 - identityPermission, 13 10 integer, 14 11 nullable, 15 12 object, ··· 991 988 }); 992 989 }); 993 990 994 - describe('blobPermission', () => { 995 - test('accepts array of mime types', () => { 996 - expect(() => blobPermission({ accept: ['image/png', 'image/jpeg'] })).not.toThrow(); 997 - }); 998 - 999 - test('accepts wildcards in array', () => { 1000 - expect(() => blobPermission({ accept: ['*/*'] })).not.toThrow(); 1001 - expect(() => blobPermission({ accept: ['image/*'] })).not.toThrow(); 1002 - }); 1003 - 1004 - test('throws on empty array', () => { 1005 - expect(() => blobPermission({ accept: [] })).toThrow("blob-permission/accept: value can't be empty"); 1006 - }); 1007 - }); 1008 - 1009 - describe('accountPermission', () => { 1010 - test('accepts valid attributes', () => { 1011 - expect(() => accountPermission({ attr: 'email' })).not.toThrow(); 1012 - expect(() => accountPermission({ attr: 'repo' })).not.toThrow(); 1013 - expect(() => accountPermission({ attr: 'status' })).not.toThrow(); 1014 - }); 1015 - 1016 - test('accepts actions', () => { 1017 - expect(() => accountPermission({ attr: 'email', action: 'read' })).not.toThrow(); 1018 - expect(() => accountPermission({ attr: 'email', action: 'manage' })).not.toThrow(); 1019 - }); 1020 - }); 1021 - 1022 - describe('identityPermission', () => { 1023 - test('accepts valid attributes', () => { 1024 - expect(() => identityPermission({ attr: 'handle' })).not.toThrow(); 1025 - expect(() => identityPermission({ attr: '*' })).not.toThrow(); 1026 - }); 1027 - }); 1028 - 1029 991 describe('permissionSet', () => { 1030 992 test('accepts permissions array', () => { 1031 993 expect(() => ··· 1113 1075 resource: 'rpc', 1114 1076 lxm: ['com.example.method'], 1115 1077 aud: 'did:web:example.com#bsky_appview', 1116 - }, 1117 - ], 1118 - }); 1119 - }); 1120 - 1121 - test('builds blob permission', () => { 1122 - const docs = build({ 1123 - documents: [ 1124 - document({ 1125 - id: 'com.example.scope', 1126 - defs: { 1127 - main: permissionSet({ 1128 - permissions: [blobPermission({ accept: ['image/png', 'image/jpeg'] })], 1129 - }), 1130 - }, 1131 - }), 1132 - ], 1133 - }); 1134 - 1135 - expect(docs['com.example.scope'].defs.main).toEqual({ 1136 - type: 'permission-set', 1137 - permissions: [ 1138 - { 1139 - type: 'permission', 1140 - resource: 'blob', 1141 - accept: ['image/png', 'image/jpeg'], 1142 - }, 1143 - ], 1144 - }); 1145 - }); 1146 - 1147 - test('builds account permission', () => { 1148 - const docs = build({ 1149 - documents: [ 1150 - document({ 1151 - id: 'com.example.scope', 1152 - defs: { 1153 - main: permissionSet({ 1154 - permissions: [accountPermission({ attr: 'email', action: 'manage' })], 1155 - }), 1156 - }, 1157 - }), 1158 - ], 1159 - }); 1160 - 1161 - expect(docs['com.example.scope'].defs.main).toEqual({ 1162 - type: 'permission-set', 1163 - permissions: [ 1164 - { 1165 - type: 'permission', 1166 - resource: 'account', 1167 - attr: 'email', 1168 - action: 'manage', 1169 - }, 1170 - ], 1171 - }); 1172 - }); 1173 - 1174 - test('builds identity permission', () => { 1175 - const docs = build({ 1176 - documents: [ 1177 - document({ 1178 - id: 'com.example.scope', 1179 - defs: { 1180 - main: permissionSet({ 1181 - permissions: [identityPermission({ attr: 'handle' })], 1182 - }), 1183 - }, 1184 - }), 1185 - ], 1186 - }); 1187 - 1188 - expect(docs['com.example.scope'].defs.main).toEqual({ 1189 - type: 'permission-set', 1190 - permissions: [ 1191 - { 1192 - type: 'permission', 1193 - resource: 'identity', 1194 - attr: 'handle', 1195 1078 }, 1196 1079 ], 1197 1080 });
+1 -123
packages/lexicons/lexicon-doc/lib/builder.ts
··· 1362 1362 return { ...def, type: 'rpc-permission' }; 1363 1363 }; 1364 1364 1365 - export type BlobAccept = `${string}/${string}`; 1366 - 1367 - /** 1368 - * builder definition for blob permissions 1369 - */ 1370 - export interface LexBlobPermissionBuilder { 1371 - type: 'blob-permission'; 1372 - /** accepted MIME types */ 1373 - accept: BlobAccept[]; 1374 - } 1375 - 1376 - /** 1377 - * builds a blob permission definition 1378 - * @param def permission definition parameters 1379 - * @returns blob permission builder definition 1380 - */ 1381 - export const blobPermission = (def: Omit<LexBlobPermissionBuilder, 'type'>): LexBlobPermissionBuilder => { 1382 - const { accept } = def; 1383 - 1384 - if (accept.length === 0) { 1385 - throw new Error(`blob-permission/accept: value can't be empty`); 1386 - } 1387 - 1388 - if (accept.includes('*/*')) { 1389 - if (accept.length > 1) { 1390 - throw new Error( 1391 - `blob-permission/accept: no other MIME types can be specified when a wildcard is present`, 1392 - ); 1393 - } 1394 - } else { 1395 - for (let idx = 0, len = accept.length; idx < len; idx++) { 1396 - const mime = accept[idx]; 1397 - 1398 - if (!MIME_TYPE_RE.test(mime)) { 1399 - throw new Error(`blob-permission/accept[${idx}]: invalid MIME type (${mime})`); 1400 - } 1401 - } 1402 - } 1403 - 1404 - return { ...def, type: 'blob-permission' }; 1405 - }; 1406 - 1407 - export type AccountAction = 'read' | 'manage'; 1408 - export type AccountAttribute = 'email' | 'repo' | 'status'; 1409 - 1410 - /** 1411 - * builder definition for account permissions 1412 - */ 1413 - export interface LexAccountPermissionBuilder { 1414 - type: 'account-permission'; 1415 - /** targeted account attribute */ 1416 - attr: AccountAttribute; 1417 - /** permitted action */ 1418 - action?: AccountAction; 1419 - } 1420 - 1421 - /** 1422 - * builds an account permission definition 1423 - * @param def permission definition parameters 1424 - * @returns account permission builder definition 1425 - */ 1426 - export const accountPermission = ( 1427 - def: Omit<LexAccountPermissionBuilder, 'type'>, 1428 - ): LexAccountPermissionBuilder => { 1429 - return { ...def, type: 'account-permission' }; 1430 - }; 1431 - 1432 - export type IdentityAttribute = 'handle' | '*'; 1433 - 1434 - /** 1435 - * builder definition for identity permissions 1436 - */ 1437 - export interface LexIdentityPermissionBuilder { 1438 - type: 'identity-permission'; 1439 - /** targeted identity attribute */ 1440 - attr: IdentityAttribute; 1441 - } 1442 - 1443 - /** 1444 - * builds an identity permission definition 1445 - * @param def permission definition parameters 1446 - * @returns identity permission builder definition 1447 - */ 1448 - export const identityPermission = ( 1449 - def: Omit<LexIdentityPermissionBuilder, 'type'>, 1450 - ): LexIdentityPermissionBuilder => { 1451 - return { ...def, type: 'identity-permission' }; 1452 - }; 1453 - 1454 - export type LexPermissionBuilder = 1455 - | LexRepoPermissionBuilder 1456 - | LexRpcPermissionBuilder 1457 - | LexBlobPermissionBuilder 1458 - | LexAccountPermissionBuilder 1459 - | LexIdentityPermissionBuilder; 1365 + export type LexPermissionBuilder = LexRepoPermissionBuilder | LexRpcPermissionBuilder; 1460 1366 1461 1367 const buildPermissionSchema = (ctx: BuildContext, def: LexPermissionBuilder): t.LexPermission => { 1462 1368 switch (def.type) { ··· 1514 1420 inheritAud: inheritAud, 1515 1421 lxm: builtLxm, 1516 1422 resource: 'rpc', 1517 - type: 'permission', 1518 - }; 1519 - } 1520 - case 'blob-permission': { 1521 - const { accept } = def; 1522 - 1523 - return { 1524 - accept: accept, 1525 - resource: 'blob', 1526 - type: 'permission', 1527 - }; 1528 - } 1529 - case 'account-permission': { 1530 - const { attr, action } = def; 1531 - 1532 - return { 1533 - action: action, 1534 - attr: attr, 1535 - resource: 'account', 1536 - type: 'permission', 1537 - }; 1538 - } 1539 - case 'identity-permission': { 1540 - const { attr } = def; 1541 - 1542 - return { 1543 - attr: attr, 1544 - resource: 'identity', 1545 1423 type: 'permission', 1546 1424 }; 1547 1425 }
+5 -39
packages/lexicons/lexicon-doc/lib/refinements.ts
··· 816 816 817 817 break; 818 818 } 819 - case 'blob': { 820 - const { accept } = spec; 821 - 822 - if (!Array.isArray(accept)) { 823 - issues.push({ 824 - message: `accept must be an array`, 825 - path: ['accept'], 826 - }); 827 - } else if (accept.length === 0) { 828 - issues.push({ 829 - message: `accept can't be empty`, 830 - path: ['accept'], 831 - }); 832 - } else if (accept.includes('*/*')) { 833 - if (accept.length > 1) { 834 - issues.push({ 835 - message: `no other MIME types can be specified when a wildcard is present`, 836 - path: ['accept'], 837 - }); 838 - } 839 - } else { 840 - for (let idx = 0, len = accept.length; idx < len; idx++) { 841 - const entry = accept[idx]; 842 - 843 - if (typeof entry !== 'string') { 844 - issues.push({ 845 - message: `accept entries must be strings`, 846 - path: ['accept', idx], 847 - }); 848 - } else if (!MIME_TYPE_RE.test(entry)) { 849 - issues.push({ 850 - message: `invalid MIME type`, 851 - path: ['accept', idx], 852 - }); 853 - } 854 - } 855 - } 856 - 857 - break; 819 + default: { 820 + issues.push({ 821 + message: `invalid permission resource '${resource}'`, 822 + path: ['resource'], 823 + }); 858 824 } 859 825 } 860 826